diff --git a/.opencode/context/openagents-repo/guides/building-cli-compact.md b/.opencode/context/openagents-repo/guides/building-cli-compact.md new file mode 100644 index 0000000..c061a6e --- /dev/null +++ b/.opencode/context/openagents-repo/guides/building-cli-compact.md @@ -0,0 +1,97 @@ +# Building CLIs in OpenAgents: Compact Guide + +**Category**: guide +**Purpose**: Rapidly build, register, and deploy CLI tools for OpenAgents skills +**Framework**: FAB (Features, Advantages, Benefits) + +--- + +## 🚀 Quick Start + +**Don't start from scratch.** Use the standard pattern to build robust CLIs in minutes. + +1. **Create**: `mkdir -p .opencode/skill/{name}/scripts` +2. **Implement**: Create `skill-cli.ts` (TypeScript) and `router.sh` (Bash) +3. **Register**: Add to `registry.json` +4. **Run**: `bash .opencode/skill/{name}/router.sh help` + +--- + +## 🏗️ Core Architecture + +| Component | File | Purpose | +|-----------|------|---------| +| **Logic** | `scripts/skill-cli.ts` | Type-safe implementation using `ts-node`. Handles args, logic, and output. | +| **Router** | `router.sh` | Universal entry point. Routes commands to the TS script. | +| **Docs** | `SKILL.md` | User guide, examples, and integration details. | +| **Config** | `registry.json` | Makes the skill discoverable and installable via `install.sh`. | + +--- + +## ⚡ Implementation Patterns + +### 1. The Router (`router.sh`) +**Why**: Provides a consistent, dependency-free entry point for all environments. + +```bash +#!/bin/bash +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" + +case "$1" in + help|--help|-h) + echo "Usage: bash router.sh " + ;; + *) + # Route to TypeScript implementation + npx ts-node "$SCRIPT_DIR/scripts/skill-cli.ts" "$@" + ;; +esac +``` + +### 2. The CLI Logic (`skill-cli.ts`) +**Why**: Type safety, async/await support, and rich ecosystem access. + +```typescript +#!/usr/bin/env ts-node + +async function main() { + const [command, ...args] = process.argv.slice(2); + + switch (command) { + case 'action': + await handleAction(args); + break; + default: + console.log("Unknown command"); + process.exit(1); + } +} + +main().catch(console.error); +``` + +--- + +## ✅ Quality Checklist + +Before shipping, verify your CLI delivers value: + +- [ ] **Help Command**: Does `router.sh help` provide clear, actionable usage info? +- [ ] **Error Handling**: Do invalid inputs return helpful error messages (not stack traces)? +- [ ] **Performance**: Does it start in < 1s? (Avoid heavy imports at top level) +- [ ] **Idempotency**: Can commands be run multiple times safely? +- [ ] **Registry**: Is it added to `registry.json` with correct paths? + +--- + +## 🧠 Copywriting Principles for CLI Output + +Apply `content-creation` principles to your CLI output: + +1. **Clarity**: Use **Active Voice**. "Created file" (Good) vs "File has been created" (Bad). +2. **Specificity**: "Processed 5 files" (Good) vs "Processing complete" (Bad). +3. **Action**: Tell the user what to do next. "Run `npm test` to verify." + +--- + +**Reference**: See `.opencode/context/openagents-repo/guides/adding-skill.md` for the full, detailed walkthrough. diff --git a/.opencode/skill/task-management/router.sh b/.opencode/skill/task-management/router.sh index e6b907c..2472647 100644 --- a/.opencode/skill/task-management/router.sh +++ b/.opencode/skill/task-management/router.sh @@ -15,7 +15,8 @@ fi # Find project root find_project_root() { - local dir="$(pwd)" + local dir + dir="$(pwd)" while [ "$dir" != "/" ]; do if [ -f "$dir/.git" ] || [ -f "$dir/package.json" ]; then echo "$dir" @@ -23,7 +24,7 @@ find_project_root() { fi dir="$(dirname "$dir")" done - echo "$(pwd)" + pwd return 1 } diff --git a/install.sh b/install.sh index fdf4166..a1f0626 100755 --- a/install.sh +++ b/install.sh @@ -291,14 +291,14 @@ fetch_registry() { get_profile_components() { local profile=$1 - jq_exec ".profiles.${profile}.components[]" "$TEMP_DIR/registry.json" + jq_exec ".profiles.${profile}.components[]?" "$TEMP_DIR/registry.json" } get_component_info() { local component_id=$1 local component_type=$2 - jq_exec ".components.${component_type}[] | select(.id == \"${component_id}\")" "$TEMP_DIR/registry.json" + jq_exec ".components.${component_type}[]? | select(.id == \"${component_id}\")" "$TEMP_DIR/registry.json" } # Helper function to get the correct registry key for a component type @@ -489,7 +489,7 @@ show_main_menu() { case $choice in 1) INSTALL_MODE="profile" ;; 2) INSTALL_MODE="custom" ;; - 3) list_components; show_main_menu ;; + 3) list_components; read -p "Press Enter to continue..."; show_main_menu ;; 4) cleanup_and_exit 0 ;; *) print_error "Invalid choice"; sleep 2; show_main_menu ;; esac @@ -614,7 +614,7 @@ show_custom_menu() { echo "Use space to toggle, Enter to continue" echo "" - local categories=("agents" "subagents" "commands" "tools" "plugins" "contexts" "config") + local categories=("agents" "subagents" "commands" "tools" "plugins" "skills" "contexts" "config") local selected_categories=() # Simple selection (for now, we'll make it interactive later) @@ -674,14 +674,14 @@ show_component_selection() { echo -e "${CYAN}${BOLD}${cat_display}:${NC}" local components - components=$(jq_exec ".components.${category}[] | .id" "$TEMP_DIR/registry.json") + components=$(jq_exec ".components.${category}[]? | .id" "$TEMP_DIR/registry.json") local idx=1 while IFS= read -r comp_id; do local comp_name - comp_name=$(jq_exec ".components.${category}[] | select(.id == \"${comp_id}\") | .name" "$TEMP_DIR/registry.json") + comp_name=$(jq_exec ".components.${category}[]? | select(.id == \"${comp_id}\") | .name" "$TEMP_DIR/registry.json") local comp_desc - comp_desc=$(jq_exec ".components.${category}[] | select(.id == \"${comp_id}\") | .description" "$TEMP_DIR/registry.json") + comp_desc=$(jq_exec ".components.${category}[]? | select(.id == \"${comp_id}\") | .description" "$TEMP_DIR/registry.json") echo " ${idx}) ${comp_name}" echo " ${comp_desc}" @@ -759,6 +759,7 @@ show_installation_preview() { local commands=() local tools=() local plugins=() + local skills=() local contexts=() local configs=() @@ -770,6 +771,7 @@ show_installation_preview() { command) commands+=("$comp") ;; tool) tools+=("$comp") ;; plugin) plugins+=("$comp") ;; + skill) skills+=("$comp") ;; context) contexts+=("$comp") ;; config) configs+=("$comp") ;; esac @@ -780,6 +782,7 @@ show_installation_preview() { [ ${#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[*]##*:}" + [ ${#skills[@]} -gt 0 ] && echo -e "${CYAN}Skills (${#skills[@]}):${NC} ${skills[*]##*:}" [ ${#contexts[@]} -gt 0 ] && echo -e "${CYAN}Contexts (${#contexts[@]}):${NC} ${contexts[*]##*:}" [ ${#configs[@]} -gt 0 ] && echo -e "${CYAN}Config (${#configs[@]}):${NC} ${configs[*]##*:}" @@ -820,6 +823,7 @@ show_collision_report() { local commands=() local tools=() local plugins=() + local skills=() local contexts=() local configs=() @@ -837,6 +841,8 @@ show_collision_report() { tools+=("$file") elif [[ $file == *"/plugin/"* ]]; then plugins+=("$file") + elif [[ $file == *"/skill/"* ]]; then + skills+=("$file") elif [[ $file == *"/context/"* ]]; then contexts+=("$file") else @@ -850,6 +856,7 @@ show_collision_report() { [ ${#commands[@]} -gt 0 ] && echo -e "${YELLOW} Commands (${#commands[@]}):${NC}" && printf ' %s\n' "${commands[@]}" [ ${#tools[@]} -gt 0 ] && echo -e "${YELLOW} Tools (${#tools[@]}):${NC}" && printf ' %s\n' "${tools[@]}" [ ${#plugins[@]} -gt 0 ] && echo -e "${YELLOW} Plugins (${#plugins[@]}):${NC}" && printf ' %s\n' "${plugins[@]}" + [ ${#skills[@]} -gt 0 ] && echo -e "${YELLOW} Skills (${#skills[@]}):${NC}" && printf ' %s\n' "${skills[@]}" [ ${#contexts[@]} -gt 0 ] && echo -e "${YELLOW} Context (${#contexts[@]}):${NC}" && printf ' %s\n' "${contexts[@]}" [ ${#configs[@]} -gt 0 ] && echo -e "${YELLOW} Config (${#configs[@]}):${NC}" && printf ' %s\n' "${configs[@]}" @@ -901,7 +908,7 @@ perform_installation() { local registry_key registry_key=$(get_registry_key "$type") local path - path=$(jq_exec ".components.${registry_key}[] | select(.id == \"${id}\") | .path" "$TEMP_DIR/registry.json") + path=$(jq_exec ".components.${registry_key}[]? | select(.id == \"${id}\") | .path" "$TEMP_DIR/registry.json") if [ -n "$path" ] && [ "$path" != "null" ]; then local install_path @@ -978,7 +985,7 @@ perform_installation() { # Get component path local path - path=$(jq_exec ".components.${registry_key}[] | select(.id == \"${id}\") | .path" "$TEMP_DIR/registry.json") + path=$(jq_exec ".components.${registry_key}[]? | select(.id == \"${id}\") | .path" "$TEMP_DIR/registry.json") if [ -z "$path" ] || [ "$path" = "null" ]; then print_warning "Could not find path for ${comp}" @@ -1111,12 +1118,12 @@ show_post_install() { ############################################################################# list_components() { - clear + clear || true print_header echo -e "${BOLD}Available Components${NC}\n" - local categories=("agents" "subagents" "commands" "tools" "plugins" "contexts") + local categories=("agents" "subagents" "commands" "tools" "plugins" "skills" "contexts") for category in "${categories[@]}"; do local cat_display @@ -1124,7 +1131,7 @@ list_components() { echo -e "${CYAN}${BOLD}${cat_display}:${NC}" local components - components=$(jq_exec ".components.${category}[] | \"\(.id)|\(.name)|\(.description)\"" "$TEMP_DIR/registry.json") + components=$(jq_exec ".components.${category}[]? | \"\(.id)|\(.name)|\(.description)\"" "$TEMP_DIR/registry.json") while IFS='|' read -r id name desc; do echo -e " ${GREEN}${name}${NC} (${id})" @@ -1133,8 +1140,6 @@ list_components() { echo "" done - - read -p "Press Enter to continue..." } ############################################################################# diff --git a/registry.json b/registry.json index cde09d7..827ec48 100644 --- a/registry.json +++ b/registry.json @@ -333,17 +333,17 @@ "knowledge-management" ], "dependencies": [ - "context:core/context-system/operations/harvest", - "context:core/context-system/operations/extract", - "context:core/context-system/operations/organize", - "context:core/context-system/operations/update", - "context:core/context-system/operations/error", - "context:core/context-system/standards/mvi", - "context:core/context-system/standards/structure", - "context:core/context-system/standards/templates", - "context:core/context-system/guides/workflows", - "context:core/context-system/guides/compact", - "context:core/context-system/guides/creation" + "context:harvest", + "context:extract", + "context:organize", + "context:update", + "context:error", + "context:mvi", + "context:structure", + "context:templates", + "context:workflows", + "context:compact", + "context:creation" ], "category": "meta" }, @@ -390,11 +390,11 @@ ], "dependencies": [ "command:check-context-deps", - "context:openagents-repo/quality/registry-dependencies", - "context:core/context-system", - "context:core/context-system/standards/mvi", - "context:core/context-system/standards/structure", - "context:core/context-system/guides/workflows" + "context:registry-dependencies", + "context:context-system", + "context:mvi", + "context:structure", + "context:workflows" ], "category": "core" }, @@ -704,6 +704,36 @@ "category": "specialized" } ], + "skills": [ + { + "id": "task-management", + "name": "Task Management", + "type": "skill", + "path": ".opencode/skill/task-management/SKILL.md", + "description": "Task management CLI for tracking and managing feature subtasks with status, dependencies, and validation", + "tags": [ + "tasks", + "cli", + "workflow" + ], + "dependencies": [], + "category": "essential" + }, + { + "id": "smart-router-skill", + "name": "Smart Router Skill", + "type": "skill", + "path": ".opencode/skill/smart-router-skill/SKILL.md", + "description": "Movie character personality skill with configurable missions and themed workflows", + "tags": [ + "routing", + "personality", + "demo" + ], + "dependencies": [], + "category": "specialized" + } + ], "contexts": [ { "id": "essential-patterns", diff --git a/scripts/tests/test-collision-detection.sh b/scripts/tests/test-collision-detection.sh index d411860..a4ba092 100755 --- a/scripts/tests/test-collision-detection.sh +++ b/scripts/tests/test-collision-detection.sh @@ -60,7 +60,7 @@ test_no_collisions() { for file in "${files[@]}"; do if [ -f "$file" ]; then - ((collisions++)) + ((collisions+=1)) fi done diff --git a/scripts/tests/test-compatibility.sh b/scripts/tests/test-compatibility.sh index 7bb2805..9d28494 100755 --- a/scripts/tests/test-compatibility.sh +++ b/scripts/tests/test-compatibility.sh @@ -87,7 +87,7 @@ fi # Test 6: Profile argument parsing echo "" echo "Test 6: Profile Argument Parsing" -for profile in core developer full advanced; do +for profile in essential developer full advanced; do if echo "n" | bash install.sh "$profile" 2>&1 | grep -q "Profile:"; then pass "Profile '$profile' argument works" else @@ -98,7 +98,7 @@ done # Test 7: Profile with dashes echo "" echo "Test 7: Profile Arguments with Dashes" -for profile in --core --developer --full --advanced; do +for profile in --essential --developer --full --advanced; do if echo "n" | bash install.sh "$profile" 2>&1 | grep -q "Profile:"; then pass "Profile '$profile' argument works" else diff --git a/scripts/tests/test-e2e-install.sh b/scripts/tests/test-e2e-install.sh index 212987f..7f7d1b8 100755 --- a/scripts/tests/test-e2e-install.sh +++ b/scripts/tests/test-e2e-install.sh @@ -17,12 +17,12 @@ FAILED=0 pass() { echo -e "${GREEN}✓${NC} $1" - ((PASSED++)) + ((PASSED+=1)) } fail() { echo -e "${RED}✗${NC} $1" - ((FAILED++)) + ((FAILED+=1)) } warn() { @@ -71,7 +71,7 @@ test_essential_profile() { pass "Found: $file" else fail "Missing: $file" - ((missing++)) + ((missing+=1)) fi done @@ -101,7 +101,7 @@ test_developer_profile() { local found=0 for file in "${expected_files[@]}"; do if [ -f "$install_dir/$file" ]; then - ((found++)) + ((found+=1)) fi done @@ -244,7 +244,7 @@ test_help_and_list() { fail "Help command failed" fi - if bash "$REPO_ROOT/install.sh" list 2>&1 | grep -q "Available Components\|Agents"; then + if bash "$REPO_ROOT/install.sh" list 2>&1 | grep "Available Components\|Agents" > /dev/null; then pass "List command works" else fail "List command failed" diff --git a/scripts/tests/test-non-interactive.sh b/scripts/tests/test-non-interactive.sh index 638794a..13acab7 100755 --- a/scripts/tests/test-non-interactive.sh +++ b/scripts/tests/test-non-interactive.sh @@ -25,12 +25,12 @@ FAILED=0 pass() { echo -e "${GREEN}✓${NC} $1" - ((PASSED++)) + ((PASSED+=1)) } fail() { echo -e "${RED}✗${NC} $1" - ((FAILED++)) + ((FAILED+=1)) } warn() { @@ -56,6 +56,19 @@ print_header() { echo -e "${NC}" } +run_with_timeout() { + local duration=$1 + shift + if command -v timeout &> /dev/null; then + timeout "$duration" "$@" + elif command -v gtimeout &> /dev/null; then + gtimeout "$duration" "$@" + else + # Fallback: run without timeout + "$@" + fi +} + ############################################################################# # Test 1: Fresh install with piped input (simulates curl | bash) ############################################################################# @@ -121,7 +134,7 @@ test_profile_non_interactive() { local install_dir="$TEST_DIR/profile-essential/.opencode" local output - output=$(echo "" | timeout 60 bash "$REPO_ROOT/install.sh" essential --install-dir="$install_dir" 2>&1) || true + output=$(echo "" | run_with_timeout 60 bash "$REPO_ROOT/install.sh" essential --install-dir="$install_dir" 2>&1) || true if echo "$output" | grep -q "Installation complete"; then pass "Profile 'essential' installed successfully" @@ -140,7 +153,7 @@ test_simulated_curl_pipe() { local install_dir="$TEST_DIR/curl-sim/.opencode" - cat "$REPO_ROOT/install.sh" | bash -s essential --install-dir="$install_dir" 2>&1 | tail -5 > "$TEST_DIR/curl-output.txt" + cat "$REPO_ROOT/install.sh" | bash -s essential --install-dir="$install_dir" > "$TEST_DIR/curl-output.txt" 2>&1 if grep -q "Installation complete\|Installed:" "$TEST_DIR/curl-output.txt"; then pass "Simulated 'curl | bash -s essential' worked" diff --git a/scripts/validate-registry.sh b/scripts/validate-registry.sh index 851ba45..2018a29 100755 --- a/scripts/validate-registry.sh +++ b/scripts/validate-registry.sh @@ -180,7 +180,7 @@ suggest_fix() { if [ -n "$similar_files" ]; then echo -e " ${YELLOW}→ Possible matches:${NC}" while IFS= read -r file; do - local rel_path="${file#$REPO_ROOT/}" + local rel_path="${file#"$REPO_ROOT"/}" echo -e " ${CYAN}${rel_path}${NC}" done <<< "$similar_files" fi @@ -205,8 +205,8 @@ scan_for_orphaned_files() { # Find all .md and .ts files (excluding node_modules) while IFS= read -r file; do - local rel_path="${file#$REPO_ROOT/}" - + local rel_path="${file#"$REPO_ROOT"/}" + # Skip node_modules if [[ "$rel_path" == *"/node_modules/"* ]]; then continue