aidevops 2.172.18 → 2.172.19

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/VERSION CHANGED
@@ -1 +1 @@
1
- 2.172.18
1
+ 2.172.19
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.172.18
6
+ # Version: 2.172.19
7
7
 
8
8
  set -euo pipefail
9
9
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "aidevops",
3
- "version": "2.172.18",
3
+ "version": "2.172.19",
4
4
  "description": "AI DevOps Framework - AI-assisted development workflows, code quality, and deployment automation",
5
5
  "type": "module",
6
6
  "bin": {
@@ -58,6 +58,7 @@
58
58
  "bin/",
59
59
  "aidevops.sh",
60
60
  "setup.sh",
61
+ "setup-modules/",
61
62
  "VERSION",
62
63
  "scripts/npm-postinstall.cjs",
63
64
  "templates/"
@@ -26,7 +26,14 @@ try {
26
26
  const log = (msg = '') => {
27
27
  const line = msg + '\n';
28
28
  if (ttyFd !== null) {
29
- fs.writeSync(ttyFd, line);
29
+ try {
30
+ fs.writeSync(ttyFd, line);
31
+ } catch {
32
+ // TTY write failed. Fall back to stderr for this and subsequent calls.
33
+ try { fs.closeSync(ttyFd); } catch { /* best effort to close */ }
34
+ ttyFd = null;
35
+ process.stderr.write(line);
36
+ }
30
37
  } else {
31
38
  process.stderr.write(line);
32
39
  }
@@ -72,5 +79,9 @@ log('');
72
79
 
73
80
  // Clean up
74
81
  if (ttyFd !== null) {
75
- fs.closeSync(ttyFd);
82
+ try {
83
+ fs.closeSync(ttyFd);
84
+ } catch {
85
+ // Ignore errors on close, as there's nothing more to do.
86
+ }
76
87
  }
@@ -0,0 +1,627 @@
1
+ #!/usr/bin/env bash
2
+ # Agent deployment functions: deploy_aidevops_agents, deploy_ai_templates, inject_agents_reference, safety-hooks, beads
3
+ # Part of aidevops setup.sh modularization (t316.3)
4
+
5
+ # Shell safety baseline
6
+ set -Eeuo pipefail
7
+ IFS=$'\n\t'
8
+ # shellcheck disable=SC2154 # rc is assigned by $? in the trap string
9
+ trap 'rc=$?; echo "[ERROR] ${BASH_SOURCE[0]}:${LINENO} exit $rc" >&2' ERR
10
+ shopt -s inherit_errexit 2>/dev/null || true
11
+
12
+ deploy_ai_templates() {
13
+ print_info "Deploying AI assistant templates..."
14
+
15
+ if [[ -f "templates/deploy-templates.sh" ]]; then
16
+ print_info "Running template deployment script..."
17
+ if bash templates/deploy-templates.sh; then
18
+ print_success "AI assistant templates deployed successfully"
19
+ else
20
+ print_warning "Template deployment encountered issues (non-critical)"
21
+ fi
22
+ else
23
+ print_warning "Template deployment script not found - skipping"
24
+ fi
25
+ return 0
26
+ }
27
+
28
+ extract_opencode_prompts() {
29
+ local extract_script=".agents/scripts/extract-opencode-prompts.sh"
30
+ if [[ -f "$extract_script" ]]; then
31
+ if bash "$extract_script"; then
32
+ print_success "OpenCode prompts extracted"
33
+ else
34
+ print_warning "OpenCode prompt extraction encountered issues (non-critical)"
35
+ fi
36
+ fi
37
+ return 0
38
+ }
39
+
40
+ check_opencode_prompt_drift() {
41
+ local drift_script=".agents/scripts/opencode-prompt-drift-check.sh"
42
+ if [[ -f "$drift_script" ]]; then
43
+ local output exit_code=0
44
+ output=$(bash "$drift_script" --quiet 2>/dev/null) || exit_code=$?
45
+ if [[ "$exit_code" -eq 1 && "$output" == PROMPT_DRIFT* ]]; then
46
+ local local_hash upstream_hash
47
+ local_hash=$(echo "$output" | cut -d'|' -f2)
48
+ upstream_hash=$(echo "$output" | cut -d'|' -f3)
49
+ print_warning "OpenCode upstream prompt has changed (${local_hash} → ${upstream_hash})"
50
+ print_info " Review: https://github.com/anomalyco/opencode/compare/${local_hash}...${upstream_hash}"
51
+ print_info " Update .agents/prompts/build.txt if needed"
52
+ elif [[ "$exit_code" -eq 0 ]]; then
53
+ print_success "OpenCode prompt in sync with upstream"
54
+ else
55
+ print_warning "Could not check prompt drift (network issue or missing dependency)"
56
+ fi
57
+ fi
58
+ return 0
59
+ }
60
+
61
+ deploy_aidevops_agents() {
62
+ print_info "Deploying aidevops agents to ~/.aidevops/agents/..."
63
+
64
+ # Use INSTALL_DIR (set by setup.sh) — BASH_SOURCE[0] points to setup-modules/
65
+ # which is not the repo root, so we can't derive .agents/ from it
66
+ local repo_dir="${INSTALL_DIR:?INSTALL_DIR must be set by setup.sh}"
67
+ local source_dir="$repo_dir/.agents"
68
+ local target_dir="$HOME/.aidevops/agents"
69
+ local plugins_file="$HOME/.config/aidevops/plugins.json"
70
+
71
+ # Validate source directory exists (catches curl install from wrong directory)
72
+ if [[ ! -d "$source_dir" ]]; then
73
+ print_error "Agent source directory not found: $source_dir"
74
+ print_info "This usually means setup.sh was run from the wrong directory."
75
+ print_info "The bootstrap should have cloned the repo and re-executed."
76
+ print_info ""
77
+ print_info "To fix manually:"
78
+ print_info " cd ~/Git/aidevops && ./setup.sh"
79
+ return 1
80
+ fi
81
+
82
+ # Collect plugin namespace directories to preserve during deployment
83
+ local -a plugin_namespaces=()
84
+ if [[ -f "$plugins_file" ]] && command -v jq &>/dev/null; then
85
+ local ns
86
+ local safe_ns
87
+ while IFS= read -r ns; do
88
+ if [[ -n "$ns" ]] && safe_ns=$(sanitize_plugin_namespace "$ns" 2>/dev/null); then
89
+ plugin_namespaces+=("$safe_ns")
90
+ fi
91
+ done < <(jq -r '.plugins[].namespace // empty' "$plugins_file" 2>/dev/null)
92
+ fi
93
+
94
+ # Create backup if target exists (with rotation)
95
+ if [[ -d "$target_dir" ]]; then
96
+ create_backup_with_rotation "$target_dir" "agents"
97
+ fi
98
+
99
+ # Create target directory and copy agents
100
+ mkdir -p "$target_dir"
101
+
102
+ # If clean mode, remove stale files first (preserving user and plugin directories)
103
+ if [[ "$CLEAN_MODE" == "true" ]]; then
104
+ # Build list of directories to preserve: custom, draft, plus plugin namespaces
105
+ local -a preserved_dirs=("custom" "draft")
106
+ if [[ ${#plugin_namespaces[@]} -gt 0 ]]; then
107
+ for pns in "${plugin_namespaces[@]}"; do
108
+ preserved_dirs+=("$pns")
109
+ done
110
+ fi
111
+ print_info "Clean mode: removing stale files from $target_dir (preserving ${preserved_dirs[*]})"
112
+ local tmp_preserve
113
+ tmp_preserve="$(mktemp -d)"
114
+ trap 'rm -rf "${tmp_preserve:-}"' RETURN
115
+ if [[ -z "$tmp_preserve" || ! -d "$tmp_preserve" ]]; then
116
+ print_error "Failed to create temp dir for preserving agents"
117
+ return 1
118
+ fi
119
+ local preserve_failed=false
120
+ for pdir in "${preserved_dirs[@]}"; do
121
+ if [[ -d "$target_dir/$pdir" ]]; then
122
+ if ! cp -R "$target_dir/$pdir" "$tmp_preserve/$pdir"; then
123
+ preserve_failed=true
124
+ fi
125
+ fi
126
+ done
127
+ if [[ "$preserve_failed" == "true" ]]; then
128
+ print_error "Failed to preserve user/plugin agents; aborting clean"
129
+ rm -rf "$tmp_preserve"
130
+ return 1
131
+ fi
132
+ rm -rf "${target_dir:?}"/*
133
+ # Restore preserved directories
134
+ for pdir in "${preserved_dirs[@]}"; do
135
+ if [[ -d "$tmp_preserve/$pdir" ]]; then
136
+ cp -R "$tmp_preserve/$pdir" "$target_dir/$pdir"
137
+ fi
138
+ done
139
+ rm -rf "$tmp_preserve"
140
+ fi
141
+
142
+ # Copy all agent files and folders, excluding:
143
+ # - loop-state/ (local runtime state, not agents)
144
+ # - custom/ (user's private agents, never overwritten)
145
+ # - draft/ (user's experimental agents, never overwritten)
146
+ # - plugin namespace directories (managed separately)
147
+ # Use rsync for selective exclusion
148
+ local deploy_ok=false
149
+ if command -v rsync &>/dev/null; then
150
+ local -a rsync_excludes=("--exclude=loop-state/" "--exclude=custom/" "--exclude=draft/")
151
+ if [[ ${#plugin_namespaces[@]} -gt 0 ]]; then
152
+ for pns in "${plugin_namespaces[@]}"; do
153
+ rsync_excludes+=("--exclude=${pns}/")
154
+ done
155
+ fi
156
+ if rsync -a "${rsync_excludes[@]}" "$source_dir/" "$target_dir/"; then
157
+ deploy_ok=true
158
+ fi
159
+ else
160
+ # Fallback: use tar with exclusions to match rsync behavior
161
+ local -a tar_excludes=("--exclude=loop-state" "--exclude=custom" "--exclude=draft")
162
+ if [[ ${#plugin_namespaces[@]} -gt 0 ]]; then
163
+ for pns in "${plugin_namespaces[@]}"; do
164
+ tar_excludes+=("--exclude=$pns")
165
+ done
166
+ fi
167
+ if (cd "$source_dir" && tar cf - "${tar_excludes[@]}" .) | (cd "$target_dir" && tar xf -); then
168
+ deploy_ok=true
169
+ fi
170
+ fi
171
+
172
+ if [[ "$deploy_ok" == "true" ]]; then
173
+ print_success "Deployed agents to $target_dir"
174
+
175
+ # Set permissions on scripts (top-level and modularised subdirectories)
176
+ chmod +x "$target_dir/scripts/"*.sh 2>/dev/null || true
177
+ find "$target_dir/scripts" -mindepth 2 -name "*.sh" -exec chmod +x {} + 2>/dev/null || true
178
+
179
+ # Count what was deployed
180
+ local agent_count
181
+ agent_count=$(find "$target_dir" -name "*.md" -type f | wc -l | tr -d ' ')
182
+ local script_count
183
+ script_count=$(find "$target_dir/scripts" -name "*.sh" -type f 2>/dev/null | wc -l | tr -d ' ')
184
+
185
+ print_info "Deployed $agent_count agent files and $script_count scripts"
186
+
187
+ # Copy VERSION file from repo root to deployed agents
188
+ if [[ -f "$repo_dir/VERSION" ]]; then
189
+ if cp "$repo_dir/VERSION" "$target_dir/VERSION"; then
190
+ print_info "Copied VERSION file to deployed agents"
191
+ else
192
+ print_warning "Failed to copy VERSION file (Plan+ may not read version correctly)"
193
+ fi
194
+ else
195
+ print_warning "VERSION file not found in repo root"
196
+ fi
197
+
198
+ # Inject extracted OpenCode plan-reminder into Plan+ if available
199
+ local plan_reminder="$HOME/.aidevops/cache/opencode-prompts/plan-reminder.txt"
200
+ local plan_plus="$target_dir/plan-plus.md"
201
+ if [[ -f "$plan_reminder" && -f "$plan_plus" ]]; then
202
+ # Check if plan-plus.md has the placeholder marker
203
+ if grep -q "OPENCODE-PLAN-REMINDER-INJECT" "$plan_plus"; then
204
+ # Replace placeholder with extracted content using sed
205
+ # (awk -v doesn't handle multi-line content with special chars well)
206
+ local tmp_file
207
+ tmp_file=$(mktemp)
208
+ trap 'rm -f "${tmp_file:-}"' RETURN
209
+ local in_placeholder=false
210
+ while IFS= read -r line || [[ -n "$line" ]]; do
211
+ if [[ "$line" == *"OPENCODE-PLAN-REMINDER-INJECT-START"* ]]; then
212
+ echo "$line" >>"$tmp_file"
213
+ cat "$plan_reminder" >>"$tmp_file"
214
+ in_placeholder=true
215
+ elif [[ "$line" == *"OPENCODE-PLAN-REMINDER-INJECT-END"* ]]; then
216
+ echo "$line" >>"$tmp_file"
217
+ in_placeholder=false
218
+ elif [[ "$in_placeholder" == false ]]; then
219
+ echo "$line" >>"$tmp_file"
220
+ fi
221
+ done <"$plan_plus"
222
+ mv "$tmp_file" "$plan_plus"
223
+ print_info "Injected OpenCode plan-reminder into Plan+"
224
+ fi
225
+ fi
226
+ # Migrate mailbox from TOON files to SQLite (if old files exist)
227
+ local aidevops_workspace_dir="${AIDEVOPS_WORKSPACE_DIR:-$HOME/.aidevops/.agent-workspace}"
228
+ local mail_dir="${AIDEVOPS_MAIL_DIR:-${aidevops_workspace_dir}/mail}"
229
+ local mail_script="$target_dir/scripts/mail-helper.sh"
230
+ if [[ -x "$mail_script" ]] && find "$mail_dir" -name "*.toon" 2>/dev/null | grep -q .; then
231
+ if "$mail_script" migrate; then
232
+ print_success "Mailbox migration complete"
233
+ else
234
+ print_warning "Mailbox migration had issues (non-critical, old files preserved)"
235
+ fi
236
+ fi
237
+
238
+ # Migration: wavespeed.md moved from services/ai-generation/ to tools/video/ (v2.111+)
239
+ local old_wavespeed="$target_dir/services/ai-generation/wavespeed.md"
240
+ if [[ -f "$old_wavespeed" ]]; then
241
+ rm -f "$old_wavespeed"
242
+ rmdir "$target_dir/services/ai-generation" 2>/dev/null || true
243
+ print_info "Migrated wavespeed.md from services/ai-generation/ to tools/video/"
244
+ fi
245
+
246
+ # Deploy enabled plugins from plugins.json
247
+ deploy_plugins "$target_dir" "$plugins_file"
248
+ else
249
+ print_error "Failed to deploy agents"
250
+ return 1
251
+ fi
252
+
253
+ return 0
254
+ }
255
+
256
+ inject_agents_reference() {
257
+ print_info "Adding aidevops reference to AI assistant configurations..."
258
+
259
+ local reference_line='Add ~/.aidevops/agents/AGENTS.md to context for AI DevOps capabilities.'
260
+
261
+ # AI assistant agent directories - these receive AGENTS.md reference
262
+ # Format: "config_dir:agents_subdir" where agents_subdir is the folder containing agent files
263
+ # Only Claude Code (companion CLI) and .opencode are included here.
264
+ # OpenCode excluded: its agent/ dir treats every .md as a subagent, so AGENTS.md
265
+ # would show as a mode. OpenCode gets the reference via opencode.json instructions
266
+ # field and the config-root AGENTS.md (deployed by deploy_opencode_greeting below).
267
+ local ai_agent_dirs=(
268
+ "$HOME/.claude:commands"
269
+ "$HOME/.opencode:."
270
+ )
271
+
272
+ local updated_count=0
273
+
274
+ for entry in "${ai_agent_dirs[@]}"; do
275
+ local config_dir="${entry%%:*}"
276
+ local agents_subdir="${entry##*:}"
277
+ local agents_dir="$config_dir/$agents_subdir"
278
+ local agents_file="$agents_dir/AGENTS.md"
279
+
280
+ # Only process if the config directory exists (tool is installed)
281
+ if [[ -d "$config_dir" ]]; then
282
+ # Create agents subdirectory if needed
283
+ mkdir -p "$agents_dir"
284
+
285
+ # Check if AGENTS.md exists and has our reference
286
+ if [[ -f "$agents_file" ]]; then
287
+ # Check first line for our reference
288
+ local first_line
289
+ first_line=$(head -1 "$agents_file" 2>/dev/null || echo "")
290
+ if [[ "$first_line" != *"~/.aidevops/agents/AGENTS.md"* ]]; then
291
+ # Prepend reference to existing file
292
+ local temp_file
293
+ temp_file=$(mktemp)
294
+ trap 'rm -f "${temp_file:-}"' RETURN
295
+ echo "$reference_line" >"$temp_file"
296
+ echo "" >>"$temp_file"
297
+ cat "$agents_file" >>"$temp_file"
298
+ mv "$temp_file" "$agents_file"
299
+ print_success "Added reference to $agents_file"
300
+ ((++updated_count))
301
+ else
302
+ print_info "Reference already exists in $agents_file"
303
+ fi
304
+ else
305
+ # Create new file with just the reference
306
+ echo "$reference_line" >"$agents_file"
307
+ print_success "Created $agents_file with aidevops reference"
308
+ ((++updated_count))
309
+ fi
310
+ fi
311
+ done
312
+
313
+ if [[ $updated_count -eq 0 ]]; then
314
+ print_info "No AI assistant configs found to update (tools may not be installed yet)"
315
+ else
316
+ print_success "Updated $updated_count AI assistant configuration(s)"
317
+ fi
318
+
319
+ # Clean up stale AGENTS.md from OpenCode agent dir (was incorrectly showing as subagent)
320
+ rm -f "$HOME/.config/opencode/agent/AGENTS.md"
321
+
322
+ # Deploy OpenCode config-level AGENTS.md from managed template
323
+ # This controls the session greeting (auto-loaded by OpenCode from config root)
324
+ local opencode_config_dir="$HOME/.config/opencode"
325
+ local opencode_config_agents="$opencode_config_dir/AGENTS.md"
326
+ local template_source="$INSTALL_DIR/templates/opencode-config-agents.md"
327
+
328
+ if [[ -d "$opencode_config_dir" && -f "$template_source" ]]; then
329
+ # Backup if file exists and differs from template
330
+ if [[ -f "$opencode_config_agents" ]]; then
331
+ if ! diff -q "$template_source" "$opencode_config_agents" &>/dev/null; then
332
+ create_backup_with_rotation "$opencode_config_agents" "opencode-agents"
333
+ fi
334
+ fi
335
+ if cp "$template_source" "$opencode_config_agents"; then
336
+ print_success "Deployed greeting template to $opencode_config_agents"
337
+ else
338
+ print_error "Failed to deploy greeting template to $opencode_config_agents"
339
+ fi
340
+ fi
341
+
342
+ return 0
343
+ }
344
+
345
+ install_beads_binary() {
346
+ local os arch tarball_name
347
+ os=$(uname -s | tr '[:upper:]' '[:lower:]')
348
+ arch=$(uname -m)
349
+
350
+ # Map architecture names to Beads release naming convention
351
+ case "$arch" in
352
+ x86_64 | amd64) arch="amd64" ;;
353
+ aarch64 | arm64) arch="arm64" ;;
354
+ *)
355
+ print_warning "Unsupported architecture for Beads binary download: $arch"
356
+ return 1
357
+ ;;
358
+ esac
359
+
360
+ # Get latest version tag from GitHub API
361
+ local latest_version
362
+ latest_version=$(curl -fsSL "https://api.github.com/repos/steveyegge/beads/releases/latest" 2>/dev/null |
363
+ grep '"tag_name"' | head -1 | sed 's/.*"tag_name": *"v\{0,1\}\([^"]*\)".*/\1/')
364
+
365
+ if [[ -z "$latest_version" ]]; then
366
+ print_warning "Could not determine latest Beads version from GitHub"
367
+ return 1
368
+ fi
369
+
370
+ tarball_name="beads_${latest_version}_${os}_${arch}.tar.gz"
371
+ local download_url="https://github.com/steveyegge/beads/releases/download/v${latest_version}/${tarball_name}"
372
+
373
+ print_info "Downloading Beads CLI v${latest_version} (${os}/${arch})..."
374
+
375
+ local tmp_dir
376
+ tmp_dir=$(mktemp -d)
377
+ # shellcheck disable=SC2064 # Intentional: $tmp_dir must expand at trap definition time, not execution time
378
+ trap "rm -rf '$tmp_dir'" RETURN
379
+
380
+ if ! curl -fsSL "$download_url" -o "$tmp_dir/$tarball_name" 2>/dev/null; then
381
+ print_warning "Failed to download Beads binary from $download_url"
382
+ return 1
383
+ fi
384
+
385
+ # Extract and install
386
+ if ! tar -xzf "$tmp_dir/$tarball_name" -C "$tmp_dir" 2>/dev/null; then
387
+ print_warning "Failed to extract Beads binary"
388
+ return 1
389
+ fi
390
+
391
+ # Find the bd binary in the extracted files
392
+ local bd_binary
393
+ bd_binary=$(find "$tmp_dir" -name "bd" -type f 2>/dev/null | head -1)
394
+ if [[ -z "$bd_binary" ]]; then
395
+ print_warning "bd binary not found in downloaded archive"
396
+ return 1
397
+ fi
398
+
399
+ # Install to a writable location
400
+ local install_dir="/usr/local/bin"
401
+ if [[ ! -w "$install_dir" ]]; then
402
+ if command -v sudo &>/dev/null; then
403
+ sudo install -m 755 "$bd_binary" "$install_dir/bd"
404
+ else
405
+ # Fallback to user-local bin
406
+ install_dir="$HOME/.local/bin"
407
+ mkdir -p "$install_dir"
408
+ install -m 755 "$bd_binary" "$install_dir/bd"
409
+ # Ensure ~/.local/bin is in PATH
410
+ if [[ ":$PATH:" != *":$HOME/.local/bin:"* ]]; then
411
+ export PATH="$HOME/.local/bin:$PATH"
412
+ print_info "Added ~/.local/bin to PATH for this session"
413
+ fi
414
+ fi
415
+ else
416
+ install -m 755 "$bd_binary" "$install_dir/bd"
417
+ fi
418
+
419
+ if command -v bd &>/dev/null; then
420
+ print_success "Beads CLI installed via binary download (v${latest_version})"
421
+ return 0
422
+ else
423
+ print_warning "Beads binary installed to $install_dir/bd but not found in PATH"
424
+ return 1
425
+ fi
426
+ }
427
+
428
+ install_beads_go() {
429
+ if ! command -v go &>/dev/null; then
430
+ return 1
431
+ fi
432
+ if run_with_spinner "Installing Beads via Go" go install github.com/steveyegge/beads/cmd/bd@latest; then
433
+ print_info "Ensure \$GOPATH/bin is in your PATH"
434
+ return 0
435
+ fi
436
+ print_warning "Go installation failed"
437
+ return 1
438
+ }
439
+
440
+ setup_beads() {
441
+ print_info "Setting up Beads (task graph visualization)..."
442
+
443
+ # Check if Beads CLI (bd) is already installed
444
+ if command -v bd &>/dev/null; then
445
+ local bd_version
446
+ bd_version=$(bd --version 2>/dev/null | head -1 || echo "unknown")
447
+ print_success "Beads CLI (bd) already installed: $bd_version"
448
+ else
449
+ # Try to install via Homebrew first (macOS/Linux with Homebrew)
450
+ if command -v brew &>/dev/null; then
451
+ if run_with_spinner "Installing Beads via Homebrew" brew install steveyegge/beads/bd; then
452
+ : # Success message handled by spinner
453
+ else
454
+ print_warning "Homebrew tap installation failed, trying alternative..."
455
+ install_beads_binary || install_beads_go
456
+ fi
457
+ elif command -v go &>/dev/null; then
458
+ if run_with_spinner "Installing Beads via Go" go install github.com/steveyegge/beads/cmd/bd@latest; then
459
+ print_info "Ensure \$GOPATH/bin is in your PATH"
460
+ else
461
+ print_warning "Go installation failed, trying binary download..."
462
+ install_beads_binary
463
+ fi
464
+ else
465
+ # No brew, no Go -- try binary download first, then offer Homebrew install
466
+ if ! install_beads_binary; then
467
+ # Binary download failed -- offer to install Homebrew (Linux only)
468
+ if ensure_homebrew; then
469
+ # Homebrew now available, retry via tap
470
+ if run_with_spinner "Installing Beads via Homebrew" brew install steveyegge/beads/bd; then
471
+ : # Success
472
+ else
473
+ print_warning "Homebrew tap installation failed"
474
+ fi
475
+ else
476
+ print_warning "Beads CLI (bd) not installed"
477
+ echo ""
478
+ echo " Install options:"
479
+ echo " Binary download: https://github.com/steveyegge/beads/releases"
480
+ echo " macOS/Linux (Homebrew): brew install steveyegge/beads/bd"
481
+ echo " Go: go install github.com/steveyegge/beads/cmd/bd@latest"
482
+ echo ""
483
+ fi
484
+ fi
485
+ fi
486
+ fi
487
+
488
+ print_info "Beads provides task graph visualization for TODO.md and PLANS.md"
489
+ print_info "After installation, run: aidevops init beads"
490
+
491
+ # Offer to install optional Beads UI tools
492
+ setup_beads_ui
493
+
494
+ return 0
495
+ }
496
+
497
+ setup_beads_ui() {
498
+ echo ""
499
+ print_info "Beads UI tools provide enhanced visualization:"
500
+ echo " • bv (Go) - PageRank, critical path, graph analytics TUI"
501
+ echo " • beads-ui (Node.js) - Web dashboard with live updates"
502
+ echo " • bdui (Node.js) - React/Ink terminal UI"
503
+ echo " • perles (Rust) - BQL query language TUI"
504
+ echo ""
505
+
506
+ read -r -p "Install optional Beads UI tools? [Y/n]: " install_beads_ui
507
+
508
+ if [[ ! "$install_beads_ui" =~ ^[Yy]?$ ]]; then
509
+ print_info "Skipped Beads UI tools (can install later from beads.md docs)"
510
+ return 0
511
+ fi
512
+
513
+ local installed_count=0
514
+
515
+ # bv (beads_viewer) - Go TUI installed via Homebrew
516
+ # https://github.com/Dicklesworthstone/beads_viewer
517
+ read -r -p " Install bv (TUI with PageRank, critical path, graph analytics)? [Y/n]: " install_viewer
518
+ if [[ "$install_viewer" =~ ^[Yy]?$ ]]; then
519
+ if command -v brew &>/dev/null; then
520
+ # brew install user/tap/formula auto-taps
521
+ if run_with_spinner "Installing bv via Homebrew" brew install dicklesworthstone/tap/bv; then
522
+ print_info "Run: bv (in a beads-enabled project)"
523
+ ((++installed_count))
524
+ else
525
+ print_warning "Homebrew install failed - try manually:"
526
+ print_info " brew install dicklesworthstone/tap/bv"
527
+ fi
528
+ else
529
+ # No Homebrew - try install script or Go
530
+ print_warning "Homebrew not found"
531
+ if command -v go &>/dev/null; then
532
+ # Go available - use go install
533
+ if run_with_spinner "Installing bv via Go" go install github.com/Dicklesworthstone/beads_viewer/cmd/bv@latest; then
534
+ print_info "Run: bv (in a beads-enabled project)"
535
+ ((++installed_count))
536
+ else
537
+ print_warning "Go install failed"
538
+ fi
539
+ else
540
+ # Offer verified install script (download-then-execute, not piped)
541
+ read -r -p " Install bv via install script? [Y/n]: " use_script
542
+ if [[ "$use_script" =~ ^[Yy]?$ ]]; then
543
+ if verified_install "bv (beads viewer)" "https://raw.githubusercontent.com/Dicklesworthstone/beads_viewer/main/install.sh"; then
544
+ print_info "Run: bv (in a beads-enabled project)"
545
+ ((++installed_count))
546
+ else
547
+ print_warning "Install script failed - try manually:"
548
+ print_info " Homebrew: brew tap dicklesworthstone/tap && brew install dicklesworthstone/tap/bv"
549
+ fi
550
+ else
551
+ print_info "Install later:"
552
+ print_info " Homebrew: brew tap dicklesworthstone/tap && brew install dicklesworthstone/tap/bv"
553
+ print_info " Go: go install github.com/Dicklesworthstone/beads_viewer/cmd/bv@latest"
554
+ fi
555
+ fi
556
+ fi
557
+ fi
558
+
559
+ # beads-ui (Node.js)
560
+ if command -v npm &>/dev/null; then
561
+ read -r -p " Install beads-ui (Web dashboard)? [Y/n]: " install_web
562
+ if [[ "$install_web" =~ ^[Yy]?$ ]]; then
563
+ if run_with_spinner "Installing beads-ui" npm_global_install beads-ui; then
564
+ print_info "Run: beads-ui"
565
+ ((++installed_count))
566
+ fi
567
+ fi
568
+
569
+ read -r -p " Install bdui (React/Ink TUI)? [Y/n]: " install_bdui
570
+ if [[ "$install_bdui" =~ ^[Yy]?$ ]]; then
571
+ if run_with_spinner "Installing bdui" npm_global_install bdui; then
572
+ print_info "Run: bdui"
573
+ ((++installed_count))
574
+ fi
575
+ fi
576
+ fi
577
+
578
+ # perles (Rust)
579
+ if command -v cargo &>/dev/null; then
580
+ read -r -p " Install perles (BQL query language TUI)? [Y/n]: " install_perles
581
+ if [[ "$install_perles" =~ ^[Yy]?$ ]]; then
582
+ if run_with_spinner "Installing perles (Rust compile)" cargo install perles; then
583
+ print_info "Run: perles"
584
+ ((++installed_count))
585
+ fi
586
+ fi
587
+ fi
588
+
589
+ if [[ $installed_count -gt 0 ]]; then
590
+ print_success "Installed $installed_count Beads UI tool(s)"
591
+ else
592
+ print_info "No Beads UI tools installed"
593
+ fi
594
+
595
+ echo ""
596
+ print_info "Beads UI documentation: ~/.aidevops/agents/tools/task-management/beads.md"
597
+
598
+ return 0
599
+ }
600
+
601
+ setup_safety_hooks() {
602
+ print_info "Setting up Claude Code safety hooks..."
603
+
604
+ # Check Python is available
605
+ if ! command -v python3 &>/dev/null; then
606
+ print_warning "Python 3 not found - safety hooks require Python 3"
607
+ return 0
608
+ fi
609
+
610
+ local helper_script="$HOME/.aidevops/agents/scripts/install-hooks-helper.sh"
611
+ if [[ ! -f "$helper_script" ]]; then
612
+ # Fall back to repo copy (INSTALL_DIR set by setup.sh)
613
+ helper_script="${INSTALL_DIR:-.}/.agents/scripts/install-hooks-helper.sh"
614
+ fi
615
+
616
+ if [[ ! -f "$helper_script" ]]; then
617
+ print_warning "install-hooks-helper.sh not found - skipping safety hooks"
618
+ return 0
619
+ fi
620
+
621
+ if bash "$helper_script" install; then
622
+ print_success "Claude Code safety hooks installed"
623
+ else
624
+ print_warning "Safety hook installation encountered issues (non-critical)"
625
+ fi
626
+ return 0
627
+ }