feat: add interactive installer with component registry system

- Add component registry (registry.json) with 4 installation profiles
- Create interactive installer (install.sh) with profile and custom selection
- Add auto-registration script for CI/CD integration
- Create validation script for component structure
- Add update script for keeping components current
- Include GitHub Actions workflow for auto-registry updates
- Update README with installation instructions
- Add CONTRIBUTING guide for new components

Features:
- Interactive CLI with colored output and menus
- Smart dependency resolution
- Backup existing installations
- Profile-based installation (core, developer, full, advanced)
- Custom component selection
- Component validation and testing tools
This commit is contained in:
darrenhinde 2025-11-17 22:40:55 +00:00
parent bd6aec49c6
commit 777b3fc702
8 changed files with 1617 additions and 1 deletions

75
.github/workflows/update-registry.yml vendored Normal file
View file

@ -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

237
CONTRIBUTING.md Normal file
View file

@ -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! 🎉

View file

@ -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

609
install.sh Executable file
View file

@ -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 "$@"

0
registry.json Normal file
View file

364
scripts/register-component.sh Executable file
View file

@ -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 "$@"

171
scripts/validate-component.sh Executable file
View file

@ -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 "$@"

83
update.sh Executable file
View file

@ -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 "$@"