diff --git a/.github/workflows/update-registry.yml b/.github/workflows/update-registry.yml new file mode 100644 index 0000000..94ade1d --- /dev/null +++ b/.github/workflows/update-registry.yml @@ -0,0 +1,75 @@ +name: Update Component Registry + +on: + push: + branches: + - main + paths: + - '.opencode/**' + - '!registry.json' + workflow_dispatch: + +permissions: + contents: write + pull-requests: write + +jobs: + update-registry: + runs-on: ubuntu-latest + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - name: Install dependencies + run: | + sudo apt-get update + sudo apt-get install -y jq + + - name: Run component registration + run: | + chmod +x scripts/register-component.sh + ./scripts/register-component.sh + + - name: Check for changes + id: check_changes + run: | + if git diff --quiet registry.json; then + echo "changed=false" >> $GITHUB_OUTPUT + echo "No changes to registry.json" + else + echo "changed=true" >> $GITHUB_OUTPUT + echo "Registry has been updated" + fi + + - name: Commit and push changes + if: steps.check_changes.outputs.changed == 'true' + run: | + git config --local user.email "github-actions[bot]@users.noreply.github.com" + git config --local user.name "github-actions[bot]" + git add registry.json + git commit -m "chore: auto-update component registry [skip ci]" + git push + + - name: Summary + if: steps.check_changes.outputs.changed == 'true' + run: | + echo "✅ Registry updated successfully" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + echo "### Updated Components" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + jq -r ' + "- **Agents:** \(.components.agents | length)", + "- **Subagents:** \(.components.subagents | length)", + "- **Commands:** \(.components.commands | length)", + "- **Tools:** \(.components.tools | length)", + "- **Plugins:** \(.components.plugins | length)", + "- **Contexts:** \(.components.contexts | length)" + ' registry.json >> $GITHUB_STEP_SUMMARY + + - name: No changes summary + if: steps.check_changes.outputs.changed == 'false' + run: | + echo "â„šī¸ No changes to registry" >> $GITHUB_STEP_SUMMARY diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000..7e39911 --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,237 @@ +# Contributing to OpenCode Agents + +Thank you for your interest in contributing! This guide will help you add new components to the registry. + +## Quick Start + +1. Fork the repository +2. Create a new branch for your feature +3. Add your component +4. Test it works +5. Submit a pull request + +## Adding New Components + +### Component Types + +- **Agents** (`.opencode/agent/*.md`) - Main AI agents +- **Subagents** (`.opencode/agent/subagents/*.md`) - Specialized helpers +- **Commands** (`.opencode/command/*.md`) - Slash commands +- **Tools** (`.opencode/tool/*/index.ts`) - Utility tools +- **Plugins** (`.opencode/plugin/*.ts`) - Integrations +- **Contexts** (`.opencode/context/**/*.md`) - Context files + +### Component Structure + +#### For Markdown Files (Agents, Commands, Contexts) + +All markdown files should include YAML frontmatter: + +```markdown +--- +description: "Brief description of what this does" +mode: primary # For agents only +model: claude-4-sonnet # For agents only +temperature: 0.1 # For agents only +tools: # For agents only + read: true + edit: true + write: true +permissions: # Optional + bash: + "*": "deny" +--- + +# Component Name + +Your component content here... +``` + +**Required fields:** +- `description` - Brief description (all components) + +**Agent-specific fields:** +- `mode` - Agent mode (primary, secondary, etc.) +- `model` - AI model to use +- `temperature` - Temperature setting +- `tools` - Available tools +- `permissions` - Security permissions + +#### For TypeScript Files (Tools, Plugins) + +Include JSDoc comments at the top: + +```typescript +/** + * Tool Name + * + * Brief description of what this tool does + */ + +export function myTool() { + // Implementation +} +``` + +### File Naming Conventions + +- **kebab-case** for file names: `my-new-agent.md` +- **PascalCase** for TypeScript types/interfaces +- **camelCase** for variables and functions + +### Adding Your Component + +1. **Create the component file** in the appropriate directory: + ```bash + # Example: Adding a new agent + touch .opencode/agent/my-new-agent.md + ``` + +2. **Add frontmatter and content** following the structure above + +3. **Test your component**: + ```bash + # Validate structure + ./scripts/validate-component.sh + ``` + +4. **Update the registry** (automatic on merge to main): + ```bash + # Manual update (optional) + ./scripts/register-component.sh + ``` + +## Component Categories + +When adding components, they're automatically categorized: + +- **core** - Essential components included in minimal installs +- **extended** - Additional features for developer profile +- **advanced** - Experimental or specialized components + +The auto-registration script assigns categories based on component type and location. + +## Testing Your Component + +### Local Testing + +1. **Install locally**: + ```bash + # Test the installer + ./install.sh --list + ``` + +2. **Validate structure**: + ```bash + ./scripts/validate-component.sh + ``` + +3. **Test with OpenCode**: + ```bash + opencode --agent your-new-agent + ``` + +### Automated Testing + +When you submit a PR, GitHub Actions will: +- Validate component structure +- Update the registry +- Run validation checks + +## Pull Request Guidelines + +### PR Title Format + +Use conventional commits: +- `feat: add new agent for X` +- `fix: correct issue in Y command` +- `docs: update Z documentation` +- `chore: update dependencies` + +### PR Description + +Include: +1. **What** - What component are you adding/changing? +2. **Why** - Why is this useful? +3. **How** - How does it work? +4. **Testing** - How did you test it? + +Example: +```markdown +## What +Adds a new `database-agent` for managing database migrations. + +## Why +Automates common database tasks and ensures migration safety. + +## How +- Scans migration files +- Validates migration order +- Runs migrations with rollback support + +## Testing +- [x] Validated with `./scripts/validate-component.sh` +- [x] Tested with PostgreSQL and MySQL +- [x] Tested rollback scenarios +``` + +## Component Dependencies + +If your component depends on others, declare them in the registry: + +```json +{ + "id": "my-component", + "dependencies": ["tool:env", "agent:task-manager"] +} +``` + +The installer will automatically install dependencies. + +## Registry Auto-Update + +The registry is automatically updated when: +- You push to `main` branch +- Changes are made to `.opencode/` directory + +The GitHub Action: +1. Scans all components +2. Extracts metadata +3. Updates `registry.json` +4. Commits changes + +You don't need to manually edit `registry.json`! + +## Code Style + +### Markdown +- Use clear, concise language +- Include examples +- Add code blocks with syntax highlighting +- Use proper heading hierarchy + +### TypeScript +- Follow existing code style +- Add JSDoc comments +- Use TypeScript types (no `any`) +- Export functions explicitly + +### Bash Scripts +- Use `set -e` for error handling +- Add comments for complex logic +- Use meaningful variable names +- Include help text + +## Questions? + +- **Issues**: Open an issue for bugs or feature requests +- **Discussions**: Use GitHub Discussions for questions +- **Security**: Email security issues privately + +## License + +By contributing, you agree that your contributions will be licensed under the MIT License. + +--- + +Thank you for contributing! 🎉 diff --git a/README.md b/README.md index 679a04b..c56a192 100644 --- a/README.md +++ b/README.md @@ -23,6 +23,31 @@ https://opencode.ai/docs ``` ### Step 2: Install Agents & Commands + +**Option A: Interactive Installer (Recommended)** +```bash +curl -fsSL https://raw.githubusercontent.com/darrenhinde/opencode-agents/main/install.sh | bash +``` + +The installer offers: +- đŸŽ¯ **Quick Profiles**: Core, Developer, Full, or Advanced +- 🎨 **Custom Selection**: Pick exactly what you need +- đŸ“Ļ **Smart Dependencies**: Auto-installs required components +- ✨ **Interactive Menus**: User-friendly component browser + +**Option B: Profile-Based Install** +```bash +# Core essentials only +curl -fsSL https://raw.githubusercontent.com/darrenhinde/opencode-agents/main/install.sh | bash -s core + +# Balanced for daily development +curl -fsSL https://raw.githubusercontent.com/darrenhinde/opencode-agents/main/install.sh | bash -s developer + +# Everything included +curl -fsSL https://raw.githubusercontent.com/darrenhinde/opencode-agents/main/install.sh | bash -s full +``` + +**Option C: Manual Install** ```bash # Clone this repository git clone https://github.com/darrenhinde/opencode-agents.git @@ -220,7 +245,11 @@ A: The agents work with any language (TypeScript, Python, Go, Rust, etc.) and ad A: It's a teaching document explaining architecture patterns and how to extend the system. **Q: Can I use just one command or agent?** -A: Yes! Cherry-pick individual files with curl: +A: Yes! Use the installer's list feature to see all components: +```bash +./install.sh --list +``` +Or cherry-pick individual files with curl: ```bash curl -o ~/.opencode/agent/codebase-agent.md \ https://raw.githubusercontent.com/darrenhinde/opencode-agents/main/.opencode/agent/codebase-agent.md @@ -228,6 +257,54 @@ curl -o ~/.opencode/agent/codebase-agent.md \ --- +## Installation Profiles + +The installer offers four pre-configured profiles: + +### đŸŽ¯ Core (Minimal) +Essential agents and commands for basic OpenCode usage. +- **Agents**: task-manager, codebase-agent +- **Subagents**: reviewer, tester, documentation, coder-agent, build-agent +- **Commands**: test, commit, context +- **Tools**: env utilities +- **Best for**: Getting started, minimal setup + +### đŸ’ŧ Developer (Recommended) +Balanced setup for daily development work. +- Everything in Core, plus: +- **Agents**: image-specialist, workflow-orchestrator +- **Subagents**: codebase-pattern-analyst +- **Commands**: clean, optimize, prompt-enhancer +- **Tools**: Gemini AI integration +- **Plugins**: Telegram notifications +- **Best for**: Most developers, daily use + +### đŸ“Ļ Full +Complete installation with all available components. +- Everything in Developer, plus: +- **Commands**: worktrees (git worktree management) +- **Best for**: Power users, exploring all features + +### 🚀 Advanced +Full installation plus experimental features and planning docs. +- Everything in Full, plus: +- **Additional**: .Building/ directory, GitHub workflows +- **Best for**: Contributors, learning the architecture + +## Updating Components + +Keep your components up to date: + +```bash +# Update all installed components +./update.sh + +# Or re-run the installer +curl -fsSL https://raw.githubusercontent.com/darrenhinde/opencode-agents/main/install.sh | bash +``` + +--- + ## Advanced ### Understanding the System diff --git a/install.sh b/install.sh new file mode 100755 index 0000000..4ba6708 --- /dev/null +++ b/install.sh @@ -0,0 +1,609 @@ +#!/usr/bin/env bash + +############################################################################# +# OpenCode Agents Installer +# Interactive installer for OpenCode agents, commands, tools, and plugins +############################################################################# + +set -e + +# Colors for output +RED='\033[0;31m' +GREEN='\033[0;32m' +YELLOW='\033[1;33m' +BLUE='\033[0;34m' +MAGENTA='\033[0;35m' +CYAN='\033[0;36m' +BOLD='\033[1m' +NC='\033[0m' # No Color + +# Configuration +REPO_URL="https://github.com/darrenhinde/opencode-agents" +RAW_URL="https://raw.githubusercontent.com/darrenhinde/opencode-agents/main" +REGISTRY_URL="${RAW_URL}/registry.json" +INSTALL_DIR=".opencode" +TEMP_DIR="/tmp/opencode-installer-$$" + +# Global variables +SELECTED_COMPONENTS=() +INSTALL_MODE="" +PROFILE="" + +############################################################################# +# Utility Functions +############################################################################# + +print_header() { + echo -e "${CYAN}${BOLD}" + echo "╔════════════════════════════════════════════════════════════════╗" + echo "║ ║" + echo "║ OpenCode Agents Installer v1.0.0 ║" + echo "║ ║" + echo "╚════════════════════════════════════════════════════════════════╝" + echo -e "${NC}" +} + +print_success() { + echo -e "${GREEN}✓${NC} $1" +} + +print_error() { + echo -e "${RED}✗${NC} $1" +} + +print_info() { + echo -e "${BLUE}ℹ${NC} $1" +} + +print_warning() { + echo -e "${YELLOW}⚠${NC} $1" +} + +print_step() { + echo -e "\n${MAGENTA}${BOLD}â–ļ${NC} $1\n" +} + +############################################################################# +# Dependency Checks +############################################################################# + +check_dependencies() { + print_step "Checking dependencies..." + + local missing_deps=() + + if ! command -v curl &> /dev/null; then + missing_deps+=("curl") + fi + + if ! command -v jq &> /dev/null; then + missing_deps+=("jq") + fi + + if [ ${#missing_deps[@]} -ne 0 ]; then + print_error "Missing required dependencies: ${missing_deps[*]}" + echo "" + echo "Please install them:" + echo " macOS: brew install ${missing_deps[*]}" + echo " Ubuntu: sudo apt-get install ${missing_deps[*]}" + echo " Fedora: sudo dnf install ${missing_deps[*]}" + exit 1 + fi + + print_success "All dependencies found" +} + +############################################################################# +# Registry Functions +############################################################################# + +fetch_registry() { + print_step "Fetching component registry..." + + mkdir -p "$TEMP_DIR" + + if ! curl -fsSL "$REGISTRY_URL" -o "$TEMP_DIR/registry.json"; then + print_error "Failed to fetch registry from $REGISTRY_URL" + exit 1 + fi + + print_success "Registry fetched successfully" +} + +get_profile_components() { + local profile=$1 + jq -r ".profiles.${profile}.components[]" "$TEMP_DIR/registry.json" +} + +get_component_info() { + local component_id=$1 + local component_type=$2 + + jq -r ".components.${component_type}[] | select(.id == \"${component_id}\")" "$TEMP_DIR/registry.json" +} + +resolve_dependencies() { + local component=$1 + local type="${component%%:*}" + local id="${component##*:}" + + # Get dependencies for this component + local deps=$(jq -r ".components.${type}s[] | select(.id == \"${id}\") | .dependencies[]?" "$TEMP_DIR/registry.json" 2>/dev/null || echo "") + + if [ -n "$deps" ]; then + for dep in $deps; do + # Add dependency if not already in list + if [[ ! " ${SELECTED_COMPONENTS[@]} " =~ " ${dep} " ]]; then + SELECTED_COMPONENTS+=("$dep") + # Recursively resolve dependencies + resolve_dependencies "$dep" + fi + done + fi +} + +############################################################################# +# Installation Mode Selection +############################################################################# + +show_main_menu() { + clear + print_header + + echo -e "${BOLD}Choose installation mode:${NC}\n" + echo " 1) Quick Install (Choose a profile)" + echo " 2) Custom Install (Pick individual components)" + echo " 3) List Available Components" + echo " 4) Exit" + echo "" + read -p "Enter your choice [1-4]: " choice + + case $choice in + 1) INSTALL_MODE="profile" ;; + 2) INSTALL_MODE="custom" ;; + 3) list_components; show_main_menu ;; + 4) cleanup_and_exit 0 ;; + *) print_error "Invalid choice"; sleep 2; show_main_menu ;; + esac +} + +############################################################################# +# Profile Installation +############################################################################# + +show_profile_menu() { + clear + print_header + + echo -e "${BOLD}Available Installation Profiles:${NC}\n" + + # Core profile + local core_desc=$(jq -r '.profiles.core.description' "$TEMP_DIR/registry.json") + local core_count=$(jq -r '.profiles.core.components | length' "$TEMP_DIR/registry.json") + echo -e " ${GREEN}1) Core${NC}" + echo -e " ${core_desc}" + echo -e " Components: ${core_count}\n" + + # Developer profile + local dev_desc=$(jq -r '.profiles.developer.description' "$TEMP_DIR/registry.json") + local dev_count=$(jq -r '.profiles.developer.components | length' "$TEMP_DIR/registry.json") + echo -e " ${BLUE}2) Developer${NC}" + echo -e " ${dev_desc}" + echo -e " Components: ${dev_count}\n" + + # Full profile + local full_desc=$(jq -r '.profiles.full.description' "$TEMP_DIR/registry.json") + local full_count=$(jq -r '.profiles.full.components | length' "$TEMP_DIR/registry.json") + echo -e " ${MAGENTA}3) Full${NC}" + echo -e " ${full_desc}" + echo -e " Components: ${full_count}\n" + + # Advanced profile + local adv_desc=$(jq -r '.profiles.advanced.description' "$TEMP_DIR/registry.json") + local adv_count=$(jq -r '.profiles.advanced.components | length' "$TEMP_DIR/registry.json") + echo -e " ${YELLOW}4) Advanced${NC}" + echo -e " ${adv_desc}" + echo -e " Components: ${adv_count}\n" + + echo " 5) Back to main menu" + echo "" + read -p "Enter your choice [1-5]: " choice + + case $choice in + 1) PROFILE="core" ;; + 2) PROFILE="developer" ;; + 3) PROFILE="full" ;; + 4) PROFILE="advanced" ;; + 5) show_main_menu; return ;; + *) print_error "Invalid choice"; sleep 2; show_profile_menu; return ;; + esac + + # Load profile components + mapfile -t SELECTED_COMPONENTS < <(get_profile_components "$PROFILE") + + show_installation_preview +} + +############################################################################# +# Custom Component Selection +############################################################################# + +show_custom_menu() { + clear + print_header + + echo -e "${BOLD}Select component categories to install:${NC}\n" + echo "Use space to toggle, Enter to continue" + echo "" + + local categories=("agents" "subagents" "commands" "tools" "plugins" "contexts" "config") + local selected_categories=() + + # Simple selection (for now, we'll make it interactive later) + echo "Available categories:" + for i in "${!categories[@]}"; do + local cat="${categories[$i]}" + local count=$(jq -r ".components.${cat} | length" "$TEMP_DIR/registry.json") + echo " $((i+1))) ${cat^} (${count} available)" + done + echo " $((${#categories[@]}+1))) Select All" + echo " $((${#categories[@]}+2))) Continue to component selection" + echo " $((${#categories[@]}+3))) Back to main menu" + echo "" + + read -p "Enter category numbers (space-separated) or option: " -a selections + + for sel in "${selections[@]}"; do + if [ "$sel" -eq $((${#categories[@]}+1)) ]; then + selected_categories=("${categories[@]}") + break + elif [ "$sel" -eq $((${#categories[@]}+2)) ]; then + break + elif [ "$sel" -eq $((${#categories[@]}+3)) ]; then + show_main_menu + return + elif [ "$sel" -ge 1 ] && [ "$sel" -le ${#categories[@]} ]; then + selected_categories+=("${categories[$((sel-1))]}") + fi + done + + if [ ${#selected_categories[@]} -eq 0 ]; then + print_warning "No categories selected" + sleep 2 + show_custom_menu + return + fi + + show_component_selection "${selected_categories[@]}" +} + +show_component_selection() { + local categories=("$@") + clear + print_header + + echo -e "${BOLD}Select components to install:${NC}\n" + + local all_components=() + local component_details=() + + for category in "${categories[@]}"; do + echo -e "${CYAN}${BOLD}${category^}:${NC}" + + local components=$(jq -r ".components.${category}[] | .id" "$TEMP_DIR/registry.json") + + local idx=1 + while IFS= read -r comp_id; do + local comp_name=$(jq -r ".components.${category}[] | select(.id == \"${comp_id}\") | .name" "$TEMP_DIR/registry.json") + local comp_desc=$(jq -r ".components.${category}[] | select(.id == \"${comp_id}\") | .description" "$TEMP_DIR/registry.json") + + echo " ${idx}) ${comp_name}" + echo " ${comp_desc}" + + all_components+=("${category}:${comp_id}") + component_details+=("${comp_name}|${comp_desc}") + + idx=$((idx+1)) + done <<< "$components" + + echo "" + done + + echo "Enter component numbers (space-separated), 'all' for all, or 'done' to continue:" + read -a selections + + for sel in "${selections[@]}"; do + if [ "$sel" = "all" ]; then + SELECTED_COMPONENTS=("${all_components[@]}") + break + elif [ "$sel" = "done" ]; then + break + elif [ "$sel" -ge 1 ] && [ "$sel" -le ${#all_components[@]} ]; then + SELECTED_COMPONENTS+=("${all_components[$((sel-1))]}") + fi + done + + if [ ${#SELECTED_COMPONENTS[@]} -eq 0 ]; then + print_warning "No components selected" + sleep 2 + show_custom_menu + return + fi + + # Resolve dependencies + print_step "Resolving dependencies..." + local original_count=${#SELECTED_COMPONENTS[@]} + for comp in "${SELECTED_COMPONENTS[@]}"; do + resolve_dependencies "$comp" + done + + if [ ${#SELECTED_COMPONENTS[@]} -gt $original_count ]; then + print_info "Added $((${#SELECTED_COMPONENTS[@]} - original_count)) dependencies" + fi + + show_installation_preview +} + +############################################################################# +# Installation Preview & Confirmation +############################################################################# + +show_installation_preview() { + clear + print_header + + echo -e "${BOLD}Installation Preview${NC}\n" + + if [ -n "$PROFILE" ]; then + echo -e "Profile: ${GREEN}${PROFILE}${NC}" + else + echo -e "Mode: ${GREEN}Custom${NC}" + fi + + echo -e "\nComponents to install (${#SELECTED_COMPONENTS[@]} total):\n" + + # Group by type + local agents=() + local subagents=() + local commands=() + local tools=() + local plugins=() + local contexts=() + local configs=() + + for comp in "${SELECTED_COMPONENTS[@]}"; do + local type="${comp%%:*}" + case $type in + agent) agents+=("$comp") ;; + subagent) subagents+=("$comp") ;; + command) commands+=("$comp") ;; + tool) tools+=("$comp") ;; + plugin) plugins+=("$comp") ;; + context) contexts+=("$comp") ;; + config) configs+=("$comp") ;; + esac + done + + [ ${#agents[@]} -gt 0 ] && echo -e "${CYAN}Agents (${#agents[@]}):${NC} ${agents[*]##*:}" + [ ${#subagents[@]} -gt 0 ] && echo -e "${CYAN}Subagents (${#subagents[@]}):${NC} ${subagents[*]##*:}" + [ ${#commands[@]} -gt 0 ] && echo -e "${CYAN}Commands (${#commands[@]}):${NC} ${commands[*]##*:}" + [ ${#tools[@]} -gt 0 ] && echo -e "${CYAN}Tools (${#tools[@]}):${NC} ${tools[*]##*:}" + [ ${#plugins[@]} -gt 0 ] && echo -e "${CYAN}Plugins (${#plugins[@]}):${NC} ${plugins[*]##*:}" + [ ${#contexts[@]} -gt 0 ] && echo -e "${CYAN}Contexts (${#contexts[@]}):${NC} ${contexts[*]##*:}" + [ ${#configs[@]} -gt 0 ] && echo -e "${CYAN}Config (${#configs[@]}):${NC} ${configs[*]##*:}" + + echo "" + read -p "Proceed with installation? [Y/n]: " confirm + + if [[ $confirm =~ ^[Nn] ]]; then + print_info "Installation cancelled" + cleanup_and_exit 0 + fi + + perform_installation +} + +############################################################################# +# Installation +############################################################################# + +perform_installation() { + print_step "Installing components..." + + # Check if .opencode already exists + if [ -d "$INSTALL_DIR" ]; then + print_warning "$INSTALL_DIR directory already exists" + read -p "Backup and continue? [Y/n]: " backup_confirm + + if [[ ! $backup_confirm =~ ^[Nn] ]]; then + local backup_dir="${INSTALL_DIR}.backup.$(date +%Y%m%d-%H%M%S)" + mv "$INSTALL_DIR" "$backup_dir" + print_success "Backed up to $backup_dir" + else + print_error "Installation cancelled" + cleanup_and_exit 1 + fi + fi + + # Create directory structure + mkdir -p "$INSTALL_DIR"/{agent/subagents,command,tool,plugin,context/{core,project}} + + local installed=0 + local failed=0 + + for comp in "${SELECTED_COMPONENTS[@]}"; do + local type="${comp%%:*}" + local id="${comp##*:}" + + # Get component path + local path=$(jq -r ".components.${type}s[] | select(.id == \"${id}\") | .path" "$TEMP_DIR/registry.json") + + if [ -z "$path" ] || [ "$path" = "null" ]; then + print_warning "Could not find path for ${comp}" + ((failed++)) + continue + fi + + # Download component + local url="${RAW_URL}/${path}" + local dest="${path}" + + # Create parent directory if needed + mkdir -p "$(dirname "$dest")" + + if curl -fsSL "$url" -o "$dest"; then + print_success "Installed ${type}: ${id}" + ((installed++)) + else + print_error "Failed to install ${type}: ${id}" + ((failed++)) + fi + done + + # Handle additional paths for advanced profile + if [ "$PROFILE" = "advanced" ]; then + local additional_paths=$(jq -r '.profiles.advanced.additionalPaths[]?' "$TEMP_DIR/registry.json") + if [ -n "$additional_paths" ]; then + print_step "Installing additional paths..." + while IFS= read -r path; do + # For directories, we'd need to recursively download + # For now, just note them + print_info "Additional path: $path (manual download required)" + done <<< "$additional_paths" + fi + fi + + echo "" + print_success "Installation complete!" + echo -e " Installed: ${GREEN}${installed}${NC}" + [ $failed -gt 0 ] && echo -e " Failed: ${RED}${failed}${NC}" + + show_post_install +} + +############################################################################# +# Post-Installation +############################################################################# + +show_post_install() { + echo "" + print_step "Next Steps" + + echo "1. Review the installed components in .opencode/" + echo "2. Copy env.example to .env and configure:" + echo " ${CYAN}cp env.example .env${NC}" + echo "3. Start using OpenCode agents:" + echo " ${CYAN}opencode${NC}" + echo "" + + print_info "Documentation: ${REPO_URL}" + echo "" + + cleanup_and_exit 0 +} + +############################################################################# +# Component Listing +############################################################################# + +list_components() { + clear + print_header + + echo -e "${BOLD}Available Components${NC}\n" + + local categories=("agents" "subagents" "commands" "tools" "plugins" "contexts") + + for category in "${categories[@]}"; do + echo -e "${CYAN}${BOLD}${category^}:${NC}" + + local components=$(jq -r ".components.${category}[] | \"\(.id)|\(.name)|\(.description)\"" "$TEMP_DIR/registry.json") + + while IFS='|' read -r id name desc; do + echo -e " ${GREEN}${name}${NC} (${id})" + echo -e " ${desc}" + done <<< "$components" + + echo "" + done + + read -p "Press Enter to continue..." +} + +############################################################################# +# Cleanup +############################################################################# + +cleanup_and_exit() { + rm -rf "$TEMP_DIR" + exit "$1" +} + +trap 'cleanup_and_exit 1' INT TERM + +############################################################################# +# Main +############################################################################# + +main() { + # Parse command line arguments + case "${1:-}" in + --core) + INSTALL_MODE="profile" + PROFILE="core" + ;; + --developer) + INSTALL_MODE="profile" + PROFILE="developer" + ;; + --full) + INSTALL_MODE="profile" + PROFILE="full" + ;; + --advanced) + INSTALL_MODE="profile" + PROFILE="advanced" + ;; + --list) + check_dependencies + fetch_registry + list_components + cleanup_and_exit 0 + ;; + --help|-h) + print_header + echo "Usage: $0 [OPTIONS]" + echo "" + echo "Options:" + echo " --core Install core profile" + echo " --developer Install developer profile" + echo " --full Install full profile" + echo " --advanced Install advanced profile" + echo " --list List all available components" + echo " --help Show this help message" + echo "" + echo "Without options, runs in interactive mode" + exit 0 + ;; + esac + + check_dependencies + fetch_registry + + if [ -n "$PROFILE" ]; then + # Non-interactive mode + mapfile -t SELECTED_COMPONENTS < <(get_profile_components "$PROFILE") + show_installation_preview + else + # Interactive mode + show_main_menu + + if [ "$INSTALL_MODE" = "profile" ]; then + show_profile_menu + elif [ "$INSTALL_MODE" = "custom" ]; then + show_custom_menu + fi + fi +} + +main "$@" diff --git a/registry.json b/registry.json new file mode 100644 index 0000000..e69de29 diff --git a/scripts/register-component.sh b/scripts/register-component.sh new file mode 100755 index 0000000..48beb44 --- /dev/null +++ b/scripts/register-component.sh @@ -0,0 +1,364 @@ +#!/usr/bin/env bash + +############################################################################# +# Component Registration Script +# Automatically scans .opencode/ and updates registry.json +############################################################################# + +set -e + +# Colors +RED='\033[0;31m' +GREEN='\033[0;32m' +YELLOW='\033[1;33m' +BLUE='\033[0;34m' +CYAN='\033[0;36m' +BOLD='\033[1m' +NC='\033[0m' + +# Configuration +OPENCODE_DIR=".opencode" +REGISTRY_FILE="registry.json" +TEMP_REGISTRY="/tmp/registry-temp-$$.json" + +print_success() { echo -e "${GREEN}✓${NC} $1"; } +print_error() { echo -e "${RED}✗${NC} $1"; } +print_info() { echo -e "${BLUE}ℹ${NC} $1"; } +print_warning() { echo -e "${YELLOW}⚠${NC} $1"; } +print_step() { echo -e "\n${CYAN}${BOLD}â–ļ${NC} $1\n"; } + +############################################################################# +# Validation +############################################################################# + +check_dependencies() { + if ! command -v jq &> /dev/null; then + print_error "jq is required but not installed" + echo "Install with: brew install jq (macOS) or apt-get install jq (Linux)" + exit 1 + fi +} + +validate_registry() { + if [ ! -f "$REGISTRY_FILE" ]; then + print_error "Registry file not found: $REGISTRY_FILE" + exit 1 + fi + + if ! jq empty "$REGISTRY_FILE" 2>/dev/null; then + print_error "Invalid JSON in registry file" + exit 1 + fi +} + +############################################################################# +# Component Discovery +############################################################################# + +extract_frontmatter() { + local file=$1 + local field=$2 + + # Extract YAML frontmatter between --- markers + awk -v field="$field" ' + BEGIN { in_fm=0; } + /^---$/ { + if (in_fm == 0) { in_fm=1; next; } + else { exit; } + } + in_fm && $0 ~ "^" field ":" { + sub("^" field ": *", ""); + gsub(/^["\047]|["\047]$/, ""); + print; + exit; + } + ' "$file" +} + +scan_agents() { + print_step "Scanning agents..." + + local json_array="[]" + + while IFS= read -r -d '' file; do + local id=$(basename "$file" .md) + local name=$(extract_frontmatter "$file" "name") + local desc=$(extract_frontmatter "$file" "description") + + # Use defaults if not found in frontmatter + [ -z "$name" ] && name=$(echo "$id" | sed 's/-/ /g' | awk '{for(i=1;i<=NF;i++)sub(/./,toupper(substr($i,1,1)),$i)}1') + [ -z "$desc" ] && desc="Agent: $name" + + local path="${file#./}" + + # Build JSON using jq + json_array=$(echo "$json_array" | jq \ + --arg id "$id" \ + --arg name "$name" \ + --arg path "$path" \ + --arg desc "$desc" \ + '. += [{ + "id": $id, + "name": $name, + "type": "agent", + "path": $path, + "description": $desc, + "tags": [], + "dependencies": [], + "category": "core" + }]') + + print_info "Found agent: $id" + done < <(find "$OPENCODE_DIR/agent" -maxdepth 1 -name "*.md" -type f -print0 2>/dev/null) + + echo "$json_array" +} + +scan_subagents() { + print_step "Scanning subagents..." + + local subagents=() + + while IFS= read -r -d '' file; do + local id=$(basename "$file" .md) + local name=$(extract_frontmatter "$file" "name") + local desc=$(extract_frontmatter "$file" "description") + + [ -z "$name" ] && name=$(echo "$id" | sed 's/-/ /g' | awk '{for(i=1;i<=NF;i++)sub(/./,toupper(substr($i,1,1)),$i)}1') + [ -z "$desc" ] && desc="Subagent: $name" + + local path="${file#./}" + + subagents+=("{\"id\":\"$id\",\"name\":\"$name\",\"type\":\"subagent\",\"path\":\"$path\",\"description\":\"$desc\",\"tags\":[],\"dependencies\":[],\"category\":\"core\"}") + + print_info "Found subagent: $id" + done < <(find "$OPENCODE_DIR/agent/subagents" -name "*.md" -type f -print0 2>/dev/null) + + if [ ${#subagents[@]} -gt 0 ]; then + echo "[$(IFS=,; echo "${subagents[*]}")]" + else + echo "[]" + fi +} + +scan_commands() { + print_step "Scanning commands..." + + local commands=() + + while IFS= read -r -d '' file; do + local id=$(basename "$file" .md) + local name=$(extract_frontmatter "$file" "name") + local desc=$(extract_frontmatter "$file" "description") + + [ -z "$name" ] && name=$(echo "$id" | sed 's/-/ /g' | awk '{for(i=1;i<=NF;i++)sub(/./,toupper(substr($i,1,1)),$i)}1') + [ -z "$desc" ] && desc="Command: $name" + + local path="${file#./}" + + commands+=("{\"id\":\"$id\",\"name\":\"$name\",\"type\":\"command\",\"path\":\"$path\",\"description\":\"$desc\",\"tags\":[],\"dependencies\":[],\"category\":\"core\"}") + + print_info "Found command: $id" + done < <(find "$OPENCODE_DIR/command" -name "*.md" -type f -print0 2>/dev/null) + + if [ ${#commands[@]} -gt 0 ]; then + echo "[$(IFS=,; echo "${commands[*]}")]" + else + echo "[]" + fi +} + +scan_tools() { + print_step "Scanning tools..." + + local tools=() + + # Look for directories with index.ts + while IFS= read -r -d '' dir; do + local id=$(basename "$dir") + + # Skip node_modules and template + [[ "$id" == "node_modules" || "$id" == "template" ]] && continue + + local index_file="$dir/index.ts" + [ ! -f "$index_file" ] && continue + + local name=$(echo "$id" | sed 's/-/ /g' | awk '{for(i=1;i<=NF;i++)sub(/./,toupper(substr($i,1,1)),$i)}1') + local desc="Tool: $name" + + # Try to extract description from comments + local comment_desc=$(grep -m 1 "^\s*\*.*" "$index_file" | sed 's/^\s*\*\s*//' || echo "") + [ -n "$comment_desc" ] && desc="$comment_desc" + + local path="${index_file#./}" + + tools+=("{\"id\":\"$id\",\"name\":\"$name\",\"type\":\"tool\",\"path\":\"$path\",\"description\":\"$desc\",\"tags\":[],\"dependencies\":[],\"category\":\"core\"}") + + print_info "Found tool: $id" + done < <(find "$OPENCODE_DIR/tool" -mindepth 1 -maxdepth 1 -type d -print0 2>/dev/null) + + if [ ${#tools[@]} -gt 0 ]; then + echo "[$(IFS=,; echo "${tools[*]}")]" + else + echo "[]" + fi +} + +scan_plugins() { + print_step "Scanning plugins..." + + local plugins=() + + while IFS= read -r -d '' file; do + local id=$(basename "$file" .ts) + + # Skip lib directory files + [[ "$file" == *"/lib/"* ]] && continue + + local name=$(echo "$id" | sed 's/-/ /g' | awk '{for(i=1;i<=NF;i++)sub(/./,toupper(substr($i,1,1)),$i)}1') + local desc="Plugin: $name" + + # Try to extract description from comments + local comment_desc=$(grep -m 1 "^\s*\*.*" "$file" | sed 's/^\s*\*\s*//' || echo "") + [ -n "$comment_desc" ] && desc="$comment_desc" + + local path="${file#./}" + + plugins+=("{\"id\":\"$id\",\"name\":\"$name\",\"type\":\"plugin\",\"path\":\"$path\",\"description\":\"$desc\",\"tags\":[],\"dependencies\":[],\"category\":\"extended\"}") + + print_info "Found plugin: $id" + done < <(find "$OPENCODE_DIR/plugin" -maxdepth 1 -name "*.ts" -type f -print0 2>/dev/null) + + if [ ${#plugins[@]} -gt 0 ]; then + echo "[$(IFS=,; echo "${plugins[*]}")]" + else + echo "[]" + fi +} + +scan_contexts() { + print_step "Scanning contexts..." + + local contexts=() + + while IFS= read -r -d '' file; do + local id=$(basename "$file" .md) + local name=$(extract_frontmatter "$file" "name") + local desc=$(extract_frontmatter "$file" "description") + + [ -z "$name" ] && name=$(echo "$id" | sed 's/-/ /g' | awk '{for(i=1;i<=NF;i++)sub(/./,toupper(substr($i,1,1)),$i)}1') + [ -z "$desc" ] && desc="Context: $name" + + local path="${file#./}" + + contexts+=("{\"id\":\"$id\",\"name\":\"$name\",\"type\":\"context\",\"path\":\"$path\",\"description\":\"$desc\",\"tags\":[],\"dependencies\":[],\"category\":\"core\"}") + + print_info "Found context: $id" + done < <(find "$OPENCODE_DIR/context" -name "*.md" -type f -print0 2>/dev/null) + + if [ ${#contexts[@]} -gt 0 ]; then + echo "[$(IFS=,; echo "${contexts[*]}")]" + else + echo "[]" + fi +} + +############################################################################# +# Registry Update +############################################################################# + +update_registry() { + print_step "Updating registry..." + + # Scan all components + local agents_json=$(scan_agents) + local subagents_json=$(scan_subagents) + local commands_json=$(scan_commands) + local tools_json=$(scan_tools) + local plugins_json=$(scan_plugins) + local contexts_json=$(scan_contexts) + + # Read existing registry + local existing_registry=$(cat "$REGISTRY_FILE") + + # Update components while preserving profiles and metadata + local updated_registry=$(echo "$existing_registry" | jq \ + --argjson agents "$agents_json" \ + --argjson subagents "$subagents_json" \ + --argjson commands "$commands_json" \ + --argjson tools "$tools_json" \ + --argjson plugins "$plugins_json" \ + --argjson contexts "$contexts_json" \ + ' + .components.agents = $agents | + .components.subagents = $subagents | + .components.commands = $commands | + .components.tools = $tools | + .components.plugins = $plugins | + .components.contexts = $contexts | + .metadata.lastUpdated = (now | strftime("%Y-%m-%d")) + ') + + # Write to temp file first + echo "$updated_registry" | jq '.' > "$TEMP_REGISTRY" + + # Validate + if jq empty "$TEMP_REGISTRY" 2>/dev/null; then + mv "$TEMP_REGISTRY" "$REGISTRY_FILE" + print_success "Registry updated successfully" + else + print_error "Generated invalid JSON, registry not updated" + rm -f "$TEMP_REGISTRY" + exit 1 + fi +} + +############################################################################# +# Statistics +############################################################################# + +show_statistics() { + print_step "Registry Statistics" + + local agents=$(jq '.components.agents | length' "$REGISTRY_FILE") + local subagents=$(jq '.components.subagents | length' "$REGISTRY_FILE") + local commands=$(jq '.components.commands | length' "$REGISTRY_FILE") + local tools=$(jq '.components.tools | length' "$REGISTRY_FILE") + local plugins=$(jq '.components.plugins | length' "$REGISTRY_FILE") + local contexts=$(jq '.components.contexts | length' "$REGISTRY_FILE") + local total=$((agents + subagents + commands + tools + plugins + contexts)) + + echo " Agents: $agents" + echo " Subagents: $subagents" + echo " Commands: $commands" + echo " Tools: $tools" + echo " Plugins: $plugins" + echo " Contexts: $contexts" + echo " ─────────────" + echo " Total: $total" + echo "" +} + +############################################################################# +# Main +############################################################################# + +main() { + echo -e "${CYAN}${BOLD}" + echo "╔════════════════════════════════════════════════════════════════╗" + echo "║ ║" + echo "║ Component Registration Script ║" + echo "║ ║" + echo "╚════════════════════════════════════════════════════════════════╝" + echo -e "${NC}" + + check_dependencies + validate_registry + + update_registry + show_statistics + + print_success "Done!" +} + +main "$@" diff --git a/scripts/validate-component.sh b/scripts/validate-component.sh new file mode 100755 index 0000000..9296519 --- /dev/null +++ b/scripts/validate-component.sh @@ -0,0 +1,171 @@ +#!/usr/bin/env bash + +############################################################################# +# Component Validation Script +# Validates component structure and metadata for PRs +############################################################################# + +set -e + +# Colors +RED='\033[0;31m' +GREEN='\033[0;32m' +YELLOW='\033[1;33m' +BLUE='\033[0;34m' +NC='\033[0m' + +ERRORS=0 +WARNINGS=0 + +print_success() { echo -e "${GREEN}✓${NC} $1"; } +print_error() { echo -e "${RED}✗${NC} $1"; ((ERRORS++)); } +print_warning() { echo -e "${YELLOW}⚠${NC} $1"; ((WARNINGS++)); } +print_info() { echo -e "${BLUE}ℹ${NC} $1"; } + +validate_markdown_frontmatter() { + local file=$1 + + print_info "Validating $file" + + # Check if file has frontmatter + if ! head -n 1 "$file" | grep -q "^---$"; then + print_warning "Missing frontmatter in $file" + return + fi + + # Extract frontmatter + local frontmatter=$(awk '/^---$/{if(++n==2)exit;next}n==1' "$file") + + # Check for description + if ! echo "$frontmatter" | grep -q "^description:"; then + print_warning "Missing 'description' in frontmatter of $file" + else + print_success "Has description" + fi + + # For agents, check for mode + if [[ "$file" == *"/agent/"* ]] && [[ "$file" != *"/subagents/"* ]]; then + if ! echo "$frontmatter" | grep -q "^mode:"; then + print_warning "Missing 'mode' in agent frontmatter of $file" + fi + fi +} + +validate_typescript_file() { + local file=$1 + + print_info "Validating $file" + + # Check for basic TypeScript syntax (very basic check) + if ! grep -q "export" "$file"; then + print_warning "No exports found in $file" + else + print_success "Has exports" + fi + + # Check for comments/documentation + if ! grep -q "^\s*\*" "$file"; then + print_warning "No JSDoc comments found in $file" + else + print_success "Has documentation" + fi +} + +validate_directory_structure() { + print_info "Validating directory structure" + + local required_dirs=( + ".opencode" + ".opencode/agent" + ".opencode/command" + ".opencode/tool" + ) + + for dir in "${required_dirs[@]}"; do + if [ ! -d "$dir" ]; then + print_error "Missing required directory: $dir" + else + print_success "Directory exists: $dir" + fi + done +} + +validate_registry() { + print_info "Validating registry.json" + + if [ ! -f "registry.json" ]; then + print_error "registry.json not found" + return + fi + + # Check if valid JSON + if ! jq empty registry.json 2>/dev/null; then + print_error "registry.json is not valid JSON" + return + fi + + print_success "registry.json is valid JSON" + + # Check required fields + local required_fields=("version" "repository" "components" "profiles" "metadata") + + for field in "${required_fields[@]}"; do + if ! jq -e ".$field" registry.json > /dev/null 2>&1; then + print_error "Missing required field in registry.json: $field" + else + print_success "Has field: $field" + fi + done +} + +main() { + echo "╔════════════════════════════════════════════════════════════════╗" + echo "║ Component Validation ║" + echo "╚════════════════════════════════════════════════════════════════╝" + echo "" + + # Validate directory structure + validate_directory_structure + echo "" + + # Validate registry + validate_registry + echo "" + + # Validate all markdown files + echo "Validating markdown files..." + while IFS= read -r -d '' file; do + validate_markdown_frontmatter "$file" + done < <(find .opencode -name "*.md" -type f -print0 2>/dev/null) + echo "" + + # Validate TypeScript files + echo "Validating TypeScript files..." + while IFS= read -r -d '' file; do + validate_typescript_file "$file" + done < <(find .opencode -name "*.ts" -type f -not -path "*/node_modules/*" -print0 2>/dev/null) + echo "" + + # Summary + echo "════════════════════════════════════════════════════════════════" + echo "Validation Summary:" + echo " Errors: $ERRORS" + echo " Warnings: $WARNINGS" + echo "════════════════════════════════════════════════════════════════" + + if [ $ERRORS -gt 0 ]; then + echo "" + print_error "Validation failed with $ERRORS error(s)" + exit 1 + elif [ $WARNINGS -gt 0 ]; then + echo "" + print_warning "Validation passed with $WARNINGS warning(s)" + exit 0 + else + echo "" + print_success "All validations passed!" + exit 0 + fi +} + +main "$@" diff --git a/update.sh b/update.sh new file mode 100755 index 0000000..a241247 --- /dev/null +++ b/update.sh @@ -0,0 +1,83 @@ +#!/usr/bin/env bash + +############################################################################# +# OpenCode Agents Updater +# Updates existing OpenCode components to latest versions +############################################################################# + +set -e + +# Colors +GREEN='\033[0;32m' +YELLOW='\033[1;33m' +BLUE='\033[0;34m' +CYAN='\033[0;36m' +BOLD='\033[1m' +NC='\033[0m' + +REPO_URL="https://raw.githubusercontent.com/darrenhinde/opencode-agents/main" +INSTALL_DIR=".opencode" + +print_success() { echo -e "${GREEN}✓${NC} $1"; } +print_info() { echo -e "${BLUE}ℹ${NC} $1"; } +print_step() { echo -e "\n${CYAN}${BOLD}â–ļ${NC} $1\n"; } + +print_header() { + echo -e "${CYAN}${BOLD}" + echo "╔════════════════════════════════════════════════════════════════╗" + echo "║ ║" + echo "║ OpenCode Agents Updater v1.0.0 ║" + echo "║ ║" + echo "╚════════════════════════════════════════════════════════════════╝" + echo -e "${NC}" +} + +update_component() { + local path=$1 + local url="${REPO_URL}/${path}" + + if [ ! -f "$path" ]; then + print_info "Skipping $path (not installed)" + return + fi + + # Backup existing file + cp "$path" "${path}.backup" + + if curl -fsSL "$url" -o "$path"; then + print_success "Updated $path" + rm "${path}.backup" + else + print_info "Failed to update $path, restoring backup" + mv "${path}.backup" "$path" + fi +} + +main() { + print_header + + if [ ! -d "$INSTALL_DIR" ]; then + echo "Error: $INSTALL_DIR directory not found" + echo "Run install.sh first to install components" + exit 1 + fi + + print_step "Updating components..." + + # Update all markdown files in .opencode + while IFS= read -r -d '' file; do + update_component "$file" + done < <(find "$INSTALL_DIR" -name "*.md" -type f -print0) + + # Update TypeScript files + while IFS= read -r -d '' file; do + update_component "$file" + done < <(find "$INSTALL_DIR" -name "*.ts" -type f -not -path "*/node_modules/*" -print0) + + # Update config files + [ -f "env.example" ] && update_component "env.example" + + print_success "Update complete!" +} + +main "$@"