aidevops 2.106.0 → 2.108.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -92,10 +92,10 @@ The result: AI agents that work *with* your development process, not around it.
92
92
 
93
93
  ### Agent Structure
94
94
 
95
- - Primary agents (Build+, SEO, Marketing, etc.) with @plan-plus subagent for planning-only mode
96
- - 765+ subagent markdown files organized by domain
97
- - 172 helper scripts in `.agents/scripts/`
98
- - 28 slash commands for common workflows
95
+ - 11 primary agents (Build+, SEO, Marketing, etc.) with specialist @subagents on demand
96
+ - 735+ subagent markdown files organized by domain
97
+ - 194 helper scripts in `.agents/scripts/`
98
+ - 53 slash commands for common workflows
99
99
 
100
100
  <!-- AI-CONTEXT-END -->
101
101
 
@@ -257,26 +257,6 @@ See `.agents/tools/task-management/beads.md` for complete documentation and inst
257
257
 
258
258
  **Your AI assistant now has agentic access to 30+ service integrations.**
259
259
 
260
- ### OpenCode Antigravity OAuth Plugin
261
-
262
- The setup automatically installs the [opencode-antigravity-auth](https://github.com/NoeFabris/opencode-antigravity-auth) plugin, enabling Google OAuth authentication for OpenCode. This gives you access to Antigravity rate limits and premium models.
263
-
264
- **After setup, authenticate:**
265
-
266
- ```bash
267
- opencode auth login
268
- # Select: Google → OAuth with Google (Antigravity)
269
- # Press Enter to skip Project ID prompt
270
- ```
271
-
272
- **Available models via Antigravity:**
273
-
274
- - `gemini-3-pro-high` / `gemini-3-pro-low` / `gemini-3-flash`
275
- - `claude-sonnet-4-5` / `claude-sonnet-4-5-thinking` / `claude-opus-4-5-thinking`
276
- - `gpt-oss-120b-medium`
277
-
278
- **Multi-account load balancing:** Add multiple Google accounts for automatic rate limit distribution and failover. See the [plugin documentation](https://github.com/NoeFabris/opencode-antigravity-auth) for model configuration.
279
-
280
260
  ### OpenCode Anthropic OAuth (Built-in)
281
261
 
282
262
  OpenCode v1.1.36+ includes Anthropic OAuth authentication natively. No external plugin is needed.
@@ -453,7 +433,7 @@ aidevops implements proven agent design patterns identified by [Lance Martin (La
453
433
 
454
434
  | Pattern | Description | aidevops Implementation |
455
435
  |---------|-------------|------------------------|
456
- | **Give Agents a Computer** | Filesystem + shell for persistent context | `~/.aidevops/.agent-workspace/`, 172 helper scripts |
436
+ | **Give Agents a Computer** | Filesystem + shell for persistent context | `~/.aidevops/.agent-workspace/`, 194 helper scripts |
457
437
  | **Multi-Layer Action Space** | Few tools, push actions to computer | Per-agent MCP filtering (~12-20 tools each) |
458
438
  | **Progressive Disclosure** | Load context on-demand | Subagent routing with content summaries, YAML frontmatter, read-on-demand |
459
439
  | **Offload Context** | Write results to filesystem | `.agent-workspace/work/[project]/` for persistence |
@@ -465,6 +445,8 @@ aidevops implements proven agent design patterns identified by [Lance Martin (La
465
445
  | **Evolve Context** | Learn from sessions | `/remember`, `/recall` with SQLite FTS5 + opt-in semantic search |
466
446
  | **Pattern Tracking** | Learn what works/fails | `pattern-tracker-helper.sh`, `/patterns` command |
467
447
  | **Cost-Aware Routing** | Match model to task complexity | `model-routing.md` with 5-tier guidance, `/route` command |
448
+ | **Model Comparison** | Compare models side-by-side | `/compare-models` (live data), `/compare-models-free` (offline) |
449
+ | **Response Scoring** | Evaluate actual model outputs | `/score-responses` with structured criteria |
468
450
 
469
451
  **Key insight**: Context is a finite resource with diminishing returns. aidevops treats every token as precious - loading only what's needed, when it's needed.
470
452
 
@@ -886,7 +868,7 @@ See `.agents/tools/ocr/glm-ocr.md` for batch processing, PDF workflows, and Peek
886
868
 
887
869
  ## **MCP Integrations**
888
870
 
889
- **Model Context Protocol servers for real-time AI assistant integration.** The framework configures these MCPs for **[OpenCode](https://opencode.ai/)** (TUI, Desktop, and Extension for Zed/VSCode/AntiGravity).
871
+ **Model Context Protocol servers for real-time AI assistant integration.** The framework configures these MCPs for **[OpenCode](https://opencode.ai/)** (TUI, Desktop, and Extension for Zed/VSCode).
890
872
 
891
873
  ### **All Supported MCPs (19 available)**
892
874
 
@@ -1406,7 +1388,7 @@ aidevops is registered as a **Claude Code plugin marketplace**. Install with two
1406
1388
  /plugin install aidevops@aidevops
1407
1389
  ```
1408
1390
 
1409
- This installs the complete framework: 15 primary agents, 765+ subagents, and 172 helper scripts.
1391
+ This installs the complete framework: 11 primary agents, 735+ subagents, and 194 helper scripts.
1410
1392
 
1411
1393
  ### Importing External Skills
1412
1394
 
@@ -1472,29 +1454,27 @@ Call them in your AI assistant conversation with a simple @mention
1472
1454
 
1473
1455
  ### **Main Agents**
1474
1456
 
1475
- Ordered as they appear in OpenCode Tab selector and other AI assistants (15 total):
1476
-
1477
- | Name | File | Purpose | MCPs Enabled |
1478
- |------|------|---------|--------------|
1479
- | @plan-plus | `plan-plus.md` | Planning-only subagent (Build+ handles planning by default) | context7, augment, repomix |
1480
- | Build+ | `build-plus.md` | Enhanced Build with context tools | context7, augment, repomix |
1481
- | Build-Agent | `build-agent.md` | Design and improve AI agents | context7, augment, repomix |
1482
- | Build-MCP | `build-mcp.md` | Build MCP servers with TS+Bun+ElysiaJS | context7, augment, repomix |
1483
- | Accounts | `accounts.md` | Financial operations | quickfile, augment |
1484
- | AI-DevOps | `aidevops.md` | Framework operations, meta-agents, setup | context7, augment, repomix |
1485
- | Content | `content.md` | Content creation, humanise AI text | augment |
1486
- | Health | `health.md` | Health and wellness guidance | augment |
1487
- | Legal | `legal.md` | Legal compliance and documentation | augment |
1488
- | Marketing | `marketing.md` | Marketing strategy and automation | augment |
1489
- | Research | `research.md` | Research and analysis tasks | context7, augment |
1490
- | Sales | `sales.md` | Sales operations and CRM | augment |
1491
- | SEO | `seo.md` | SEO optimization, Search Console, keyword research | gsc, ahrefs, serper, context7, augment |
1492
- | Video | `video.md` | AI video generation, prompt engineering, programmatic video | augment |
1493
- | WordPress | `wordpress.md` | WordPress ecosystem (dev, admin, MainWP, LocalWP) | localwp, context7, augment |
1457
+ Primary agents as registered in `subagent-index.toon` (11 total). MCPs are loaded on-demand per subagent, not per primary agent:
1458
+
1459
+ | Name | File | Purpose | Model Tier |
1460
+ |------|------|---------|------------|
1461
+ | Build+ | `build-plus.md` | Enhanced Build with context tools (default agent) | opus |
1462
+ | Accounts | `accounts.md` | Financial operations | opus |
1463
+ | Content | `content.md` | Content creation workflows | opus |
1464
+ | Health | `health.md` | Health and wellness | opus |
1465
+ | Legal | `legal.md` | Legal compliance | opus |
1466
+ | Marketing | `marketing.md` | Marketing strategy and email campaigns | opus |
1467
+ | Research | `research.md` | Research and analysis tasks | gemini/grok |
1468
+ | Sales | `sales.md` | Sales operations and CRM pipeline | opus |
1469
+ | SEO | `seo.md` | SEO optimization and analysis | opus |
1470
+ | Social-Media | `social-media.md` | Social media management | opus |
1471
+ | Video | `video.md` | AI video generation and prompt engineering | opus |
1472
+
1473
+ **Specialist subagents** (@plan-plus, @aidevops, @wordpress, Build-Agent, Build-MCP, etc.) live under `tools/` or as `mode: subagent` files and are invoked via @mention when domain expertise is needed. See `subagent-index.toon` for the full listing.
1494
1474
 
1495
1475
  ### **Example Subagents with MCP Integration**
1496
1476
 
1497
- These are examples of subagents that have supporting MCPs enabled. See `.agents/` for the full list of 765+ subagents organized by domain.
1477
+ These are examples of subagents that have supporting MCPs enabled. See `.agents/` for the full list of 735+ subagents organized by domain.
1498
1478
 
1499
1479
  | Agent | Purpose | MCPs Enabled |
1500
1480
  |-------|---------|--------------|
@@ -1712,6 +1692,15 @@ Configure time tracking per-repo via `.aidevops.json`.
1712
1692
  | `/runners` | Batch dispatch tasks to parallel agents (task IDs, PR URLs, or descriptions) |
1713
1693
  | `/log-issue-aidevops` | Report issues with aidevops (gathers diagnostics, checks duplicates, creates GitHub issue) |
1714
1694
 
1695
+ **AI Model Comparison**:
1696
+
1697
+ | Command | Purpose |
1698
+ |---------|---------|
1699
+ | `/compare-models` | Compare AI models by pricing, context, capabilities (with live web data) |
1700
+ | `/compare-models-free` | Compare AI models using offline embedded data only (no web fetches) |
1701
+ | `/score-responses` | Score and compare actual model responses with structured criteria |
1702
+ | `/route` | Suggest optimal model tier for a task description |
1703
+
1715
1704
  ### Ralph Loop - Iterative AI Development
1716
1705
 
1717
1706
  The **Ralph Loop** (named after Ralph Wiggum's persistent optimism) enables autonomous iterative development. The AI keeps working on a task until it's complete, automatically resolving issues that arise.
@@ -2128,8 +2117,8 @@ aidevops/
2128
2117
  ├── AGENTS.md # AI agent guidance (dev)
2129
2118
  ├── .agents/ # Agents and documentation
2130
2119
  │ ├── AGENTS.md # User guide (deployed to ~/.aidevops/agents/)
2131
- │ ├── *.md # 15 primary agents
2132
- │ ├── scripts/ # 172 helper scripts
2120
+ │ ├── *.md # 11 primary agents
2121
+ │ ├── scripts/ # 194 helper scripts
2133
2122
  │ ├── tools/ # Cross-domain utilities (video, browser, git, etc.)
2134
2123
  │ ├── services/ # External service integrations
2135
2124
  │ └── workflows/ # Development process guides
package/VERSION CHANGED
@@ -1 +1 @@
1
- 2.106.0
1
+ 2.108.0
package/aidevops.sh CHANGED
@@ -3,7 +3,7 @@
3
3
  # AI DevOps Framework CLI
4
4
  # Usage: aidevops <command> [options]
5
5
  #
6
- # Version: 2.106.0
6
+ # Version: 2.108.0
7
7
 
8
8
  set -euo pipefail
9
9
 
@@ -597,6 +597,7 @@ cmd_update() {
597
597
  print_error "Failed to create temp file for setup script"
598
598
  return 1
599
599
  }
600
+ trap 'rm -f "${tmp_setup:-}"' RETURN
600
601
  if curl -fsSL "https://raw.githubusercontent.com/marcusquinn/aidevops/main/setup.sh" -o "$tmp_setup" 2>/dev/null && [[ -s "$tmp_setup" ]]; then
601
602
  chmod +x "$tmp_setup"
602
603
  bash "$tmp_setup"
@@ -851,6 +852,7 @@ cmd_init() {
851
852
  local enable_time_tracking=false
852
853
  local enable_database=false
853
854
  local enable_beads=false
855
+ local enable_sops=false
854
856
 
855
857
  case "$features" in
856
858
  all)
@@ -881,6 +883,9 @@ cmd_init() {
881
883
  enable_beads=true
882
884
  enable_planning=true # beads requires planning
883
885
  ;;
886
+ sops)
887
+ enable_sops=true
888
+ ;;
884
889
  *)
885
890
  # Comma-separated list
886
891
  IFS=',' read -ra FEATURE_LIST <<< "$features"
@@ -898,6 +903,7 @@ cmd_init() {
898
903
  enable_beads=true
899
904
  enable_planning=true
900
905
  ;;
906
+ sops) enable_sops=true ;;
901
907
  esac
902
908
  done
903
909
  ;;
@@ -933,13 +939,31 @@ cmd_init() {
933
939
  "seeds_path": "seeds",
934
940
  "auto_generate_migration": true
935
941
  },
936
- "beads": {
937
- "enabled": $enable_beads,
938
- "sync_on_commit": false,
939
- "auto_ready_check": true
940
- }
942
+ "beads": {
943
+ "enabled": $enable_beads,
944
+ "sync_on_commit": false,
945
+ "auto_ready_check": true
946
+ },
947
+ "sops": {
948
+ "enabled": $enable_sops,
949
+ "backend": "age",
950
+ "patterns": ["*.secret.yaml", "*.secret.json", "configs/*.enc.json", "configs/*.enc.yaml"]
951
+ }
952
+ },
953
+ "plugins": []
941
954
  }
942
955
  EOF
956
+ # Note: plugins array is always present but empty by default.
957
+ # Users add plugins via: aidevops plugin add <repo-url> [--namespace <name>]
958
+ # Schema per plugin entry:
959
+ # {
960
+ # "name": "pro",
961
+ # "repo": "https://github.com/user/aidevops-pro.git",
962
+ # "branch": "main",
963
+ # "namespace": "pro",
964
+ # "enabled": true
965
+ # }
966
+ # Plugins deploy to ~/.aidevops/agents/<namespace>/ (namespaced, no collisions)
943
967
  print_success "Created .aidevops.json"
944
968
 
945
969
  # Create .agents symlink (or migrate from legacy .agent)
@@ -1109,6 +1133,77 @@ EOF
1109
1133
  fi
1110
1134
  fi
1111
1135
 
1136
+ # Initialize SOPS if enabled
1137
+ if [[ "$enable_sops" == "true" ]]; then
1138
+ print_info "Setting up SOPS encrypted config support..."
1139
+
1140
+ # Check for sops and age
1141
+ local sops_ready=true
1142
+ if ! command -v sops &>/dev/null; then
1143
+ print_warning "SOPS not installed"
1144
+ echo " Install with: brew install sops"
1145
+ sops_ready=false
1146
+ fi
1147
+ if ! command -v age-keygen &>/dev/null; then
1148
+ print_warning "age not installed (default SOPS backend)"
1149
+ echo " Install with: brew install age"
1150
+ sops_ready=false
1151
+ fi
1152
+
1153
+ # Generate age key if none exists
1154
+ local age_key_file="$HOME/.config/sops/age/keys.txt"
1155
+ if [[ "$sops_ready" == "true" ]] && [[ ! -f "$age_key_file" ]]; then
1156
+ print_info "Generating age key for SOPS..."
1157
+ mkdir -p "$(dirname "$age_key_file")"
1158
+ age-keygen -o "$age_key_file" 2>/dev/null
1159
+ chmod 600 "$age_key_file"
1160
+ print_success "Age key generated at $age_key_file"
1161
+ fi
1162
+
1163
+ # Create .sops.yaml if it doesn't exist
1164
+ if [[ ! -f "$project_root/.sops.yaml" ]]; then
1165
+ local age_pubkey=""
1166
+ if [[ -f "$age_key_file" ]]; then
1167
+ age_pubkey=$(grep -o 'age1[a-z0-9]*' "$age_key_file" | head -1)
1168
+ fi
1169
+
1170
+ if [[ -n "$age_pubkey" ]]; then
1171
+ cat > "$project_root/.sops.yaml" << SOPSEOF
1172
+ # SOPS configuration - encrypts values in config files while keeping keys visible
1173
+ # See: .agents/tools/credentials/sops.md
1174
+ creation_rules:
1175
+ - path_regex: '\.secret\.(yaml|yml|json)$'
1176
+ age: >-
1177
+ $age_pubkey
1178
+ - path_regex: 'configs/.*\.enc\.(yaml|yml|json)$'
1179
+ age: >-
1180
+ $age_pubkey
1181
+ SOPSEOF
1182
+ print_success "Created .sops.yaml with age key"
1183
+ else
1184
+ cat > "$project_root/.sops.yaml" << 'SOPSEOF'
1185
+ # SOPS configuration - encrypts values in config files while keeping keys visible
1186
+ # See: .agents/tools/credentials/sops.md
1187
+ #
1188
+ # Generate an age key first:
1189
+ # age-keygen -o ~/.config/sops/age/keys.txt
1190
+ #
1191
+ # Then replace AGE_PUBLIC_KEY below with your public key:
1192
+ creation_rules:
1193
+ - path_regex: '\.secret\.(yaml|yml|json)$'
1194
+ age: >-
1195
+ AGE_PUBLIC_KEY
1196
+ - path_regex: 'configs/.*\.enc\.(yaml|yml|json)$'
1197
+ age: >-
1198
+ AGE_PUBLIC_KEY
1199
+ SOPSEOF
1200
+ print_warning "Created .sops.yaml template (replace AGE_PUBLIC_KEY with your key)"
1201
+ fi
1202
+ else
1203
+ print_info ".sops.yaml already exists"
1204
+ fi
1205
+ fi
1206
+
1112
1207
  # Add to .gitignore if needed
1113
1208
  local gitignore="$project_root/.gitignore"
1114
1209
  if [[ -f "$gitignore" ]]; then
@@ -1159,6 +1254,7 @@ EOF
1159
1254
  [[ "$enable_time_tracking" == "true" ]] && features_list="${features_list}time-tracking,"
1160
1255
  [[ "$enable_database" == "true" ]] && features_list="${features_list}database,"
1161
1256
  [[ "$enable_beads" == "true" ]] && features_list="${features_list}beads,"
1257
+ [[ "$enable_sops" == "true" ]] && features_list="${features_list}sops,"
1162
1258
  features_list="${features_list%,}" # Remove trailing comma
1163
1259
 
1164
1260
  # Register repo in repos.json
@@ -1174,6 +1270,7 @@ EOF
1174
1270
  [[ "$enable_time_tracking" == "true" ]] && echo " ✓ Time tracking (estimates, actuals)"
1175
1271
  [[ "$enable_database" == "true" ]] && echo " ✓ Database (schemas/, migrations/, seeds/)"
1176
1272
  [[ "$enable_beads" == "true" ]] && echo " ✓ Beads (task graph visualization)"
1273
+ [[ "$enable_sops" == "true" ]] && echo " ✓ SOPS (encrypted config files with age backend)"
1177
1274
  echo ""
1178
1275
  echo "Next steps:"
1179
1276
  if [[ "$enable_beads" == "true" ]]; then
@@ -1388,6 +1485,7 @@ cmd_upgrade_planning() {
1388
1485
  local temp_file="${todo_file}.merge"
1389
1486
  local tasks_file
1390
1487
  tasks_file=$(mktemp)
1488
+ trap 'rm -f "${tasks_file:-}"' RETURN
1391
1489
  printf '%s\n' "$existing_tasks" > "$tasks_file"
1392
1490
  # Use while-read to avoid BSD awk "newline in string" warning with -v
1393
1491
  local in_backlog=false
@@ -1452,6 +1550,7 @@ cmd_upgrade_planning() {
1452
1550
  local temp_file="${plans_file}.merge"
1453
1551
  local plans_content_file
1454
1552
  plans_content_file=$(mktemp)
1553
+ trap 'rm -f "${plans_content_file:-}"' RETURN
1455
1554
  printf '%s\n' "$existing_plans" > "$plans_content_file"
1456
1555
  # Use while-read to avoid BSD awk "newline in string" warning with -v
1457
1556
  local in_active=false
@@ -1558,12 +1657,27 @@ cmd_features() {
1558
1657
  echo " - Ready task detection (/ready)"
1559
1658
  echo " - Bi-directional sync with TODO.md/PLANS.md"
1560
1659
  echo ""
1660
+ echo " sops Encrypted config files with SOPS + age"
1661
+ echo " - Value-level encryption (keys visible, values encrypted)"
1662
+ echo " - .sops.yaml with age backend (simpler than GPG)"
1663
+ echo " - Patterns: *.secret.yaml, configs/*.enc.json"
1664
+ echo " - See: .agents/tools/credentials/sops.md"
1665
+ echo ""
1666
+ echo "Extensibility:"
1667
+ echo ""
1668
+ echo " plugins Third-party agent plugins (configured in .aidevops.json)"
1669
+ echo " - Git repos deployed to ~/.aidevops/agents/<namespace>/"
1670
+ echo " - Namespaced to avoid collisions with core agents"
1671
+ echo " - Enable/disable per-plugin without removal"
1672
+ echo " - See: .agents/aidevops/plugins.md"
1673
+ echo ""
1561
1674
  echo "Usage:"
1562
- echo " aidevops init # Enable all features"
1675
+ echo " aidevops init # Enable all features (except sops)"
1563
1676
  echo " aidevops init planning # Enable only planning"
1677
+ echo " aidevops init sops # Enable SOPS encryption"
1564
1678
  echo " aidevops init beads # Enable beads (includes planning)"
1565
1679
  echo " aidevops init database # Enable only database"
1566
- echo " aidevops init planning,database # Enable multiple"
1680
+ echo " aidevops init planning,sops # Enable multiple"
1567
1681
  echo ""
1568
1682
  }
1569
1683
 
@@ -1980,6 +2094,444 @@ cmd_skill() {
1980
2094
  esac
1981
2095
  }
1982
2096
 
2097
+ # Plugin management command
2098
+ cmd_plugin() {
2099
+ local action="${1:-help}"
2100
+ shift || true
2101
+
2102
+ local plugins_file="$CONFIG_DIR/plugins.json"
2103
+ local agents_dir="$AGENTS_DIR"
2104
+
2105
+ # Reserved namespaces that plugins cannot use
2106
+ local reserved_namespaces="custom draft scripts tools services workflows templates memory plugins seo wordpress aidevops"
2107
+
2108
+ # Ensure config dir exists
2109
+ mkdir -p "$CONFIG_DIR"
2110
+
2111
+ # Initialize plugins.json if missing
2112
+ if [[ ! -f "$plugins_file" ]]; then
2113
+ echo '{"plugins":[]}' > "$plugins_file"
2114
+ fi
2115
+
2116
+ #######################################
2117
+ # Validate a namespace is safe to use
2118
+ # Arguments: namespace
2119
+ # Returns: 0 if valid, 1 if reserved/invalid
2120
+ #######################################
2121
+ validate_namespace() {
2122
+ local ns="$1"
2123
+ # Must be lowercase alphanumeric with hyphens
2124
+ if [[ ! "$ns" =~ ^[a-z][a-z0-9-]*$ ]]; then
2125
+ print_error "Invalid namespace '$ns': must be lowercase alphanumeric with hyphens, starting with a letter"
2126
+ return 1
2127
+ fi
2128
+ # Must not be reserved
2129
+ local reserved
2130
+ for reserved in $reserved_namespaces; do
2131
+ if [[ "$ns" == "$reserved" ]]; then
2132
+ print_error "Namespace '$ns' is reserved. Choose a different name."
2133
+ return 1
2134
+ fi
2135
+ done
2136
+ return 0
2137
+ }
2138
+
2139
+ #######################################
2140
+ # Get a plugin field from plugins.json
2141
+ # Arguments: plugin_name, field
2142
+ #######################################
2143
+ get_plugin_field() {
2144
+ local name="$1"
2145
+ local field="$2"
2146
+ jq -r --arg n "$name" --arg f "$field" '.plugins[] | select(.name == $n) | .[$f] // empty' "$plugins_file" 2>/dev/null || echo ""
2147
+ }
2148
+
2149
+ case "$action" in
2150
+ add|a)
2151
+ if [[ $# -lt 1 ]]; then
2152
+ print_error "Repository URL required"
2153
+ echo ""
2154
+ echo "Usage: aidevops plugin add <repo-url> [options]"
2155
+ echo ""
2156
+ echo "Options:"
2157
+ echo " --namespace <name> Namespace directory (default: derived from repo name)"
2158
+ echo " --branch <branch> Branch to track (default: main)"
2159
+ echo " --name <name> Human-readable name (default: derived from repo)"
2160
+ echo ""
2161
+ echo "Examples:"
2162
+ echo " aidevops plugin add https://github.com/marcusquinn/aidevops-pro.git --namespace pro"
2163
+ echo " aidevops plugin add https://github.com/marcusquinn/aidevops-anon.git --namespace anon"
2164
+ return 1
2165
+ fi
2166
+
2167
+ local repo_url="$1"
2168
+ shift
2169
+ local namespace="" branch="main" plugin_name=""
2170
+
2171
+ # Parse options
2172
+ while [[ $# -gt 0 ]]; do
2173
+ case "$1" in
2174
+ --namespace|--ns) namespace="$2"; shift 2 ;;
2175
+ --branch|-b) branch="$2"; shift 2 ;;
2176
+ --name|-n) plugin_name="$2"; shift 2 ;;
2177
+ *) print_error "Unknown option: $1"; return 1 ;;
2178
+ esac
2179
+ done
2180
+
2181
+ # Derive namespace from repo URL if not provided
2182
+ if [[ -z "$namespace" ]]; then
2183
+ namespace=$(basename "$repo_url" .git | sed 's/^aidevops-//')
2184
+ namespace=$(echo "$namespace" | tr '[:upper:]' '[:lower:]' | sed 's/[^a-z0-9-]/-/g')
2185
+ fi
2186
+
2187
+ # Derive name from namespace if not provided
2188
+ if [[ -z "$plugin_name" ]]; then
2189
+ plugin_name="$namespace"
2190
+ fi
2191
+
2192
+ # Validate namespace
2193
+ if ! validate_namespace "$namespace"; then
2194
+ return 1
2195
+ fi
2196
+
2197
+ # Check if plugin already exists
2198
+ local existing
2199
+ existing=$(jq -r --arg n "$plugin_name" '.plugins[] | select(.name == $n) | .name' "$plugins_file" 2>/dev/null || echo "")
2200
+ if [[ -n "$existing" ]]; then
2201
+ print_error "Plugin '$plugin_name' already exists. Use 'aidevops plugin update $plugin_name' to update."
2202
+ return 1
2203
+ fi
2204
+
2205
+ # Check if namespace is already in use
2206
+ if [[ -d "$agents_dir/$namespace" ]]; then
2207
+ local ns_owner
2208
+ ns_owner=$(jq -r --arg ns "$namespace" '.plugins[] | select(.namespace == $ns) | .name' "$plugins_file" 2>/dev/null || echo "")
2209
+ if [[ -n "$ns_owner" ]]; then
2210
+ print_error "Namespace '$namespace' is already used by plugin '$ns_owner'"
2211
+ else
2212
+ print_error "Directory '$agents_dir/$namespace/' already exists"
2213
+ echo " Choose a different namespace with --namespace <name>"
2214
+ fi
2215
+ return 1
2216
+ fi
2217
+
2218
+ print_info "Adding plugin '$plugin_name' from $repo_url..."
2219
+ print_info " Namespace: $namespace"
2220
+ print_info " Branch: $branch"
2221
+
2222
+ # Clone the repo
2223
+ local clone_dir="$agents_dir/$namespace"
2224
+ if ! git clone --branch "$branch" --depth 1 "$repo_url" "$clone_dir" 2>&1; then
2225
+ print_error "Failed to clone repository"
2226
+ rm -rf "$clone_dir" 2>/dev/null || true
2227
+ return 1
2228
+ fi
2229
+
2230
+ # Remove .git directory (we track via plugins.json, not nested git)
2231
+ rm -rf "$clone_dir/.git"
2232
+
2233
+ # Add to plugins.json
2234
+ local tmp_file="${plugins_file}.tmp"
2235
+ jq --arg name "$plugin_name" \
2236
+ --arg repo "$repo_url" \
2237
+ --arg branch "$branch" \
2238
+ --arg ns "$namespace" \
2239
+ '.plugins += [{"name": $name, "repo": $repo, "branch": $branch, "namespace": $ns, "enabled": true}]' \
2240
+ "$plugins_file" > "$tmp_file" && mv "$tmp_file" "$plugins_file"
2241
+
2242
+ print_success "Plugin '$plugin_name' installed to $clone_dir"
2243
+ echo ""
2244
+ echo " Agents available at: ~/.aidevops/agents/$namespace/"
2245
+ echo " Update: aidevops plugin update $plugin_name"
2246
+ echo " Remove: aidevops plugin remove $plugin_name"
2247
+ ;;
2248
+
2249
+ list|ls|l)
2250
+ local count
2251
+ count=$(jq '.plugins | length' "$plugins_file" 2>/dev/null || echo "0")
2252
+
2253
+ if [[ "$count" == "0" ]]; then
2254
+ echo "No plugins installed."
2255
+ echo ""
2256
+ echo "Add a plugin: aidevops plugin add <repo-url> --namespace <name>"
2257
+ return 0
2258
+ fi
2259
+
2260
+ echo "Installed plugins ($count):"
2261
+ echo ""
2262
+ printf " %-15s %-10s %-8s %s\n" "NAME" "NAMESPACE" "ENABLED" "REPO"
2263
+ printf " %-15s %-10s %-8s %s\n" "----" "---------" "-------" "----"
2264
+
2265
+ jq -r '.plugins[] | " \(.name)\t\(.namespace)\t\(.enabled // true)\t\(.repo)"' "$plugins_file" 2>/dev/null | \
2266
+ while IFS=$'\t' read -r name ns enabled repo; do
2267
+ local status_icon="yes"
2268
+ if [[ "$enabled" == "false" ]]; then
2269
+ status_icon="no"
2270
+ fi
2271
+ printf " %-15s %-10s %-8s %s\n" "$name" "$ns" "$status_icon" "$repo"
2272
+ done
2273
+ ;;
2274
+
2275
+ update|u)
2276
+ local target="${1:-}"
2277
+
2278
+ if [[ -n "$target" ]]; then
2279
+ # Update specific plugin
2280
+ local repo ns branch_name
2281
+ repo=$(get_plugin_field "$target" "repo")
2282
+ ns=$(get_plugin_field "$target" "namespace")
2283
+ branch_name=$(get_plugin_field "$target" "branch")
2284
+ branch_name="${branch_name:-main}"
2285
+
2286
+ if [[ -z "$repo" ]]; then
2287
+ print_error "Plugin '$target' not found"
2288
+ return 1
2289
+ fi
2290
+
2291
+ print_info "Updating plugin '$target'..."
2292
+ local clone_dir="$agents_dir/$ns"
2293
+ rm -rf "$clone_dir"
2294
+ if git clone --branch "$branch_name" --depth 1 "$repo" "$clone_dir" 2>&1; then
2295
+ rm -rf "$clone_dir/.git"
2296
+ print_success "Plugin '$target' updated"
2297
+ else
2298
+ print_error "Failed to update plugin '$target'"
2299
+ return 1
2300
+ fi
2301
+ else
2302
+ # Update all enabled plugins
2303
+ local names
2304
+ names=$(jq -r '.plugins[] | select(.enabled != false) | .name' "$plugins_file" 2>/dev/null || echo "")
2305
+ if [[ -z "$names" ]]; then
2306
+ echo "No enabled plugins to update."
2307
+ return 0
2308
+ fi
2309
+
2310
+ local failed=0
2311
+ while IFS= read -r pname; do
2312
+ [[ -z "$pname" ]] && continue
2313
+ local prepo pns pbranch
2314
+ prepo=$(get_plugin_field "$pname" "repo")
2315
+ pns=$(get_plugin_field "$pname" "namespace")
2316
+ pbranch=$(get_plugin_field "$pname" "branch")
2317
+ pbranch="${pbranch:-main}"
2318
+
2319
+ print_info "Updating '$pname'..."
2320
+ local pdir="$agents_dir/$pns"
2321
+ rm -rf "$pdir"
2322
+ if git clone --branch "$pbranch" --depth 1 "$prepo" "$pdir" 2>/dev/null; then
2323
+ rm -rf "$pdir/.git"
2324
+ print_success " '$pname' updated"
2325
+ else
2326
+ print_error " '$pname' failed to update"
2327
+ failed=$((failed + 1))
2328
+ fi
2329
+ done <<< "$names"
2330
+
2331
+ if [[ "$failed" -gt 0 ]]; then
2332
+ print_warning "$failed plugin(s) failed to update"
2333
+ return 1
2334
+ fi
2335
+ print_success "All plugins updated"
2336
+ fi
2337
+ ;;
2338
+
2339
+ enable)
2340
+ if [[ $# -lt 1 ]]; then
2341
+ print_error "Plugin name required"
2342
+ echo "Usage: aidevops plugin enable <name>"
2343
+ return 1
2344
+ fi
2345
+ local target_name="$1"
2346
+ local target_repo target_ns target_branch
2347
+ target_repo=$(get_plugin_field "$target_name" "repo")
2348
+ if [[ -z "$target_repo" ]]; then
2349
+ print_error "Plugin '$target_name' not found"
2350
+ return 1
2351
+ fi
2352
+
2353
+ target_ns=$(get_plugin_field "$target_name" "namespace")
2354
+ target_branch=$(get_plugin_field "$target_name" "branch")
2355
+ target_branch="${target_branch:-main}"
2356
+
2357
+ # Update enabled flag
2358
+ local tmp_file="${plugins_file}.tmp"
2359
+ jq --arg n "$target_name" '(.plugins[] | select(.name == $n)).enabled = true' "$plugins_file" > "$tmp_file" && mv "$tmp_file" "$plugins_file"
2360
+
2361
+ # Deploy if not already present
2362
+ if [[ ! -d "$agents_dir/$target_ns" ]]; then
2363
+ print_info "Deploying plugin '$target_name'..."
2364
+ if git clone --branch "$target_branch" --depth 1 "$target_repo" "$agents_dir/$target_ns" 2>/dev/null; then
2365
+ rm -rf "$agents_dir/$target_ns/.git"
2366
+ fi
2367
+ fi
2368
+
2369
+ print_success "Plugin '$target_name' enabled"
2370
+ ;;
2371
+
2372
+ disable)
2373
+ if [[ $# -lt 1 ]]; then
2374
+ print_error "Plugin name required"
2375
+ echo "Usage: aidevops plugin disable <name>"
2376
+ return 1
2377
+ fi
2378
+ local target_name="$1"
2379
+ local target_ns
2380
+ target_ns=$(get_plugin_field "$target_name" "namespace")
2381
+ if [[ -z "$target_ns" ]]; then
2382
+ print_error "Plugin '$target_name' not found"
2383
+ return 1
2384
+ fi
2385
+
2386
+ # Update enabled flag
2387
+ local tmp_file="${plugins_file}.tmp"
2388
+ jq --arg n "$target_name" '(.plugins[] | select(.name == $n)).enabled = false' "$plugins_file" > "$tmp_file" && mv "$tmp_file" "$plugins_file"
2389
+
2390
+ # Remove deployed files
2391
+ if [[ -d "$agents_dir/$target_ns" ]]; then
2392
+ rm -rf "$agents_dir/$target_ns"
2393
+ fi
2394
+
2395
+ print_success "Plugin '$target_name' disabled (config preserved)"
2396
+ ;;
2397
+
2398
+ remove|rm)
2399
+ if [[ $# -lt 1 ]]; then
2400
+ print_error "Plugin name required"
2401
+ echo "Usage: aidevops plugin remove <name>"
2402
+ return 1
2403
+ fi
2404
+ local target_name="$1"
2405
+ local target_ns
2406
+ target_ns=$(get_plugin_field "$target_name" "namespace")
2407
+ if [[ -z "$target_ns" ]]; then
2408
+ print_error "Plugin '$target_name' not found"
2409
+ return 1
2410
+ fi
2411
+
2412
+ # Remove deployed files
2413
+ if [[ -d "$agents_dir/$target_ns" ]]; then
2414
+ rm -rf "$agents_dir/$target_ns"
2415
+ print_info "Removed $agents_dir/$target_ns/"
2416
+ fi
2417
+
2418
+ # Remove from plugins.json
2419
+ local tmp_file="${plugins_file}.tmp"
2420
+ jq --arg n "$target_name" '.plugins = [.plugins[] | select(.name != $n)]' "$plugins_file" > "$tmp_file" && mv "$tmp_file" "$plugins_file"
2421
+
2422
+ print_success "Plugin '$target_name' removed"
2423
+ ;;
2424
+
2425
+ init)
2426
+ local target_dir="${1:-.}"
2427
+ local plugin_name="${2:-my-plugin}"
2428
+ local namespace="${3:-$plugin_name}"
2429
+
2430
+ if [[ "$target_dir" != "." && -d "$target_dir" ]]; then
2431
+ local existing_count
2432
+ existing_count=$(find "$target_dir" -maxdepth 1 -type f | wc -l | tr -d ' ')
2433
+ if [[ "$existing_count" -gt 0 ]]; then
2434
+ print_error "Directory '$target_dir' already has files. Use an empty directory."
2435
+ return 1
2436
+ fi
2437
+ fi
2438
+
2439
+ mkdir -p "$target_dir"
2440
+
2441
+ local template_dir="$agents_dir/templates/plugin-template"
2442
+ if [[ ! -d "$template_dir" ]]; then
2443
+ print_error "Plugin template not found at $template_dir"
2444
+ print_info "Run 'aidevops update' to get the latest templates."
2445
+ return 1
2446
+ fi
2447
+
2448
+ # Copy template files with placeholder substitution
2449
+ local plugin_name_upper
2450
+ plugin_name_upper=$(echo "$plugin_name" | tr '[:lower:]' '[:upper:]' | tr '-' '_')
2451
+
2452
+ # AGENTS.md
2453
+ sed -e "s|{{PLUGIN_NAME}}|$plugin_name|g" \
2454
+ -e "s|{{PLUGIN_NAME_UPPER}}|$plugin_name_upper|g" \
2455
+ -e "s|{{NAMESPACE}}|$namespace|g" \
2456
+ -e "s|{{REPO_URL}}|https://github.com/user/aidevops-$namespace.git|g" \
2457
+ "$template_dir/AGENTS.md" > "$target_dir/AGENTS.md"
2458
+
2459
+ # Main agent file
2460
+ sed -e "s|{{PLUGIN_NAME}}|$plugin_name|g" \
2461
+ -e "s|{{PLUGIN_DESCRIPTION}}|$plugin_name plugin for aidevops|g" \
2462
+ -e "s|{{NAMESPACE}}|$namespace|g" \
2463
+ "$template_dir/main-agent.md" > "$target_dir/$namespace.md"
2464
+
2465
+ # Example subagent directory
2466
+ mkdir -p "$target_dir/$namespace"
2467
+ sed -e "s|{{PLUGIN_NAME}}|$plugin_name|g" \
2468
+ -e "s|{{NAMESPACE}}|$namespace|g" \
2469
+ "$template_dir/example-subagent.md" > "$target_dir/$namespace/example.md"
2470
+
2471
+ # Scripts directory
2472
+ mkdir -p "$target_dir/scripts"
2473
+
2474
+ print_success "Plugin scaffolded in $target_dir/"
2475
+ echo ""
2476
+ echo "Structure:"
2477
+ echo " $target_dir/"
2478
+ echo " ├── AGENTS.md # Plugin documentation"
2479
+ echo " ├── $namespace.md # Main agent"
2480
+ echo " ├── $namespace/"
2481
+ echo " │ └── example.md # Example subagent"
2482
+ echo " └── scripts/ # Helper scripts (empty)"
2483
+ echo ""
2484
+ echo "Next steps:"
2485
+ echo " 1. Edit $namespace.md with your agent instructions"
2486
+ echo " 2. Add subagents to $namespace/"
2487
+ echo " 3. Push to a git repo"
2488
+ echo " 4. Install: aidevops plugin add <repo-url> --namespace $namespace"
2489
+ ;;
2490
+
2491
+ help|--help|-h)
2492
+ print_header "Plugin Management"
2493
+ echo ""
2494
+ echo "Manage third-party agent plugins that extend aidevops."
2495
+ echo "Plugins deploy to ~/.aidevops/agents/<namespace>/ (isolated from core)."
2496
+ echo ""
2497
+ echo "Usage: aidevops plugin <command> [options]"
2498
+ echo ""
2499
+ echo "Commands:"
2500
+ echo " add <repo-url> Install a plugin from a git repository"
2501
+ echo " list List installed plugins"
2502
+ echo " update [name] Update specific or all plugins"
2503
+ echo " enable <name> Enable a disabled plugin (redeploys files)"
2504
+ echo " disable <name> Disable a plugin (removes files, keeps config)"
2505
+ echo " remove <name> Remove a plugin entirely"
2506
+ echo " init [dir] [name] Scaffold a new plugin from template"
2507
+ echo ""
2508
+ echo "Options for 'add':"
2509
+ echo " --namespace <name> Directory name under ~/.aidevops/agents/"
2510
+ echo " --branch <branch> Branch to track (default: main)"
2511
+ echo " --name <name> Human-readable plugin name"
2512
+ echo ""
2513
+ echo "Examples:"
2514
+ echo " aidevops plugin add https://github.com/marcusquinn/aidevops-pro.git --namespace pro"
2515
+ echo " aidevops plugin add https://github.com/marcusquinn/aidevops-anon.git --namespace anon"
2516
+ echo " aidevops plugin list"
2517
+ echo " aidevops plugin update"
2518
+ echo " aidevops plugin update pro"
2519
+ echo " aidevops plugin disable pro"
2520
+ echo " aidevops plugin enable pro"
2521
+ echo " aidevops plugin remove pro"
2522
+ echo " aidevops plugin init ./my-plugin my-plugin"
2523
+ echo ""
2524
+ echo "Plugin docs: ~/.aidevops/agents/aidevops/plugins.md"
2525
+ ;;
2526
+ *)
2527
+ print_error "Unknown plugin command: $action"
2528
+ echo "Run 'aidevops plugin help' for usage information."
2529
+ return 1
2530
+ ;;
2531
+ esac
2532
+ return 0
2533
+ }
2534
+
1983
2535
  # Help command
1984
2536
  cmd_help() {
1985
2537
  local version
@@ -1994,6 +2546,7 @@ cmd_help() {
1994
2546
  echo " upgrade-planning Upgrade TODO.md/PLANS.md to latest templates"
1995
2547
  echo " features List available features for init"
1996
2548
  echo " skill <cmd> Manage agent skills (add/list/check/update/remove)"
2549
+ echo " plugin <cmd> Manage plugins (add/list/update/enable/disable/remove)"
1997
2550
  echo " status Check installation status of all components"
1998
2551
  echo " update Update aidevops to the latest version (alias: upgrade)"
1999
2552
  echo " upgrade Alias for update"
@@ -2027,6 +2580,12 @@ cmd_help() {
2027
2580
  echo " aidevops secret import # Import from credentials.sh to gopass"
2028
2581
  echo " aidevops secret status # Show backend status"
2029
2582
  echo ""
2583
+ echo "Plugins:"
2584
+ echo " aidevops plugin add <url> # Install a plugin from git repo"
2585
+ echo " aidevops plugin list # List installed plugins"
2586
+ echo " aidevops plugin update # Update all plugins"
2587
+ echo " aidevops plugin remove <n> # Remove a plugin"
2588
+ echo ""
2030
2589
  echo "Skills:"
2031
2590
  echo " aidevops skill add <source> # Import a skill from GitHub"
2032
2591
  echo " aidevops skill list # List imported skills"
@@ -2118,6 +2677,10 @@ main() {
2118
2677
  shift
2119
2678
  cmd_skill "$@"
2120
2679
  ;;
2680
+ plugin|plugins)
2681
+ shift
2682
+ cmd_plugin "$@"
2683
+ ;;
2121
2684
  detect|scan)
2122
2685
  cmd_detect
2123
2686
  ;;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "aidevops",
3
- "version": "2.106.0",
3
+ "version": "2.108.0",
4
4
  "description": "AI DevOps Framework - AI-assisted development workflows, code quality, and deployment automation",
5
5
  "type": "module",
6
6
  "bin": {
package/setup.sh CHANGED
@@ -4,7 +4,7 @@ set -euo pipefail
4
4
  # AI Assistant Server Access Framework Setup Script
5
5
  # Helps developers set up the framework for their infrastructure
6
6
  #
7
- # Version: 2.106.0
7
+ # Version: 2.108.0
8
8
  #
9
9
  # Quick Install:
10
10
  # npm install -g aidevops && aidevops update (recommended)
@@ -21,7 +21,7 @@ NC='\033[0m' # No Color
21
21
  # Global flags
22
22
  CLEAN_MODE=false
23
23
  INTERACTIVE_MODE=false
24
- NON_INTERACTIVE=false
24
+ NON_INTERACTIVE="${AIDEVOPS_NON_INTERACTIVE:-false}"
25
25
  UPDATE_TOOLS_MODE=false
26
26
  REPO_URL="https://github.com/marcusquinn/aidevops.git"
27
27
  INSTALL_DIR="$HOME/Git/aidevops"
@@ -334,6 +334,7 @@ cleanup_deprecated_paths() {
334
334
  if jq -e '.plugin | index("oh-my-opencode")' "$opencode_config" >/dev/null 2>&1; then
335
335
  local tmp_file
336
336
  tmp_file=$(mktemp)
337
+ trap 'rm -f "${tmp_file:-}"' RETURN
337
338
  jq '.plugin = [.plugin[] | select(. != "oh-my-opencode")]' "$opencode_config" > "$tmp_file" && mv "$tmp_file" "$opencode_config"
338
339
  print_info "Removed oh-my-opencode from OpenCode plugin list"
339
340
  fi
@@ -514,6 +515,7 @@ cleanup_deprecated_mcps() {
514
515
  local cleaned=0
515
516
  local tmp_config
516
517
  tmp_config=$(mktemp)
518
+ trap 'rm -f "${tmp_config:-}"' RETURN
517
519
 
518
520
  cp "$opencode_config" "$tmp_config"
519
521
 
@@ -637,6 +639,7 @@ disable_ondemand_mcps() {
637
639
  local disabled=0
638
640
  local tmp_config
639
641
  tmp_config=$(mktemp)
642
+ trap 'rm -f "${tmp_config:-}"' RETURN
640
643
 
641
644
  cp "$opencode_config" "$tmp_config"
642
645
 
@@ -714,6 +717,7 @@ validate_opencode_config() {
714
717
  if jq -e ".[\"$key\"]" "$opencode_config" > /dev/null 2>&1; then
715
718
  local tmp_fix
716
719
  tmp_fix=$(mktemp)
720
+ trap 'rm -f "${tmp_fix:-}"' RETURN
717
721
  if jq "del(.[\"$key\"])" "$opencode_config" > "$tmp_fix" 2>/dev/null; then
718
722
  create_backup_with_rotation "$opencode_config" "opencode"
719
723
  mv "$tmp_fix" "$opencode_config"
@@ -2542,6 +2546,7 @@ deploy_aidevops_agents() {
2542
2546
  script_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
2543
2547
  local source_dir="$script_dir/.agents"
2544
2548
  local target_dir="$HOME/.aidevops/agents"
2549
+ local plugins_file="$HOME/.config/aidevops/plugins.json"
2545
2550
 
2546
2551
  # Validate source directory exists (catches curl install from wrong directory)
2547
2552
  if [[ ! -d "$source_dir" ]]; then
@@ -2554,6 +2559,14 @@ deploy_aidevops_agents() {
2554
2559
  return 1
2555
2560
  fi
2556
2561
 
2562
+ # Collect plugin namespace directories to preserve during deployment
2563
+ local -a plugin_namespaces=()
2564
+ if [[ -f "$plugins_file" ]] && command -v jq &>/dev/null; then
2565
+ while IFS= read -r ns; do
2566
+ [[ -n "$ns" ]] && plugin_namespaces+=("$ns")
2567
+ done < <(jq -r '.plugins[].namespace // empty' "$plugins_file" 2>/dev/null)
2568
+ fi
2569
+
2557
2570
  # Create backup if target exists (with rotation)
2558
2571
  if [[ -d "$target_dir" ]]; then
2559
2572
  create_backup_with_rotation "$target_dir" "agents"
@@ -2562,14 +2575,21 @@ deploy_aidevops_agents() {
2562
2575
  # Create target directory and copy agents
2563
2576
  mkdir -p "$target_dir"
2564
2577
 
2565
- # If clean mode, remove stale files first (preserving user directories)
2578
+ # If clean mode, remove stale files first (preserving user and plugin directories)
2566
2579
  if [[ "$CLEAN_MODE" == "true" ]]; then
2567
- print_info "Clean mode: removing stale files from $target_dir (preserving custom/, draft/)"
2568
- # Preserve user-managed directories before clean
2580
+ # Build list of directories to preserve: custom, draft, plus plugin namespaces
2569
2581
  local -a preserved_dirs=("custom" "draft")
2570
- local tmp_preserve="$(mktemp -d)"
2582
+ if [[ ${#plugin_namespaces[@]} -gt 0 ]]; then
2583
+ for pns in "${plugin_namespaces[@]}"; do
2584
+ preserved_dirs+=("$pns")
2585
+ done
2586
+ fi
2587
+ print_info "Clean mode: removing stale files from $target_dir (preserving ${preserved_dirs[*]})"
2588
+ local tmp_preserve
2589
+ tmp_preserve="$(mktemp -d)"
2590
+ trap 'rm -rf "${tmp_preserve:-}"' RETURN
2571
2591
  if [[ -z "$tmp_preserve" || ! -d "$tmp_preserve" ]]; then
2572
- print_error "Failed to create temp dir for preserving custom/draft agents"
2592
+ print_error "Failed to create temp dir for preserving agents"
2573
2593
  return 1
2574
2594
  fi
2575
2595
  local preserve_failed=false
@@ -2581,7 +2601,7 @@ deploy_aidevops_agents() {
2581
2601
  fi
2582
2602
  done
2583
2603
  if [[ "$preserve_failed" == "true" ]]; then
2584
- print_error "Failed to preserve custom/draft agents; aborting clean"
2604
+ print_error "Failed to preserve user/plugin agents; aborting clean"
2585
2605
  rm -rf "$tmp_preserve"
2586
2606
  return 1
2587
2607
  fi
@@ -2599,15 +2619,28 @@ deploy_aidevops_agents() {
2599
2619
  # - loop-state/ (local runtime state, not agents)
2600
2620
  # - custom/ (user's private agents, never overwritten)
2601
2621
  # - draft/ (user's experimental agents, never overwritten)
2622
+ # - plugin namespace directories (managed separately)
2602
2623
  # Use rsync for selective exclusion
2603
2624
  local deploy_ok=false
2604
2625
  if command -v rsync &>/dev/null; then
2605
- if rsync -a --exclude='loop-state/' --exclude='custom/' --exclude='draft/' "$source_dir/" "$target_dir/"; then
2626
+ local -a rsync_excludes=("--exclude=loop-state/" "--exclude=custom/" "--exclude=draft/")
2627
+ if [[ ${#plugin_namespaces[@]} -gt 0 ]]; then
2628
+ for pns in "${plugin_namespaces[@]}"; do
2629
+ rsync_excludes+=("--exclude=${pns}/")
2630
+ done
2631
+ fi
2632
+ if rsync -a "${rsync_excludes[@]}" "$source_dir/" "$target_dir/"; then
2606
2633
  deploy_ok=true
2607
2634
  fi
2608
2635
  else
2609
2636
  # Fallback: use tar with exclusions to match rsync behavior
2610
- if (cd "$source_dir" && tar cf - --exclude='loop-state' --exclude='custom' --exclude='draft' .) | (cd "$target_dir" && tar xf -); then
2637
+ local -a tar_excludes=("--exclude=loop-state" "--exclude=custom" "--exclude=draft")
2638
+ if [[ ${#plugin_namespaces[@]} -gt 0 ]]; then
2639
+ for pns in "${plugin_namespaces[@]}"; do
2640
+ tar_excludes+=("--exclude=$pns")
2641
+ done
2642
+ fi
2643
+ if (cd "$source_dir" && tar cf - "${tar_excludes[@]}" .) | (cd "$target_dir" && tar xf -); then
2611
2644
  deploy_ok=true
2612
2645
  fi
2613
2646
  fi
@@ -2647,6 +2680,7 @@ deploy_aidevops_agents() {
2647
2680
  # (awk -v doesn't handle multi-line content with special chars well)
2648
2681
  local tmp_file
2649
2682
  tmp_file=$(mktemp)
2683
+ trap 'rm -f "${tmp_file:-}"' RETURN
2650
2684
  local in_placeholder=false
2651
2685
  while IFS= read -r line || [[ -n "$line" ]]; do
2652
2686
  if [[ "$line" == *"OPENCODE-PLAN-REMINDER-INJECT-START"* ]]; then
@@ -2675,6 +2709,9 @@ deploy_aidevops_agents() {
2675
2709
  print_warning "Mailbox migration had issues (non-critical, old files preserved)"
2676
2710
  fi
2677
2711
  fi
2712
+
2713
+ # Deploy enabled plugins from plugins.json
2714
+ deploy_plugins "$target_dir" "$plugins_file"
2678
2715
  else
2679
2716
  print_error "Failed to deploy agents"
2680
2717
  return 1
@@ -2683,6 +2720,93 @@ deploy_aidevops_agents() {
2683
2720
  return 0
2684
2721
  }
2685
2722
 
2723
+ # Deploy enabled plugins from plugins.json
2724
+ # Arguments: target_dir, plugins_file
2725
+ deploy_plugins() {
2726
+ local target_dir="$1"
2727
+ local plugins_file="$2"
2728
+
2729
+ # Skip if no plugins.json or no jq
2730
+ if [[ ! -f "$plugins_file" ]]; then
2731
+ return 0
2732
+ fi
2733
+ if ! command -v jq &>/dev/null; then
2734
+ print_warning "jq not found; skipping plugin deployment"
2735
+ return 0
2736
+ fi
2737
+
2738
+ local plugin_count
2739
+ plugin_count=$(jq '.plugins | length' "$plugins_file" 2>/dev/null || echo "0")
2740
+ if [[ "$plugin_count" -eq 0 ]]; then
2741
+ return 0
2742
+ fi
2743
+
2744
+ local enabled_count
2745
+ enabled_count=$(jq '[.plugins[] | select(.enabled != false)] | length' "$plugins_file" 2>/dev/null || echo "0")
2746
+ if [[ "$enabled_count" -eq 0 ]]; then
2747
+ print_info "No enabled plugins to deploy ($plugin_count configured, all disabled)"
2748
+ return 0
2749
+ fi
2750
+
2751
+ # Remove directories for disabled plugins (cleanup)
2752
+ local disabled_ns
2753
+ while IFS= read -r disabled_ns; do
2754
+ [[ -z "$disabled_ns" ]] && continue
2755
+ if [[ -d "$target_dir/$disabled_ns" ]]; then
2756
+ rm -rf "$target_dir/$disabled_ns"
2757
+ print_info " Removed disabled plugin directory: $disabled_ns"
2758
+ fi
2759
+ done < <(jq -r '.plugins[] | select(.enabled == false) | .namespace // empty' "$plugins_file" 2>/dev/null)
2760
+
2761
+ print_info "Deploying $enabled_count plugin(s)..."
2762
+
2763
+ local deployed=0
2764
+ local failed=0
2765
+ local skipped=0
2766
+
2767
+ # Process each enabled plugin
2768
+ while IFS=$'\t' read -r pname prepo pns pbranch; do
2769
+ [[ -z "$pname" ]] && continue
2770
+ pbranch="${pbranch:-main}"
2771
+ local clone_dir="$target_dir/$pns"
2772
+
2773
+ if [[ -d "$clone_dir" ]]; then
2774
+ # Plugin directory exists — skip re-clone during setup
2775
+ # Users can force update via: aidevops plugin update [name]
2776
+ skipped=$((skipped + 1))
2777
+ continue
2778
+ fi
2779
+
2780
+ # Clone plugin repo
2781
+ print_info " Installing plugin '$pname' ($prepo)..."
2782
+ if git clone --branch "$pbranch" --depth 1 "$prepo" "$clone_dir" 2>/dev/null; then
2783
+ # Remove .git directory (tracked via plugins.json, not nested git)
2784
+ rm -rf "$clone_dir/.git"
2785
+ # Set permissions on any scripts
2786
+ if [[ -d "$clone_dir/scripts" ]]; then
2787
+ chmod +x "$clone_dir/scripts/"*.sh 2>/dev/null || true
2788
+ fi
2789
+ deployed=$((deployed + 1))
2790
+ else
2791
+ print_warning " Failed to install plugin '$pname' (network or auth issue)"
2792
+ failed=$((failed + 1))
2793
+ fi
2794
+ done < <(jq -r '.plugins[] | select(.enabled != false) | [.name, .repo, .namespace, (.branch // "main")] | @tsv' "$plugins_file" 2>/dev/null)
2795
+
2796
+ # Summary
2797
+ if [[ "$deployed" -gt 0 ]]; then
2798
+ print_success "Deployed $deployed plugin(s)"
2799
+ fi
2800
+ if [[ "$skipped" -gt 0 ]]; then
2801
+ print_info "$skipped plugin(s) already deployed (use 'aidevops plugin update' to refresh)"
2802
+ fi
2803
+ if [[ "$failed" -gt 0 ]]; then
2804
+ print_warning "$failed plugin(s) failed to deploy (non-blocking)"
2805
+ fi
2806
+
2807
+ return 0
2808
+ }
2809
+
2686
2810
  # Generate Agent Skills SKILL.md files for cross-tool compatibility
2687
2811
  generate_agent_skills() {
2688
2812
  print_info "Generating Agent Skills SKILL.md files..."
@@ -2987,6 +3111,7 @@ inject_agents_reference() {
2987
3111
  # Prepend reference to existing file
2988
3112
  local temp_file
2989
3113
  temp_file=$(mktemp)
3114
+ trap 'rm -f "${temp_file:-}"' RETURN
2990
3115
  echo "$reference_line" > "$temp_file"
2991
3116
  echo "" >> "$temp_file"
2992
3117
  cat "$agents_file" >> "$temp_file"
@@ -3316,6 +3441,7 @@ update_mcp_paths_in_opencode() {
3316
3441
 
3317
3442
  local tmp_config
3318
3443
  tmp_config=$(mktemp)
3444
+ trap 'rm -f "${tmp_config:-}"' RETURN
3319
3445
  cp "$opencode_config" "$tmp_config"
3320
3446
 
3321
3447
  local updated=0
@@ -4139,7 +4265,7 @@ setup_safety_hooks() {
4139
4265
  return 0
4140
4266
  }
4141
4267
 
4142
- # Setup OpenCode Plugins (Antigravity OAuth)
4268
+ # Setup OpenCode Plugins
4143
4269
  # Helper function to add/update a single plugin in OpenCode config
4144
4270
  add_opencode_plugin() {
4145
4271
  local plugin_name="$1"
@@ -4159,6 +4285,7 @@ add_opencode_plugin() {
4159
4285
  # Update existing plugin to latest version
4160
4286
  local temp_file
4161
4287
  temp_file=$(mktemp)
4288
+ trap 'rm -f "${temp_file:-}"' RETURN
4162
4289
  jq --arg old "$plugin_name" --arg new "$plugin_spec" \
4163
4290
  '.plugin = [.plugin[] | if startswith($old) then $new else . end]' \
4164
4291
  "$opencode_config" > "$temp_file" && mv "$temp_file" "$opencode_config"
@@ -4167,6 +4294,7 @@ add_opencode_plugin() {
4167
4294
  # Add plugin to existing array
4168
4295
  local temp_file
4169
4296
  temp_file=$(mktemp)
4297
+ trap 'rm -f "${temp_file:-}"' RETURN
4170
4298
  jq --arg p "$plugin_spec" '.plugin += [$p]' "$opencode_config" > "$temp_file" && mv "$temp_file" "$opencode_config"
4171
4299
  print_success "Added $plugin_name plugin to OpenCode config"
4172
4300
  fi
@@ -4174,6 +4302,7 @@ add_opencode_plugin() {
4174
4302
  # Create plugin array with the plugin
4175
4303
  local temp_file
4176
4304
  temp_file=$(mktemp)
4305
+ trap 'rm -f "${temp_file:-}"' RETURN
4177
4306
  jq --arg p "$plugin_spec" '. + {plugin: [$p]}' "$opencode_config" > "$temp_file" && mv "$temp_file" "$opencode_config"
4178
4307
  print_success "Created plugin array with $plugin_name"
4179
4308
  fi
@@ -4210,21 +4339,11 @@ setup_opencode_plugins() {
4210
4339
  print_success "aidevops compaction plugin registered (preserves context across compaction)"
4211
4340
  fi
4212
4341
 
4213
- # Setup Antigravity OAuth plugin (Google OAuth)
4214
- print_info "Setting up Antigravity OAuth plugin..."
4215
- add_opencode_plugin "opencode-antigravity-auth" "opencode-antigravity-auth@latest" "$opencode_config"
4216
-
4217
- print_info "Antigravity OAuth plugin enables Google OAuth for OpenCode"
4218
- print_info "Models available: gemini-3-pro-high, claude-opus-4-5-thinking, etc."
4219
- print_info "See: https://github.com/NoeFabris/opencode-antigravity-auth"
4220
- echo ""
4221
-
4222
4342
  # Note: opencode-anthropic-auth is built into OpenCode v1.1.36+
4223
4343
  # Adding it as an external plugin causes TypeError due to double-loading.
4224
4344
  # Removed in v2.90.0 - see PR #230.
4225
4345
 
4226
4346
  print_info "After setup, authenticate with: opencode auth login"
4227
- print_info " • For Google OAuth: Select 'Google' → 'OAuth with Google (Antigravity)'"
4228
4347
  print_info " • For Claude OAuth: Select 'Anthropic' → 'Claude Pro/Max' (built-in)"
4229
4348
 
4230
4349
  return 0
@@ -4316,6 +4435,7 @@ setup_google_analytics_mcp() {
4316
4435
  if [[ "$enable_mcp" == "true" ]]; then
4317
4436
  local tmp_config
4318
4437
  tmp_config=$(mktemp)
4438
+ trap 'rm -f "${tmp_config:-}"' RETURN
4319
4439
  if jq --arg creds "$creds_path" --arg proj "$project_id" \
4320
4440
  '.mcp["google-analytics-mcp"].environment.GOOGLE_APPLICATION_CREDENTIALS = $creds |
4321
4441
  .mcp["google-analytics-mcp"].environment.GOOGLE_PROJECT_ID = $proj |
@@ -4336,6 +4456,7 @@ setup_google_analytics_mcp() {
4336
4456
  # Add google-analytics-mcp to opencode.json
4337
4457
  local tmp_config
4338
4458
  tmp_config=$(mktemp)
4459
+ trap 'rm -f "${tmp_config:-}"' RETURN
4339
4460
 
4340
4461
  if jq --arg creds "$creds_path" --arg proj "$project_id" --argjson enabled "$enable_mcp" \
4341
4462
  '.mcp["google-analytics-mcp"] = {
@@ -4448,7 +4569,7 @@ setup_quickfile_mcp() {
4448
4569
 
4449
4570
  local tmp_config
4450
4571
  tmp_config=$(mktemp)
4451
- trap 'rm -f "$tmp_config"' RETURN
4572
+ trap 'rm -f "${tmp_config:-}"' RETURN
4452
4573
 
4453
4574
  if jq --arg np "$node_path" --arg dp "$quickfile_dir/dist/index.js" \
4454
4575
  '.mcp.quickfile = {"type": "local", "command": [$np, $dp], "enabled": true}' \