aidevops 3.5.892 → 3.8.2

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.
@@ -1,6 +1,9 @@
1
1
  #!/usr/bin/env bash
2
- # Agent deployment functions: deploy_aidevops_agents, deploy_ai_templates, inject_agents_reference, safety-hooks, beads
2
+ # SPDX-License-Identifier: MIT
3
+ # SPDX-FileCopyrightText: 2025-2026 Marcus Quinn
4
+ # Agent deployment functions: deploy_aidevops_agents, deploy_ai_templates, inject_agents_reference
3
5
  # Part of aidevops setup.sh modularization (t316.3)
6
+ # Split from original agent-deploy.sh (t1940): runtime conversion → agent-runtime.sh, beads/hooks → tool-beads.sh
4
7
 
5
8
  # Shell safety baseline
6
9
  set -Eeuo pipefail
@@ -9,11 +12,13 @@ IFS=$'\n\t'
9
12
  trap 'rc=$?; echo "[ERROR] ${BASH_SOURCE[0]}:${LINENO} exit $rc" >&2' ERR
10
13
  shopt -s inherit_errexit 2>/dev/null || true
11
14
 
15
+ # Shared reference line injected into all runtime agent configs
16
+ readonly _AIDEVOPS_REFERENCE_LINE='Add ~/.aidevops/agents/AGENTS.md to context for AI DevOps capabilities.'
17
+
12
18
  deploy_ai_templates() {
13
19
  print_info "Deploying AI assistant templates..."
14
20
 
15
21
  if [[ -f "templates/deploy-templates.sh" ]]; then
16
- print_info "Running template deployment script..."
17
22
  if bash templates/deploy-templates.sh; then
18
23
  print_success "AI assistant templates deployed successfully"
19
24
  else
@@ -26,7 +31,7 @@ deploy_ai_templates() {
26
31
  }
27
32
 
28
33
  extract_opencode_prompts() {
29
- local extract_script=".agents/scripts/extract-opencode-prompts.sh"
34
+ local extract_script="${INSTALL_DIR:-.}/.agents/scripts/extract-opencode-prompts.sh"
30
35
  if [[ -f "$extract_script" ]]; then
31
36
  if bash "$extract_script"; then
32
37
  print_success "OpenCode prompts extracted"
@@ -38,7 +43,7 @@ extract_opencode_prompts() {
38
43
  }
39
44
 
40
45
  check_opencode_prompt_drift() {
41
- local drift_script=".agents/scripts/opencode-prompt-drift-check.sh"
46
+ local drift_script="${INSTALL_DIR:-.}/.agents/scripts/opencode-prompt-drift-check.sh"
42
47
  if [[ -f "$drift_script" ]]; then
43
48
  local output exit_code=0
44
49
  # 2>/dev/null is intentional: --quiet mode suppresses expected output; all exit
@@ -60,44 +65,6 @@ check_opencode_prompt_drift() {
60
65
  return 0
61
66
  }
62
67
 
63
- # _deploy_agents_clean_mode target_dir [preserved_dirs...]
64
- # Removes stale files from target_dir while preserving listed subdirectories.
65
- _deploy_agents_clean_mode() {
66
- local target_dir="$1"
67
- shift
68
- local -a preserved_dirs=("$@")
69
-
70
- print_info "Clean mode: removing stale files from $target_dir (preserving ${preserved_dirs[*]})"
71
- local tmp_preserve
72
- tmp_preserve="$(mktemp -d)"
73
- trap 'rm -rf "${tmp_preserve:-}"' RETURN
74
- if [[ -z "$tmp_preserve" || ! -d "$tmp_preserve" ]]; then
75
- print_error "Failed to create temp dir for preserving agents"
76
- return 1
77
- fi
78
- local preserve_failed=false
79
- for pdir in "${preserved_dirs[@]}"; do
80
- if [[ -d "$target_dir/$pdir" ]]; then
81
- if ! cp -R "$target_dir/$pdir" "$tmp_preserve/$pdir"; then
82
- preserve_failed=true
83
- fi
84
- fi
85
- done
86
- if [[ "$preserve_failed" == "true" ]]; then
87
- print_error "Failed to preserve user/plugin agents; aborting clean"
88
- rm -rf "$tmp_preserve"
89
- return 1
90
- fi
91
- rm -rf "${target_dir:?}"/*
92
- for pdir in "${preserved_dirs[@]}"; do
93
- if [[ -d "$tmp_preserve/$pdir" ]]; then
94
- cp -R "$tmp_preserve/$pdir" "$target_dir/$pdir"
95
- fi
96
- done
97
- rm -rf "$tmp_preserve"
98
- return 0
99
- }
100
-
101
68
  # _deploy_agents_copy source_dir target_dir [plugin_namespaces...]
102
69
  # Copies agent files using rsync (preferred) or tar fallback.
103
70
  # Returns 0 on success, 1 on failure.
@@ -165,33 +132,158 @@ _inject_plan_reminder() {
165
132
  return 0
166
133
  }
167
134
 
168
- # _deploy_agents_post_copy target_dir repo_dir source_dir plugins_file
169
- # Runs all post-copy steps: permissions, VERSION, advisories, plan-reminder,
170
- # mailbox migration, stale-file migration, and plugin deployment.
171
- _deploy_agents_post_copy() {
135
+ # _resolve_model_tiers_in_frontmatter target_dir
136
+ # Resolves tier shorthands (sonnet, haiku, opus, etc.) in YAML frontmatter
137
+ # `model:` fields to fully-qualified provider/model IDs using model-routing-table.json.
138
+ # This enables runtimes like OpenCode that consume `model:` literally (GH#18043).
139
+ # Source .md files keep tier names; deployed files get FQIDs.
140
+ # Only processes files with YAML frontmatter (--- delimited) where `model:` contains
141
+ # a bare tier name (no `/`). Already-qualified IDs are left unchanged.
142
+ _resolve_model_tiers_in_frontmatter() {
143
+ local target_dir="$1"
144
+
145
+ # Locate routing tables: merge custom overrides with default
146
+ local default_table="$target_dir/configs/model-routing-table.json"
147
+ local custom_table="$target_dir/custom/configs/model-routing-table.json"
148
+
149
+ if [[ ! -f "$default_table" ]]; then
150
+ print_warning "model-routing-table.json not found — skipping frontmatter model resolution"
151
+ return 0
152
+ fi
153
+
154
+ # Requires jq for JSON parsing
155
+ if ! command -v jq &>/dev/null; then
156
+ print_warning "jq not available — skipping frontmatter model resolution"
157
+ return 0
158
+ fi
159
+
160
+ # Build a sed script file from the routing table(s) in ONE jq call.
161
+ # Custom table overrides specific tiers; default fills in the rest.
162
+ # Each line is a separate sed command for cross-platform compatibility
163
+ # (macOS sed doesn't support ; as command separator inside {}).
164
+ # Generates replacements for both plain and commented forms:
165
+ # model: sonnet → model: anthropic/claude-sonnet-4-6
166
+ # model: sonnet # ... → model: anthropic/claude-sonnet-4-6 # ...
167
+ local sed_file
168
+ sed_file=$(mktemp "${TMPDIR:-/tmp}/model-resolve-XXXXXX.sed")
169
+ if [[ -f "$custom_table" ]]; then
170
+ # Merge: custom tiers override default tiers (jq * operator)
171
+ jq -r -s '
172
+ (.[0].tiers // {}) * (.[1].tiers // {}) |
173
+ to_entries[] |
174
+ "s|^model: \(.key)$|model: \(.value.models[0])|",
175
+ "s|^model: \(.key) #|model: \(.value.models[0]) #|"
176
+ ' "$default_table" "$custom_table" >"$sed_file" 2>/dev/null
177
+ else
178
+ jq -r '
179
+ .tiers | to_entries[] |
180
+ "s|^model: \(.key)$|model: \(.value.models[0])|",
181
+ "s|^model: \(.key) #|model: \(.value.models[0]) #|"
182
+ ' "$default_table" >"$sed_file" 2>/dev/null
183
+ fi
184
+
185
+ if [[ ! -s "$sed_file" ]]; then
186
+ rm -f "$sed_file"
187
+ print_warning "No tiers found in routing table — skipping frontmatter model resolution"
188
+ return 0
189
+ fi
190
+
191
+ # Build a grep pattern to find only files with bare tier names.
192
+ # This avoids scanning all 3000+ .md files — only ~60 need changes.
193
+ # Extract tier names from the sed file (each line has the tier name after "model: ")
194
+ local tier_names
195
+ if [[ -f "$custom_table" ]]; then
196
+ tier_names=$(jq -r -s '(.[0].tiers // {}) * (.[1].tiers // {}) | keys[]' "$default_table" "$custom_table" 2>/dev/null | paste -sd'|' -)
197
+ else
198
+ tier_names=$(jq -r '.tiers | keys[]' "$default_table" 2>/dev/null | paste -sd'|' -)
199
+ fi
200
+ if [[ -z "$tier_names" ]]; then
201
+ rm -f "$sed_file"
202
+ return 0
203
+ fi
204
+
205
+ # Find candidate files: have a model: line with a bare tier name (no /)
206
+ # grep -rl is fast — scans content without loading full files
207
+ # The || true prevents set -e from exiting when grep finds no matches
208
+ local md_file
209
+ { grep -rlE "^model: ($tier_names)(\$| #)" "$target_dir" --include='*.md' 2>/dev/null || true; } | while IFS= read -r md_file; do
210
+ [[ -n "$md_file" ]] || continue
211
+ # Verify the match is in YAML frontmatter (first line is ---)
212
+ local first_line
213
+ first_line=$(head -1 "$md_file" 2>/dev/null) || continue
214
+ [[ "$first_line" == "---" ]] || continue
215
+
216
+ # Apply sed replacements from the script file (macOS sed -i '' vs GNU sed -i)
217
+ sed -i '' -f "$sed_file" "$md_file" 2>/dev/null ||
218
+ sed -i -f "$sed_file" "$md_file" 2>/dev/null || true
219
+ done
220
+
221
+ # Count remaining unresolved files
222
+ local remaining
223
+ remaining=$({ grep -rlE "^model: ($tier_names)(\$| #)" "$target_dir" --include='*.md' 2>/dev/null || true; } | wc -l | tr -d ' ')
224
+ if [[ "$remaining" -eq 0 ]]; then
225
+ print_success "Resolved model tiers to FQIDs in deployed agent files (via model-routing-table.json)"
226
+ else
227
+ print_warning "Some model tiers could not be resolved ($remaining files remaining)"
228
+ fi
229
+
230
+ rm -f "$sed_file"
231
+ return 0
232
+ }
233
+
234
+ # _set_script_permissions_and_report target_dir
235
+ # Sets execute permissions on all deployed scripts and reports deployed counts.
236
+ _set_script_permissions_and_report() {
172
237
  local target_dir="$1"
173
- local repo_dir="$2"
174
- local source_dir="$3"
175
- local plugins_file="$4"
176
238
 
177
- # Set permissions on scripts (top-level and modularised subdirectories)
178
239
  chmod +x "$target_dir/scripts/"*.sh 2>/dev/null || true
179
240
  find "$target_dir/scripts" -mindepth 2 -name "*.sh" -exec chmod +x {} + 2>/dev/null || true
180
241
 
181
- # Count what was deployed
182
242
  local agent_count script_count
183
243
  agent_count=$(find "$target_dir" -name "*.md" -type f | wc -l | tr -d ' ')
184
244
  script_count=$(find "$target_dir/scripts" -name "*.sh" -type f 2>/dev/null | wc -l | tr -d ' ')
185
245
  print_info "Deployed $agent_count agent files and $script_count scripts"
246
+ return 0
247
+ }
186
248
 
187
- # Symlink OpenCode's node_modules into the plugin directory (t1551)
249
+ # _install_opencode_plugin_deps target_dir
250
+ # Installs node_modules for the opencode-aidevops plugin.
251
+ # GH#17829: @bufbuild/protobuf was missing; GH#17891: only symlink on first run.
252
+ # Uses --omit=peer to skip the 630MB opencode-ai peer dep (the host app).
253
+ _install_opencode_plugin_deps() {
254
+ local target_dir="$1"
188
255
  local oc_node_modules="$HOME/.config/opencode/node_modules"
189
256
  local plugin_dir="$target_dir/plugins/opencode-aidevops"
190
- if [[ -d "$oc_node_modules" && -d "$plugin_dir" ]]; then
191
- ln -sf "$oc_node_modules" "$plugin_dir/node_modules" 2>/dev/null || true
257
+
258
+ if [[ ! -d "$plugin_dir" ]]; then
259
+ return 0
260
+ fi
261
+
262
+ # Only symlink if node_modules doesn't exist at all (first run)
263
+ if [[ ! -e "$plugin_dir/node_modules" ]]; then
264
+ if [[ -d "$oc_node_modules" ]]; then
265
+ ln -sf "$oc_node_modules" "$plugin_dir/node_modules" 2>/dev/null || true
266
+ fi
267
+ fi
268
+
269
+ # Verify critical dependency is available; npm install if not
270
+ if [[ ! -d "$plugin_dir/node_modules/@bufbuild/protobuf" ]]; then
271
+ if command -v npm &>/dev/null; then
272
+ # Remove symlink if present so npm creates a local node_modules
273
+ [[ -L "$plugin_dir/node_modules" ]] && rm "$plugin_dir/node_modules"
274
+ npm install --omit=dev --omit=peer --prefix "$plugin_dir" >/dev/null 2>&1 ||
275
+ print_warning "Failed to install plugin dependencies (non-blocking)"
276
+ fi
192
277
  fi
278
+ return 0
279
+ }
280
+
281
+ # _deploy_version_file target_dir repo_dir
282
+ # Copies VERSION file from repo root to the deployed agents directory.
283
+ _deploy_version_file() {
284
+ local target_dir="$1"
285
+ local repo_dir="$2"
193
286
 
194
- # Copy VERSION file from repo root to deployed agents
195
287
  if [[ -f "$repo_dir/VERSION" ]]; then
196
288
  if cp "$repo_dir/VERSION" "$target_dir/VERSION"; then
197
289
  print_info "Copied VERSION file to deployed agents"
@@ -201,30 +293,40 @@ _deploy_agents_post_copy() {
201
293
  else
202
294
  print_warning "VERSION file not found in repo root"
203
295
  fi
296
+ return 0
297
+ }
204
298
 
205
- # Deploy security advisories (shown in session greeting until dismissed)
299
+ # _deploy_security_advisories_files source_dir
300
+ # Copies *.advisory files to ~/.aidevops/advisories/ (shown in session greeting).
301
+ _deploy_security_advisories_files() {
302
+ local source_dir="$1"
206
303
  local advisories_source="$source_dir/advisories"
207
304
  local advisories_target="$HOME/.aidevops/advisories"
208
- if [[ -d "$advisories_source" ]]; then
209
- mkdir -p "$advisories_target"
210
- local adv_count=0
211
- for adv_file in "$advisories_source"/*.advisory; do
212
- [[ -f "$adv_file" ]] || continue
213
- cp "$adv_file" "$advisories_target/"
214
- adv_count=$((adv_count + 1))
215
- done
216
- if [[ "$adv_count" -gt 0 ]]; then
217
- print_info "Deployed $adv_count security advisory/advisories"
218
- fi
219
- fi
220
305
 
221
- # Inject extracted OpenCode plan-reminder into Plan+ if available
222
- _inject_plan_reminder "$target_dir"
306
+ if [[ ! -d "$advisories_source" ]]; then
307
+ return 0
308
+ fi
309
+ mkdir -p "$advisories_target"
310
+ local adv_count=0
311
+ for adv_file in "$advisories_source"/*.advisory; do
312
+ [[ -f "$adv_file" ]] || continue
313
+ cp "$adv_file" "$advisories_target/"
314
+ adv_count=$((adv_count + 1))
315
+ done
316
+ if [[ "$adv_count" -gt 0 ]]; then
317
+ print_info "Deployed $adv_count security advisory/advisories"
318
+ fi
319
+ return 0
320
+ }
223
321
 
224
- # Migrate mailbox from TOON files to SQLite (if old files exist)
322
+ # _migrate_mailbox_if_needed target_dir
323
+ # Migrates mailbox from legacy TOON files to SQLite if old files exist.
324
+ _migrate_mailbox_if_needed() {
325
+ local target_dir="$1"
225
326
  local aidevops_workspace_dir="${AIDEVOPS_WORKSPACE_DIR:-$HOME/.aidevops/.agent-workspace}"
226
327
  local mail_dir="${AIDEVOPS_MAIL_DIR:-${aidevops_workspace_dir}/mail}"
227
328
  local mail_script="$target_dir/scripts/mail-helper.sh"
329
+
228
330
  if [[ -x "$mail_script" ]] && find "$mail_dir" -name "*.toon" 2>/dev/null | grep -q .; then
229
331
  if "$mail_script" migrate; then
230
332
  print_success "Mailbox migration complete"
@@ -232,20 +334,89 @@ _deploy_agents_post_copy() {
232
334
  print_warning "Mailbox migration had issues (non-critical, old files preserved)"
233
335
  fi
234
336
  fi
337
+ return 0
338
+ }
235
339
 
236
- # Migration: wavespeed.md moved from services/ai-generation/ to tools/video/ (v2.111+)
340
+ # _migrate_wavespeed_md target_dir
341
+ # Removes stale wavespeed.md from deprecated services/ai-generation/ path (v2.111+).
342
+ _migrate_wavespeed_md() {
343
+ local target_dir="$1"
237
344
  local old_wavespeed="$target_dir/services/ai-generation/wavespeed.md"
345
+
238
346
  if [[ -f "$old_wavespeed" ]]; then
239
347
  rm -f "$old_wavespeed"
240
348
  rmdir "$target_dir/services/ai-generation" 2>/dev/null || true
241
349
  print_info "Migrated wavespeed.md from services/ai-generation/ to tools/video/"
242
350
  fi
351
+ return 0
352
+ }
353
+
354
+ # _deploy_agents_post_copy target_dir repo_dir source_dir plugins_file
355
+ # Orchestrates all post-copy steps: permissions, VERSION, advisories, plan-reminder,
356
+ # mailbox migration, stale-file migration, model resolution, and plugin deployment.
357
+ _deploy_agents_post_copy() {
358
+ local target_dir="$1"
359
+ local repo_dir="$2"
360
+ local source_dir="$3"
361
+ local plugins_file="$4"
243
362
 
244
- # Deploy enabled plugins from plugins.json
363
+ _set_script_permissions_and_report "$target_dir"
364
+ _install_opencode_plugin_deps "$target_dir"
365
+ _deploy_version_file "$target_dir" "$repo_dir"
366
+ _deploy_security_advisories_files "$source_dir"
367
+ _inject_plan_reminder "$target_dir"
368
+ _migrate_mailbox_if_needed "$target_dir"
369
+ _migrate_wavespeed_md "$target_dir"
370
+ # Source files keep tier names (sonnet, haiku, opus); deployed files get
371
+ # fully-qualified IDs (anthropic/claude-sonnet-4-6) that runtimes like
372
+ # OpenCode can consume directly (GH#18043).
373
+ _resolve_model_tiers_in_frontmatter "$target_dir"
245
374
  deploy_plugins "$target_dir" "$plugins_file"
246
375
  return 0
247
376
  }
248
377
 
378
+ # _warn_deployed_script_drift source_dir target_dir
379
+ # Compares deployed scripts against canonical source and warns if any differ.
380
+ # This catches the case where someone edited ~/.aidevops/agents/scripts/ directly
381
+ # (those edits are overwritten by every deploy). Emits a warning listing drifted
382
+ # files and the canonical source path to edit instead.
383
+ # Non-fatal: always returns 0 so deployment proceeds.
384
+ _warn_deployed_script_drift() {
385
+ local source_dir="$1"
386
+ local target_dir="$2"
387
+ local source_scripts="$source_dir/scripts"
388
+ local target_scripts="$target_dir/scripts"
389
+
390
+ if [[ ! -d "$source_scripts" || ! -d "$target_scripts" ]]; then
391
+ return 0
392
+ fi
393
+ if ! command -v diff &>/dev/null; then
394
+ return 0
395
+ fi
396
+
397
+ local -a drifted=()
398
+ local f bn
399
+ for f in "$target_scripts"/*.sh; do
400
+ [[ -f "$f" ]] || continue
401
+ bn=$(basename "$f")
402
+ local src="$source_scripts/$bn"
403
+ if [[ -f "$src" ]] && ! diff -q "$src" "$f" &>/dev/null; then
404
+ drifted+=("$bn")
405
+ fi
406
+ done
407
+
408
+ if [[ ${#drifted[@]} -gt 0 ]]; then
409
+ print_warning "Deployed scripts differ from canonical source (local edits will be overwritten; backup will be created):"
410
+ for bn in "${drifted[@]}"; do
411
+ print_warning " $target_scripts/$bn"
412
+ print_warning " → canonical: $source_scripts/$bn"
413
+ done
414
+ print_warning "To keep personal scripts: use $target_dir/custom/scripts/"
415
+ print_warning "To fix the canonical source: edit $source_scripts/ and re-run setup.sh"
416
+ fi
417
+ return 0
418
+ }
419
+
249
420
  deploy_aidevops_agents() {
250
421
  print_info "Deploying aidevops agents to ~/.aidevops/agents/..."
251
422
 
@@ -278,47 +449,71 @@ deploy_aidevops_agents() {
278
449
  done < <(jq -r '.plugins[].namespace // empty' "$plugins_file" 2>/dev/null)
279
450
  fi
280
451
 
452
+ # Warn if deployed scripts have been locally modified (GH#17414).
453
+ # These edits will be overwritten — users must edit the canonical source.
454
+ if [[ -d "$target_dir" ]]; then
455
+ _warn_deployed_script_drift "$source_dir" "$target_dir"
456
+ fi
457
+
281
458
  # Create backup if target exists (with rotation)
282
459
  if [[ -d "$target_dir" ]]; then
283
460
  create_backup_with_rotation "$target_dir" "agents"
284
461
  fi
285
462
 
286
- # Create target directory
287
463
  mkdir -p "$target_dir"
288
464
 
289
- # Always clean stale files from previous deployments. Files that were
290
- # moved or deleted in the source repo would otherwise persist in the
291
- # deployed directory indefinitely (rsync without --delete is additive).
292
- # Preserves user directories (custom/, draft/) and plugin namespaces.
293
- local -a preserved_dirs=("custom" "draft")
294
- if [[ ${#plugin_namespaces[@]} -gt 0 ]]; then
295
- for pns in "${plugin_namespaces[@]}"; do
296
- preserved_dirs+=("$pns")
297
- done
298
- fi
299
- _deploy_agents_clean_mode "$target_dir" "${preserved_dirs[@]}" || return 1
300
-
301
- # Copy all agent files and folders, excluding:
302
- # - loop-state/ (local runtime state, not agents)
303
- # - custom/ (user's private agents, never overwritten)
304
- # - draft/ (user's experimental agents, never overwritten)
305
- # - plugin namespace directories (managed separately)
465
+ # Atomic deploy: build a staging directory, then swap it into place.
466
+ # Previously, clean + copy happened in-place, creating a window where
467
+ # scripts were missing. The pulse could dispatch workers mid-deploy,
468
+ # hitting "No such file or directory" errors. Now we:
469
+ # 1. rsync into a staging dir (target_dir.staging)
470
+ # 2. Move preserved dirs (custom/, draft/, plugins) from live to staging
471
+ # 3. mv live .old, mv staging → live (atomic on same filesystem)
472
+ # 4. rm .old
473
+ local staging_dir="${target_dir}.staging"
474
+ local old_dir="${target_dir}.old"
475
+ rm -rf "$staging_dir" "$old_dir"
476
+ mkdir -p "$staging_dir"
477
+
478
+ # Copy source into staging
306
479
  local copy_rc
307
480
  if [[ ${#plugin_namespaces[@]} -gt 0 ]]; then
308
- _deploy_agents_copy "$source_dir" "$target_dir" "${plugin_namespaces[@]}"
481
+ _deploy_agents_copy "$source_dir" "$staging_dir" "${plugin_namespaces[@]}"
309
482
  copy_rc=$?
310
483
  else
311
- _deploy_agents_copy "$source_dir" "$target_dir"
484
+ _deploy_agents_copy "$source_dir" "$staging_dir"
312
485
  copy_rc=$?
313
486
  fi
314
- if [[ "$copy_rc" -eq 0 ]]; then
315
- print_success "Deployed agents to $target_dir"
316
- _deploy_agents_post_copy "$target_dir" "$repo_dir" "$source_dir" "$plugins_file"
317
- else
318
- print_error "Failed to deploy agents"
487
+ if [[ "$copy_rc" -ne 0 ]]; then
488
+ print_error "Failed to deploy agents to staging directory"
489
+ rm -rf "$staging_dir"
319
490
  return 1
320
491
  fi
321
492
 
493
+ # Carry over preserved directories from live target to staging
494
+ local -a preserved_dirs=("custom" "draft")
495
+ if [[ ${#plugin_namespaces[@]} -gt 0 ]]; then
496
+ for pns in "${plugin_namespaces[@]}"; do
497
+ preserved_dirs+=("$pns")
498
+ done
499
+ fi
500
+ for pdir in "${preserved_dirs[@]}"; do
501
+ if [[ -d "$target_dir/$pdir" ]]; then
502
+ # Move user dirs into staging so they survive the swap
503
+ cp -a "$target_dir/$pdir" "$staging_dir/$pdir" 2>/dev/null || true
504
+ fi
505
+ done
506
+
507
+ # Atomic swap: mv is atomic on the same filesystem (POSIX rename())
508
+ if [[ -d "$target_dir" ]]; then
509
+ mv "$target_dir" "$old_dir"
510
+ fi
511
+ mv "$staging_dir" "$target_dir"
512
+ rm -rf "$old_dir"
513
+
514
+ print_success "Deployed agents to $target_dir"
515
+ _deploy_agents_post_copy "$target_dir" "$repo_dir" "$source_dir" "$plugins_file"
516
+
322
517
  return 0
323
518
  }
324
519
 
@@ -349,7 +544,7 @@ inject_agents_reference() {
349
544
  # script is not yet available (e.g., during initial setup before .agents/ deploy).
350
545
  # Will be removed once t1665 migration is complete.
351
546
  _inject_agents_reference_legacy() {
352
- local reference_line='Add ~/.aidevops/agents/AGENTS.md to context for AI DevOps capabilities.'
547
+ local reference_line="$_AIDEVOPS_REFERENCE_LINE"
353
548
 
354
549
  # AI assistant agent directories - these receive AGENTS.md reference
355
550
  local ai_agent_dirs=(
@@ -445,11 +640,9 @@ _deploy_codex_instructions() {
445
640
 
446
641
  mkdir -p "$codex_dir"
447
642
 
448
- local reference_content
449
- reference_content="Add ~/.aidevops/agents/AGENTS.md to context for AI DevOps capabilities."
643
+ local reference_content="$_AIDEVOPS_REFERENCE_LINE"
450
644
 
451
645
  if [[ -f "$instructions_file" ]]; then
452
- # Check if our reference is already present
453
646
  # shellcheck disable=SC2088 # Tilde is a literal grep pattern, not a path
454
647
  if grep -q '~/.aidevops/agents/AGENTS.md' "$instructions_file" 2>/dev/null; then
455
648
  print_info "Codex instructions.md already has aidevops reference"
@@ -484,8 +677,7 @@ _deploy_cursor_agents_reference() {
484
677
 
485
678
  mkdir -p "$rules_dir"
486
679
 
487
- local reference_content
488
- reference_content="Add ~/.aidevops/agents/AGENTS.md to context for AI DevOps capabilities."
680
+ local reference_content="$_AIDEVOPS_REFERENCE_LINE"
489
681
 
490
682
  if [[ -f "$agents_file" ]]; then
491
683
  # shellcheck disable=SC2088 # Tilde is a literal grep pattern, not a path
@@ -514,8 +706,7 @@ _deploy_droid_agents_reference() {
514
706
 
515
707
  mkdir -p "$skills_dir"
516
708
 
517
- local reference_content
518
- reference_content="Add ~/.aidevops/agents/AGENTS.md to context for AI DevOps capabilities."
709
+ local reference_content="$_AIDEVOPS_REFERENCE_LINE"
519
710
 
520
711
  if [[ -f "$agents_file" ]]; then
521
712
  # shellcheck disable=SC2088 # Tilde is a literal grep pattern, not a path
@@ -529,566 +720,3 @@ _deploy_droid_agents_reference() {
529
720
  print_success "Deployed aidevops reference to $agents_file"
530
721
  return 0
531
722
  }
532
-
533
- # =============================================================================
534
- # Deploy aidevops main agents to runtime-native agent directories
535
- # =============================================================================
536
- # Converts aidevops agents from ~/.aidevops/agents/*.md into each runtime's
537
- # native agent format and copies them to the runtime's agent directory.
538
- # Only deploys to installed runtimes that support agent directories.
539
- #
540
- # aidevops frontmatter fields stripped (not understood by target runtimes):
541
- # mode, subagents
542
- #
543
- # Kept/mapped fields (compatible with Claude Code, Cursor, Amp):
544
- # name, description, tools, model, permissionMode, hooks, mcpServers,
545
- # maxTurns, initialPrompt, memory, background, isolation
546
-
547
- # _convert_agent_frontmatter: strips aidevops-only fields from agent markdown.
548
- # Reads from stdin, writes converted content to stdout.
549
- # Tracks whether we're inside an indented block (subagents list) to correctly
550
- # skip its child lines without stripping other indented YAML fields.
551
- _convert_agent_frontmatter() {
552
- local in_frontmatter=false
553
- local frontmatter_started=false
554
- local in_skip_block=false
555
- local line_num=0
556
-
557
- while IFS= read -r line || [[ -n "$line" ]]; do
558
- line_num=$((line_num + 1))
559
- if [[ $line_num -eq 1 && "$line" == "---" ]]; then
560
- in_frontmatter=true
561
- frontmatter_started=true
562
- echo "$line"
563
- continue
564
- fi
565
- if [[ "$frontmatter_started" == "true" && "$in_frontmatter" == "true" && "$line" == "---" ]]; then
566
- in_frontmatter=false
567
- echo "$line"
568
- continue
569
- fi
570
- if [[ "$in_frontmatter" == "true" ]]; then
571
- # Detect top-level keys (no leading whitespace)
572
- case "$line" in
573
- mode:*)
574
- in_skip_block=false
575
- continue
576
- ;;
577
- subagents:*)
578
- in_skip_block=true
579
- continue
580
- ;;
581
- esac
582
- # If inside a skipped block, consume indented continuation lines
583
- if [[ "$in_skip_block" == "true" ]]; then
584
- case "$line" in
585
- [[:space:]]*)
586
- # Indented line under a skipped key — skip it
587
- continue
588
- ;;
589
- *)
590
- # Non-indented line — we've left the skip block
591
- in_skip_block=false
592
- echo "$line"
593
- ;;
594
- esac
595
- else
596
- echo "$line"
597
- fi
598
- else
599
- echo "$line"
600
- fi
601
- done
602
- return 0
603
- }
604
-
605
- # _is_agent_definition: check if a markdown file has agent frontmatter (name: field).
606
- # Returns 0 if the file is an agent definition, 1 otherwise.
607
- _is_agent_definition() {
608
- local file="$1"
609
- # Check first 30 lines for name: in YAML frontmatter (fast path)
610
- head -30 "$file" 2>/dev/null | grep -q '^name:' 2>/dev/null
611
- return $?
612
- }
613
-
614
- # _agent_source_dirs: list directories under agents_source that contain subagents.
615
- # Excludes framework infrastructure directories that are not agent definitions.
616
- # Prints directory paths, one per line.
617
- _agent_source_dirs() {
618
- local agents_source="$1"
619
- local dir
620
- for dir in "$agents_source"/*/; do
621
- [[ -d "$dir" ]] || continue
622
- local dirname
623
- dirname=$(basename "$dir")
624
- # Skip framework infrastructure directories
625
- case "$dirname" in
626
- scripts | reference | prompts | templates | configs | hooks | \
627
- plugins | bundles | loop-state | advisories | aidevops | \
628
- custom | draft | tests | rules)
629
- continue
630
- ;;
631
- *)
632
- echo "$dir"
633
- ;;
634
- esac
635
- done
636
- return 0
637
- }
638
-
639
- # _collect_agent_files: print "abspath|relpath" lines for all deployable agent files
640
- # under agents_source. Excludes AGENTS.md, SKILL.md stubs, and non-agent markdown.
641
- # Output is consumed by deploy_agents_to_runtimes via a process substitution.
642
- _collect_agent_files() {
643
- local agents_source="$1"
644
- local f bn
645
-
646
- # Top-level agents
647
- for f in "$agents_source"/*.md; do
648
- [[ -f "$f" ]] || continue
649
- bn=$(basename "$f")
650
- [[ "$bn" == "AGENTS.md" ]] && continue
651
- if _is_agent_definition "$f"; then
652
- printf '%s|%s\n' "$f" "$bn"
653
- fi
654
- done
655
-
656
- # Subagent directories (recursive)
657
- local subdir
658
- while IFS= read -r subdir; do
659
- while IFS= read -r f; do
660
- [[ -f "$f" ]] || continue
661
- bn=$(basename "$f")
662
- # Skip SKILL.md stubs — they're directory indexes, not real agents
663
- [[ "$bn" == "SKILL.md" ]] && continue
664
- if _is_agent_definition "$f"; then
665
- local relpath="${f#"$agents_source"/}"
666
- printf '%s|%s\n' "$f" "$relpath"
667
- fi
668
- done < <(find "$subdir" -name '*.md' -type f 2>/dev/null)
669
- done < <(_agent_source_dirs "$agents_source")
670
- return 0
671
- }
672
-
673
- # _deploy_agents_to_single_runtime: convert and copy all agent files to one runtime.
674
- # Arguments: runtime_id agent_dir agent_list_file
675
- # agent_list_file contains "abspath|relpath" lines produced by _collect_agent_files.
676
- # Prints the count of successfully deployed agents to stdout.
677
- _deploy_agents_to_single_runtime() {
678
- local runtime_id="$1"
679
- local agent_dir="$2"
680
- local agent_list_file="$3"
681
-
682
- # Only deploy if the runtime is actually installed
683
- local binary config_path config_dir
684
- binary=$(rt_binary "$runtime_id")
685
- config_path=$(rt_config_path "$runtime_id")
686
- config_dir="$(dirname "$config_path" 2>/dev/null)"
687
-
688
- if ! type -P "$binary" >/dev/null 2>&1 && [[ ! -d "$config_dir" ]]; then
689
- echo "0"
690
- return 0
691
- fi
692
-
693
- mkdir -p "$agent_dir"
694
- local agent_count=0
695
- local src rel target target_parent
696
-
697
- while IFS='|' read -r src rel; do
698
- [[ -n "$src" && -n "$rel" ]] || continue
699
- target="$agent_dir/$rel"
700
- target_parent=$(dirname "$target")
701
- [[ -d "$target_parent" ]] || mkdir -p "$target_parent"
702
- if _convert_agent_frontmatter <"$src" >"$target"; then
703
- agent_count=$((agent_count + 1))
704
- fi
705
- done <"$agent_list_file"
706
-
707
- echo "$agent_count"
708
- return 0
709
- }
710
-
711
- # deploy_agents_to_runtimes: main entry point called by setup.sh.
712
- # Iterates all installed runtimes with agent directory support, converts and
713
- # deploys aidevops agents (top-level and subagent directories) to each runtime's
714
- # native agent directory. Only files with name: frontmatter are deployed.
715
- # SKILL.md stubs (directory indexes) are excluded.
716
- deploy_agents_to_runtimes() {
717
- # Source runtime registry if not already loaded
718
- local registry_script="${INSTALL_DIR:-.}/.agents/scripts/runtime-registry.sh"
719
- if [[ -z "${_RUNTIME_REGISTRY_LOADED:-}" ]]; then
720
- if [[ -f "$registry_script" ]]; then
721
- # shellcheck source=/dev/null
722
- source "$registry_script"
723
- else
724
- print_warning "Runtime registry not found — skipping agent deployment to runtimes"
725
- return 0
726
- fi
727
- fi
728
-
729
- local agents_source="${HOME}/.aidevops/agents"
730
- if [[ ! -d "$agents_source" ]]; then
731
- print_warning "No deployed agents found at $agents_source — skipping"
732
- return 0
733
- fi
734
-
735
- # Build the agent file list once (shared across all runtimes) into a temp file.
736
- # Each line: "abspath|relpath"
737
- local agent_list_file
738
- agent_list_file=$(mktemp)
739
- trap 'rm -f "${agent_list_file:-}"' RETURN
740
- _collect_agent_files "$agents_source" >"$agent_list_file"
741
-
742
- local total_agents
743
- total_agents=$(wc -l <"$agent_list_file" | tr -d ' ')
744
- if [[ "$total_agents" -eq 0 ]]; then
745
- print_warning "No agent definitions found in $agents_source"
746
- return 0
747
- fi
748
-
749
- local deployed_count=0
750
- local runtime_count=0
751
-
752
- # Deploy to each installed runtime
753
- local runtime_id agent_dir agent_count display_name
754
- while IFS= read -r runtime_id; do
755
- agent_dir=$(rt_agent_dir "$runtime_id")
756
- [[ -z "$agent_dir" ]] && continue
757
-
758
- agent_count=$(_deploy_agents_to_single_runtime "$runtime_id" "$agent_dir" "$agent_list_file")
759
- # A count of 0 means runtime not installed (skipped) — don't increment runtime_count
760
- if [[ "$agent_count" -gt 0 ]]; then
761
- display_name=$(rt_display_name "$runtime_id")
762
- print_info "Deployed $agent_count agents to $display_name ($agent_dir)"
763
- deployed_count=$((deployed_count + agent_count))
764
- runtime_count=$((runtime_count + 1))
765
- fi
766
- done < <(rt_list_with_agents)
767
-
768
- if [[ $runtime_count -eq 0 ]]; then
769
- print_info "No runtimes with agent directory support detected — skipping"
770
- else
771
- print_success "Deployed $deployed_count agent(s) across $runtime_count runtime(s)"
772
- fi
773
-
774
- return 0
775
- }
776
-
777
- install_beads_binary() {
778
- local os arch tarball_name
779
- os=$(uname -s | tr '[:upper:]' '[:lower:]')
780
- arch=$(uname -m)
781
-
782
- # Map architecture names to Beads release naming convention
783
- case "$arch" in
784
- x86_64 | amd64) arch="amd64" ;;
785
- aarch64 | arm64) arch="arm64" ;;
786
- *)
787
- print_warning "Unsupported architecture for Beads binary download: $arch"
788
- return 1
789
- ;;
790
- esac
791
-
792
- # Get latest version tag from GitHub API
793
- local latest_version
794
- latest_version=$(curl -fsSL "https://api.github.com/repos/steveyegge/beads/releases/latest" 2>/dev/null |
795
- grep '"tag_name"' | head -1 | sed 's/.*"tag_name": *"v\{0,1\}\([^"]*\)".*/\1/')
796
-
797
- if [[ -z "$latest_version" ]]; then
798
- print_warning "Could not determine latest Beads version from GitHub"
799
- return 1
800
- fi
801
-
802
- tarball_name="beads_${latest_version}_${os}_${arch}.tar.gz"
803
- local download_url="https://github.com/steveyegge/beads/releases/download/v${latest_version}/${tarball_name}"
804
-
805
- print_info "Downloading Beads CLI v${latest_version} (${os}/${arch})..."
806
-
807
- local tmp_dir
808
- tmp_dir=$(mktemp -d)
809
- # shellcheck disable=SC2064 # Intentional: $tmp_dir must expand at trap definition time, not execution time
810
- trap "rm -rf '$tmp_dir'" RETURN
811
-
812
- if ! curl -fsSL "$download_url" -o "$tmp_dir/$tarball_name" 2>/dev/null; then
813
- print_warning "Failed to download Beads binary from $download_url"
814
- return 1
815
- fi
816
-
817
- # Extract and install
818
- if ! tar -xzf "$tmp_dir/$tarball_name" -C "$tmp_dir" 2>/dev/null; then
819
- print_warning "Failed to extract Beads binary"
820
- return 1
821
- fi
822
-
823
- # Find the bd binary in the extracted files
824
- local bd_binary
825
- bd_binary=$(find "$tmp_dir" -name "bd" -type f 2>/dev/null | head -1)
826
- if [[ -z "$bd_binary" ]]; then
827
- print_warning "bd binary not found in downloaded archive"
828
- return 1
829
- fi
830
-
831
- # Install to a writable location
832
- local install_dir="/usr/local/bin"
833
- if [[ ! -w "$install_dir" ]]; then
834
- if command -v sudo &>/dev/null; then
835
- sudo install -m 755 "$bd_binary" "$install_dir/bd"
836
- else
837
- # Fallback to user-local bin
838
- install_dir="$HOME/.local/bin"
839
- mkdir -p "$install_dir"
840
- install -m 755 "$bd_binary" "$install_dir/bd"
841
- # Ensure ~/.local/bin is in PATH
842
- if [[ ":$PATH:" != *":$HOME/.local/bin:"* ]]; then
843
- export PATH="$HOME/.local/bin:$PATH"
844
- print_info "Added ~/.local/bin to PATH for this session"
845
- fi
846
- fi
847
- else
848
- install -m 755 "$bd_binary" "$install_dir/bd"
849
- fi
850
-
851
- if command -v bd &>/dev/null; then
852
- print_success "Beads CLI installed via binary download (v${latest_version})"
853
- return 0
854
- else
855
- print_warning "Beads binary installed to $install_dir/bd but not found in PATH"
856
- return 1
857
- fi
858
- }
859
-
860
- install_beads_go() {
861
- if ! command -v go &>/dev/null; then
862
- return 1
863
- fi
864
- if run_with_spinner "Installing Beads via Go" go install github.com/steveyegge/beads/cmd/bd@latest; then
865
- print_info "Ensure \$GOPATH/bin is in your PATH"
866
- return 0
867
- fi
868
- print_warning "Go installation failed"
869
- return 1
870
- }
871
-
872
- setup_beads() {
873
- print_info "Setting up Beads (task graph visualization)..."
874
-
875
- # Check if Beads CLI (bd) is already installed
876
- if command -v bd &>/dev/null; then
877
- local bd_version
878
- bd_version=$(bd --version 2>/dev/null | head -1 || echo "unknown")
879
- print_success "Beads CLI (bd) already installed: $bd_version"
880
- else
881
- # Try to install via Homebrew first (macOS/Linux with Homebrew)
882
- if command -v brew &>/dev/null; then
883
- if run_with_spinner "Installing Beads via Homebrew" brew install steveyegge/beads/bd; then
884
- : # Success message handled by spinner
885
- else
886
- print_warning "Homebrew tap installation failed, trying alternative..."
887
- install_beads_binary || install_beads_go
888
- fi
889
- elif command -v go &>/dev/null; then
890
- if run_with_spinner "Installing Beads via Go" go install github.com/steveyegge/beads/cmd/bd@latest; then
891
- print_info "Ensure \$GOPATH/bin is in your PATH"
892
- else
893
- print_warning "Go installation failed, trying binary download..."
894
- install_beads_binary
895
- fi
896
- else
897
- # No brew, no Go -- try binary download first, then offer Homebrew install
898
- if ! install_beads_binary; then
899
- # Binary download failed -- offer to install Homebrew (Linux only)
900
- if ensure_homebrew; then
901
- # Homebrew now available, retry via tap
902
- if run_with_spinner "Installing Beads via Homebrew" brew install steveyegge/beads/bd; then
903
- : # Success
904
- else
905
- print_warning "Homebrew tap installation failed"
906
- fi
907
- else
908
- print_warning "Beads CLI (bd) not installed"
909
- echo ""
910
- echo " Install options:"
911
- echo " Binary download: https://github.com/steveyegge/beads/releases"
912
- echo " macOS/Linux (Homebrew): brew install steveyegge/beads/bd"
913
- echo " Go: go install github.com/steveyegge/beads/cmd/bd@latest"
914
- echo ""
915
- fi
916
- fi
917
- fi
918
- fi
919
-
920
- print_info "Beads provides task graph visualization for TODO.md and PLANS.md"
921
- print_info "After installation, run: aidevops init beads"
922
-
923
- # Offer to install optional Beads UI tools
924
- setup_beads_ui
925
-
926
- return 0
927
- }
928
-
929
- # _install_bv_tool: install the bv (beads_viewer) TUI tool.
930
- # Returns 0 if installed, 1 if skipped or failed.
931
- _install_bv_tool() {
932
- setup_prompt install_viewer " Install bv (TUI with PageRank, critical path, graph analytics)? [Y/n]: " "Y"
933
- if [[ ! "$install_viewer" =~ ^[Yy]?$ ]]; then
934
- print_info "Install later:"
935
- print_info " Homebrew: brew tap dicklesworthstone/tap && brew install dicklesworthstone/tap/bv"
936
- print_info " Go: go install github.com/Dicklesworthstone/beads_viewer/cmd/bv@latest"
937
- return 1
938
- fi
939
- if command -v brew &>/dev/null; then
940
- if run_with_spinner "Installing bv via Homebrew" brew install dicklesworthstone/tap/bv; then
941
- print_info "Run: bv (in a beads-enabled project)"
942
- return 0
943
- else
944
- print_warning "Homebrew install failed - try manually:"
945
- print_info " brew install dicklesworthstone/tap/bv"
946
- return 1
947
- fi
948
- elif command -v go &>/dev/null; then
949
- if run_with_spinner "Installing bv via Go" go install github.com/Dicklesworthstone/beads_viewer/cmd/bv@latest; then
950
- print_info "Run: bv (in a beads-enabled project)"
951
- return 0
952
- else
953
- print_warning "Go install failed"
954
- return 1
955
- fi
956
- else
957
- # Offer verified install script (download-then-execute, not piped)
958
- setup_prompt use_script " Install bv via install script? [Y/n]: " "Y"
959
- if [[ "$use_script" =~ ^[Yy]?$ ]]; then
960
- if verified_install "bv (beads viewer)" "https://raw.githubusercontent.com/Dicklesworthstone/beads_viewer/main/install.sh"; then
961
- print_info "Run: bv (in a beads-enabled project)"
962
- return 0
963
- else
964
- print_warning "Install script failed - try manually:"
965
- print_info " Homebrew: brew tap dicklesworthstone/tap && brew install dicklesworthstone/tap/bv"
966
- return 1
967
- fi
968
- else
969
- print_info "Install later:"
970
- print_info " Homebrew: brew tap dicklesworthstone/tap && brew install dicklesworthstone/tap/bv"
971
- print_info " Go: go install github.com/Dicklesworthstone/beads_viewer/cmd/bv@latest"
972
- return 1
973
- fi
974
- fi
975
- }
976
-
977
- # _install_beads_node_tools: install beads-ui and bdui via npm.
978
- # Echoes the count of tools installed to stdout.
979
- # All informational output (spinner, status) goes to stderr so that
980
- # command-substitution callers receive only the numeric count.
981
- _install_beads_node_tools() {
982
- local count=0
983
- if ! command -v npm &>/dev/null; then
984
- echo "$count"
985
- return 0
986
- fi
987
- setup_prompt install_web " Install beads-ui (Web dashboard)? [Y/n]: " "Y"
988
- if [[ "$install_web" =~ ^[Yy]?$ ]]; then
989
- if run_with_spinner "Installing beads-ui" npm_global_install beads-ui 1>&2; then
990
- print_info "Run: beads-ui" >&2
991
- count=$((count + 1))
992
- fi
993
- fi
994
- setup_prompt install_bdui " Install bdui (React/Ink TUI)? [Y/n]: " "Y"
995
- if [[ "$install_bdui" =~ ^[Yy]?$ ]]; then
996
- if run_with_spinner "Installing bdui" npm_global_install bdui 1>&2; then
997
- print_info "Run: bdui" >&2
998
- count=$((count + 1))
999
- fi
1000
- fi
1001
- echo "$count"
1002
- return 0
1003
- }
1004
-
1005
- # _install_perles: install the perles BQL query language TUI via cargo.
1006
- # Returns 0 if installed, 1 if skipped or unavailable.
1007
- _install_perles() {
1008
- if ! command -v cargo &>/dev/null; then
1009
- return 1
1010
- fi
1011
- setup_prompt install_perles " Install perles (BQL query language TUI)? [Y/n]: " "Y"
1012
- if [[ ! "$install_perles" =~ ^[Yy]?$ ]]; then
1013
- return 1
1014
- fi
1015
- if run_with_spinner "Installing perles (Rust compile)" cargo install perles; then
1016
- print_info "Run: perles"
1017
- return 0
1018
- fi
1019
- return 1
1020
- }
1021
-
1022
- setup_beads_ui() {
1023
- echo ""
1024
- print_info "Beads UI tools provide enhanced visualization:"
1025
- echo " • bv (Go) - PageRank, critical path, graph analytics TUI"
1026
- echo " • beads-ui (Node.js) - Web dashboard with live updates"
1027
- echo " • bdui (Node.js) - React/Ink terminal UI"
1028
- echo " • perles (Rust) - BQL query language TUI"
1029
- echo ""
1030
-
1031
- setup_prompt install_beads_ui "Install optional Beads UI tools? [Y/n]: " "Y"
1032
-
1033
- if [[ ! "$install_beads_ui" =~ ^[Yy]?$ ]]; then
1034
- print_info "Skipped Beads UI tools (can install later from beads.md docs)"
1035
- return 0
1036
- fi
1037
-
1038
- local installed_count=0
1039
-
1040
- # bv (beads_viewer) - Go TUI installed via Homebrew
1041
- # https://github.com/Dicklesworthstone/beads_viewer
1042
- if _install_bv_tool; then
1043
- installed_count=$((installed_count + 1))
1044
- fi
1045
-
1046
- # beads-ui and bdui (Node.js)
1047
- local node_count
1048
- node_count=$(_install_beads_node_tools)
1049
- installed_count=$((installed_count + node_count))
1050
-
1051
- # perles (Rust)
1052
- if _install_perles; then
1053
- installed_count=$((installed_count + 1))
1054
- fi
1055
-
1056
- if [[ $installed_count -gt 0 ]]; then
1057
- print_success "Installed $installed_count Beads UI tool(s)"
1058
- else
1059
- print_info "No Beads UI tools installed"
1060
- fi
1061
-
1062
- echo ""
1063
- print_info "Beads UI documentation: ~/.aidevops/agents/tools/task-management/beads.md"
1064
-
1065
- return 0
1066
- }
1067
-
1068
- setup_safety_hooks() {
1069
- print_info "Setting up Claude Code safety hooks..."
1070
-
1071
- # Check Python is available
1072
- if ! command -v python3 &>/dev/null; then
1073
- print_warning "Python 3 not found - safety hooks require Python 3"
1074
- return 0
1075
- fi
1076
-
1077
- local helper_script="$HOME/.aidevops/agents/scripts/install-hooks-helper.sh"
1078
- if [[ ! -f "$helper_script" ]]; then
1079
- # Fall back to repo copy (INSTALL_DIR set by setup.sh)
1080
- helper_script="${INSTALL_DIR:-.}/.agents/scripts/install-hooks-helper.sh"
1081
- fi
1082
-
1083
- if [[ ! -f "$helper_script" ]]; then
1084
- print_warning "install-hooks-helper.sh not found - skipping safety hooks"
1085
- return 0
1086
- fi
1087
-
1088
- if bash "$helper_script" install; then
1089
- print_success "Claude Code safety hooks installed"
1090
- else
1091
- print_warning "Safety hook installation encountered issues (non-critical)"
1092
- fi
1093
- return 0
1094
- }