aidevops 3.13.95 → 3.14.1

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,2134 +0,0 @@
1
- #!/usr/bin/env bash
2
- # SPDX-License-Identifier: MIT
3
- # SPDX-FileCopyrightText: 2025-2026 Marcus Quinn
4
- # Tool installation functions: git-clis, fd, ripgrep, shellcheck, shfmt, rosetta, worktrunk, minisim, recommended-tools, nodejs, python, orbstack
5
- # Part of aidevops setup.sh modularization (t316.3)
6
-
7
- # Shell safety baseline
8
- set -Eeuo pipefail
9
- IFS=$'\n\t'
10
- # shellcheck disable=SC2154 # rc is assigned by $? in the trap string
11
- trap 'rc=$?; echo "[ERROR] ${BASH_SOURCE[0]}:${LINENO} exit $rc" >&2' ERR
12
- shopt -s inherit_errexit 2>/dev/null || true
13
-
14
- setup_git_clis() {
15
- print_info "Setting up Git CLI tools..."
16
-
17
- local cli_tools=()
18
- local missing_packages=()
19
- local missing_names=()
20
-
21
- # Check for GitHub CLI
22
- if ! command -v gh >/dev/null 2>&1; then
23
- missing_packages+=("gh")
24
- missing_names+=("GitHub CLI")
25
- else
26
- cli_tools+=("GitHub CLI")
27
- fi
28
-
29
- # Check for GitLab CLI
30
- if ! command -v glab >/dev/null 2>&1; then
31
- missing_packages+=("glab")
32
- missing_names+=("GitLab CLI")
33
- else
34
- cli_tools+=("GitLab CLI")
35
- fi
36
-
37
- # Report found tools
38
- if [[ ${#cli_tools[@]} -gt 0 ]]; then
39
- print_success "Found Git CLI tools: ${cli_tools[*]}"
40
- fi
41
-
42
- # Offer to install missing tools
43
- if [[ ${#missing_packages[@]} -gt 0 ]]; then
44
- print_warning "Missing Git CLI tools: ${missing_names[*]}"
45
- echo " These provide enhanced Git platform integration (repos, PRs, issues)"
46
-
47
- local pkg_manager
48
- pkg_manager=$(detect_package_manager)
49
-
50
- if [[ "$pkg_manager" != "unknown" ]]; then
51
- echo ""
52
- setup_prompt install_git_clis "Install Git CLI tools (${missing_packages[*]}) using $pkg_manager? [Y/n]: " "Y"
53
-
54
- # shellcheck disable=SC2154 # set indirectly by setup_prompt via read
55
- if [[ "$install_git_clis" =~ ^[Yy]?$ ]]; then
56
- print_info "Installing ${missing_packages[*]}..."
57
- if install_packages "$pkg_manager" "${missing_packages[@]}"; then
58
- print_success "Git CLI tools installed"
59
- echo ""
60
- echo "📋 Next steps - authenticate each CLI:"
61
- for pkg in "${missing_packages[@]}"; do
62
- case "$pkg" in
63
- gh) echo " • gh auth login -s workflow (workflow scope required for CI PRs)" ;;
64
- glab) echo " • glab auth login" ;;
65
- esac
66
- done
67
- else
68
- print_warning "Failed to install some Git CLI tools (non-critical)"
69
- fi
70
- else
71
- print_info "Skipped Git CLI tools installation"
72
- echo ""
73
- echo "📋 Manual installation:"
74
- echo " macOS: brew install ${missing_packages[*]}"
75
- echo " Ubuntu: sudo apt install ${missing_packages[*]}"
76
- echo " Fedora: sudo dnf install ${missing_packages[*]}"
77
- fi
78
- else
79
- echo ""
80
- echo "📋 Manual installation:"
81
- echo " macOS: brew install ${missing_packages[*]}"
82
- echo " Ubuntu: sudo apt install ${missing_packages[*]}"
83
- echo " Fedora: sudo dnf install ${missing_packages[*]}"
84
- fi
85
- else
86
- print_success "All Git CLI tools installed and ready!"
87
- fi
88
-
89
- # Check for Gitea CLI separately (not in standard package managers)
90
- if ! command -v tea >/dev/null 2>&1; then
91
- print_info "Gitea CLI (tea) not found - install manually if needed:"
92
- echo " go install code.gitea.io/tea/cmd/tea@latest"
93
- echo " Or download from: https://dl.gitea.io/tea/"
94
- else
95
- print_success "Gitea CLI (tea) found"
96
- fi
97
-
98
- return 0
99
- }
100
-
101
- _print_file_discovery_manual_install() {
102
- echo ""
103
- echo " Manual installation:"
104
- echo " macOS: brew install fd ripgrep ripgrep-all"
105
- echo " Ubuntu/Debian: sudo apt install fd-find ripgrep # rga: cargo install ripgrep_all"
106
- echo " Fedora: sudo dnf install fd-find ripgrep # rga: cargo install ripgrep_all"
107
- echo " Arch: sudo pacman -S fd ripgrep ripgrep-all"
108
- return 0
109
- }
110
-
111
- # Add fd=fdfind alias to shell rc files on Debian/Ubuntu after apt install.
112
- _add_fd_alias_debian() {
113
- local rc_files=("$HOME/.bashrc" "$HOME/.zshrc")
114
- local added_to=""
115
- local rc_file
116
-
117
- for rc_file in "${rc_files[@]}"; do
118
- [[ ! -f "$rc_file" ]] && continue
119
-
120
- if ! grep -q 'alias fd="fdfind"' "$rc_file" 2>/dev/null; then
121
- if { echo '' >>"$rc_file" &&
122
- echo '# fd-find alias for Debian/Ubuntu (added by aidevops)' >>"$rc_file" &&
123
- echo 'alias fd="fdfind"' >>"$rc_file"; }; then
124
- added_to="${added_to:+$added_to, }$rc_file"
125
- fi
126
- fi
127
- done
128
-
129
- if [[ -n "$added_to" ]]; then
130
- print_success "Added alias fd=fdfind to: $added_to"
131
- echo " Restart your shell to activate"
132
- else
133
- print_success "fd alias already configured"
134
- fi
135
- return 0
136
- }
137
-
138
- # Resolve apt package names (fd→fd-find on Debian/Ubuntu) and install.
139
- _install_file_discovery_packages() {
140
- local pkg_manager="$1"
141
- shift
142
- local missing_packages=("$@")
143
-
144
- print_info "Installing ${missing_packages[*]}..."
145
-
146
- local actual_packages=()
147
- local pkg
148
- for pkg in "${missing_packages[@]}"; do
149
- case "$pkg_manager" in
150
- apt)
151
- # Debian/Ubuntu uses fd-find instead of fd
152
- if [[ "$pkg" == "fd" ]]; then
153
- actual_packages+=("fd-find")
154
- else
155
- actual_packages+=("$pkg")
156
- fi
157
- ;;
158
- *)
159
- actual_packages+=("$pkg")
160
- ;;
161
- esac
162
- done
163
-
164
- if install_packages "$pkg_manager" "${actual_packages[@]}"; then
165
- print_success "File discovery tools installed"
166
- # On Debian/Ubuntu, fd is installed as fdfind — create alias in shell rc files
167
- if [[ "$pkg_manager" == "apt" ]] && command -v fdfind >/dev/null 2>&1 && ! command -v fd >/dev/null 2>&1; then
168
- _add_fd_alias_debian
169
- fi
170
- else
171
- print_warning "Failed to install some file discovery tools (non-critical)"
172
- fi
173
- return 0
174
- }
175
-
176
- setup_file_discovery_tools() {
177
- print_info "Setting up file discovery tools..."
178
-
179
- local missing_tools=()
180
- local missing_packages=()
181
- local missing_names=()
182
-
183
- local fd_version
184
- if command -v fd >/dev/null 2>&1; then
185
- fd_version=$(fd --version 2>/dev/null | head -1 || echo "unknown")
186
- print_success "fd found: $fd_version"
187
- elif command -v fdfind >/dev/null 2>&1; then
188
- fd_version=$(fdfind --version 2>/dev/null | head -1 || echo "unknown")
189
- print_success "fd found (as fdfind): $fd_version"
190
- print_warning "Note: 'fd' alias not active in current shell. Restart shell or run: alias fd=fdfind"
191
- else
192
- missing_tools+=("fd")
193
- missing_packages+=("fd")
194
- missing_names+=("fd (fast file finder)")
195
- fi
196
-
197
- # Check for ripgrep
198
- if ! command -v rg >/dev/null 2>&1; then
199
- missing_tools+=("rg")
200
- missing_packages+=("ripgrep")
201
- missing_names+=("ripgrep (fast content search)")
202
- else
203
- local rg_version
204
- rg_version=$(rg --version 2>/dev/null | head -1 || echo "unknown")
205
- print_success "ripgrep found: $rg_version"
206
- fi
207
-
208
- # Check for ripgrep-all (searches inside PDFs, DOCX, SQLite, archives)
209
- if ! command -v rga >/dev/null 2>&1; then
210
- missing_tools+=("rga")
211
- missing_packages+=("ripgrep-all")
212
- missing_names+=("ripgrep-all (search inside PDFs/docs/archives)")
213
- else
214
- local rga_version
215
- rga_version=$(rga --version 2>/dev/null | head -1 || echo "unknown")
216
- print_success "ripgrep-all found: $rga_version"
217
- fi
218
-
219
- # Offer to install missing tools
220
- if [[ ${#missing_tools[@]} -gt 0 ]]; then
221
- print_warning "Missing file discovery tools: ${missing_names[*]}"
222
- echo ""
223
- echo " These tools provide 10x faster file discovery than built-in glob:"
224
- echo " fd - Fast alternative to 'find', respects .gitignore"
225
- echo " ripgrep - Fast alternative to 'grep', respects .gitignore"
226
- echo " ripgrep-all - Extends ripgrep to search inside PDFs, DOCX, SQLite, archives"
227
- echo ""
228
- echo " AI agents use these for efficient codebase navigation."
229
- echo ""
230
-
231
- local pkg_manager
232
- pkg_manager=$(detect_package_manager)
233
-
234
- if [[ "$pkg_manager" != "unknown" ]]; then
235
- setup_prompt install_fd_tools "Install file discovery tools (${missing_packages[*]}) using $pkg_manager? [Y/n]: " "Y"
236
-
237
- # shellcheck disable=SC2154 # set indirectly by setup_prompt via read
238
- if [[ "$install_fd_tools" =~ ^[Yy]?$ ]]; then
239
- _install_file_discovery_packages "$pkg_manager" "${missing_packages[@]}"
240
- else
241
- print_info "Skipped file discovery tools installation"
242
- _print_file_discovery_manual_install
243
- fi
244
- else
245
- _print_file_discovery_manual_install
246
- fi
247
- else
248
- print_success "All file discovery tools installed!"
249
- fi
250
-
251
- return 0
252
- }
253
-
254
- setup_rtk() {
255
- # rtk — CLI proxy that reduces LLM token consumption by 60-90% (t1430)
256
- # Optional optimization: compresses git/gh/test outputs before they reach LLM context
257
- # Single Rust binary, zero dependencies, <10ms overhead
258
- # https://github.com/rtk-ai/rtk
259
-
260
- # Pin to a tagged release for stability and auditability (Gemini review feedback).
261
- # Update the tag when upstream-watch detects a new release.
262
- local rtk_installer_url="https://raw.githubusercontent.com/rtk-ai/rtk/v0.28.2/install.sh"
263
-
264
- if command -v rtk >/dev/null 2>&1; then
265
- local rtk_version
266
- rtk_version=$(rtk --version 2>/dev/null | grep -oE '[0-9]+\.[0-9]+\.[0-9]+' | head -1 || echo "unknown")
267
- print_success "rtk found: v$rtk_version (token optimization proxy)"
268
- # Fall through to ensure config is applied (telemetry, tee)
269
- else
270
- print_info "rtk (Rust Token Killer) reduces LLM token usage by 60-90% on CLI commands"
271
- echo " Compresses git, gh, test runner, and linter outputs before they reach the AI context."
272
- echo " Single binary, zero dependencies, <10ms overhead."
273
- echo ""
274
-
275
- setup_prompt install_rtk "Install rtk for token-optimized CLI output? [y/N]: " "n"
276
-
277
- # shellcheck disable=SC2154 # set indirectly by setup_prompt via read
278
- if [[ "$install_rtk" =~ ^[Yy]$ ]]; then
279
- VERIFIED_INSTALL_SHELL="sh"
280
- if command -v brew >/dev/null 2>&1; then
281
- if run_with_spinner "Installing rtk via Homebrew" brew install rtk; then
282
- print_success "rtk installed via Homebrew"
283
- else
284
- print_warning "Homebrew install failed, trying curl installer..."
285
- if verified_install "rtk" "$rtk_installer_url"; then
286
- print_success "rtk installed to ~/.local/bin/rtk"
287
- else
288
- print_warning "rtk installation failed (non-critical, optional tool)"
289
- fi
290
- fi
291
- else
292
- # Linux or macOS without brew — use verified_install for secure execution
293
- if verified_install "rtk" "$rtk_installer_url"; then
294
- print_success "rtk installed to ~/.local/bin/rtk"
295
- else
296
- print_warning "rtk installation failed (non-critical, optional tool)"
297
- echo " Manual install: https://github.com/rtk-ai/rtk#installation"
298
- fi
299
- fi
300
- else
301
- print_info "Skipped rtk installation (optional)"
302
- echo " Manual install: brew install rtk OR curl -fsSL $rtk_installer_url | sh"
303
- fi
304
- fi
305
-
306
- # Configure rtk (telemetry off, tee for failure capture) — only if binary is present
307
- if command -v rtk >/dev/null 2>&1; then
308
- local rtk_config_dir="${XDG_CONFIG_HOME:-$HOME/.config}/rtk"
309
- if [[ ! -f "$rtk_config_dir/config.toml" ]]; then
310
- mkdir -p "$rtk_config_dir"
311
- cat >"$rtk_config_dir/config.toml" <<-'RTKEOF'
312
- # rtk configuration (created by aidevops setup.sh)
313
- # https://github.com/rtk-ai/rtk
314
-
315
- [telemetry]
316
- enabled = false
317
-
318
- [tee]
319
- enabled = true
320
- mode = "failures"
321
- max_files = 20
322
- RTKEOF
323
- print_success "rtk config created (telemetry disabled)"
324
- fi
325
- fi
326
-
327
- return 0
328
- }
329
-
330
- setup_shell_linting_tools() {
331
- print_info "Setting up shell linting tools..."
332
-
333
- local missing_tools=()
334
- local pkg_manager
335
- pkg_manager=$(detect_package_manager)
336
-
337
- # Check shellcheck
338
- if command -v shellcheck >/dev/null 2>&1; then
339
- local sc_version sc_rosetta=false
340
- sc_version=$(shellcheck --version 2>/dev/null | grep 'version:' | awk '{print $2}' || echo "unknown")
341
- # Rosetta detection (macOS Apple Silicon only, requires `file` command)
342
- if [[ "$PLATFORM_MACOS" == "true" ]] && [[ "$PLATFORM_ARM64" == "true" ]] && command -v file >/dev/null 2>&1; then
343
- local sc_file_output
344
- sc_file_output=$(file "$(command -v shellcheck)" 2>/dev/null || echo "")
345
- if [[ "$sc_file_output" == *"x86_64"* ]] && [[ "$sc_file_output" != *"arm64"* ]]; then
346
- sc_rosetta=true
347
- fi
348
- fi
349
- if [[ "$sc_rosetta" == "true" ]]; then
350
- print_warning "shellcheck found but running under Rosetta (x86_64)"
351
- print_info " Run 'rosetta-audit-helper.sh migrate' to fix"
352
- else
353
- print_success "shellcheck found ($sc_version)"
354
- fi
355
- else
356
- missing_tools+=("shellcheck")
357
- fi
358
-
359
- # Check shfmt
360
- if command -v shfmt >/dev/null 2>&1; then
361
- print_success "shfmt found ($(shfmt --version 2>/dev/null))"
362
- else
363
- missing_tools+=("shfmt")
364
- fi
365
-
366
- if [[ ${#missing_tools[@]} -gt 0 ]]; then
367
- print_warning "Missing shell linting tools: ${missing_tools[*]}"
368
- echo " shellcheck - static analysis for shell scripts"
369
- echo " shfmt - shell script formatter (fast syntax checks)"
370
-
371
- if [[ "$pkg_manager" != "unknown" ]]; then
372
- local install_linters
373
- setup_prompt install_linters "Install missing shell linting tools using $pkg_manager? [Y/n]: " "Y"
374
-
375
- if [[ "$install_linters" =~ ^[Yy]?$ ]]; then
376
- if install_packages "$pkg_manager" "${missing_tools[@]}"; then
377
- print_success "Shell linting tools installed"
378
- else
379
- print_warning "Failed to install some shell linting tools"
380
- fi
381
- else
382
- print_info "Skipped shell linting tools"
383
- fi
384
- else
385
- echo " Install manually:"
386
- echo " macOS: brew install ${missing_tools[*]}"
387
- echo " Linux: apt install ${missing_tools[*]}"
388
- fi
389
- fi
390
-
391
- return 0
392
- }
393
-
394
- setup_setsid_advisory() {
395
- # setsid is required to detach pulse workers into their own process group
396
- # (t2757, GH#20561, GH#21102). Without it, workers inherit pulse's PGID and
397
- # are killed by any PG-scoped signal (launchd unload, restart chain).
398
- #
399
- # Linux: setsid ships with util-linux (present on all mainstream distros).
400
- # macOS: available from macOS 12+ at /usr/bin/setsid. Older macOS or systems
401
- # where /usr/bin/setsid is absent need util-linux via Homebrew.
402
- # util-linux is keg-only on Homebrew — binary is not linked into PATH
403
- # automatically, so we create a symlink after install.
404
- if command -v setsid >/dev/null 2>&1; then
405
- local setsid_path
406
- setsid_path="$(command -v setsid)"
407
- print_success "setsid found at $setsid_path (worker process-group isolation enabled)"
408
- return 0
409
- fi
410
-
411
- # setsid missing — on macOS with Homebrew, auto-install util-linux and
412
- # symlink setsid into PATH (GH#21102 / t2926). On Linux and macOS without
413
- # Homebrew, emit an actionable error with install instructions.
414
- if [[ "$(uname)" == "Darwin" ]]; then
415
- if command -v brew >/dev/null 2>&1; then
416
- print_info "setsid not found — installing util-linux for worker PGID isolation (GH#21102)"
417
- if brew install util-linux 2>&1 | tail -3; then
418
- # util-linux is keg-only: binary lives under the keg, not in /opt/homebrew/bin.
419
- # Symlink setsid into a standard PATH directory so 'command -v setsid' works.
420
- local brew_prefix
421
- brew_prefix="$(brew --prefix 2>/dev/null || echo "")"
422
- local keg_setsid="${brew_prefix}/opt/util-linux/bin/setsid"
423
- local link_target="${brew_prefix}/bin/setsid"
424
- if [[ -x "$keg_setsid" && ! -e "$link_target" ]]; then
425
- ln -s "$keg_setsid" "$link_target" && \
426
- print_success "Symlinked setsid: $keg_setsid → $link_target"
427
- fi
428
- # Verify setsid is now reachable
429
- if command -v setsid >/dev/null 2>&1; then
430
- print_success "setsid installed at $(command -v setsid) (worker PGID isolation enabled)"
431
- else
432
- print_error "util-linux installed but setsid still not in PATH — check brew --prefix"
433
- fi
434
- else
435
- print_error "brew install util-linux failed — workers will share pulse PGID until resolved"
436
- echo " Manual fix: brew install util-linux"
437
- fi
438
- else
439
- print_error "setsid not found — worker isolation broken; install util-linux"
440
- echo " Impact: every pulse restart sends SIGHUP to workers in its PGID,"
441
- echo " killing in-flight workers before they can finish (GH#21102)"
442
- echo " Fix: install Homebrew, then run: brew install util-linux"
443
- echo " Or upgrade to macOS 12+ where /usr/bin/setsid ships by default"
444
- fi
445
- else
446
- # Linux: setsid should be present on all mainstream distros via util-linux.
447
- # If it is missing, emit an error rather than a warning — workers will be
448
- # killed on every pulse cycle restart without it.
449
- print_error "setsid not found — worker isolation broken; install util-linux"
450
- echo " Impact: every pulse restart sends SIGHUP to workers in its PGID,"
451
- echo " killing in-flight workers before they can finish (GH#21102)"
452
- echo " Fix: sudo apt install util-linux # Debian/Ubuntu"
453
- echo " sudo dnf install util-linux # Fedora/RHEL"
454
- echo " sudo pacman -S util-linux # Arch"
455
- echo " sudo apk add util-linux # Alpine"
456
- fi
457
- echo ""
458
-
459
- return 0
460
- }
461
-
462
- setup_shellcheck_wrapper() {
463
- # Replace the real shellcheck binary with our wrapper script to prevent
464
- # --external-sources from causing exponential memory growth (GH#2915).
465
- # This intercepts ALL callers including compiled binaries (e.g., OpenCode)
466
- # that invoke shellcheck by absolute path rather than via PATH.
467
-
468
- local wrapper_src="${INSTALL_DIR:-.}/.agents/scripts/shellcheck-wrapper.sh"
469
- if [[ ! -f "$wrapper_src" ]]; then
470
- print_info "shellcheck-wrapper.sh not found — skipping binary replacement"
471
- return 0
472
- fi
473
-
474
- # Find the real shellcheck binary
475
- local sc_path
476
- sc_path="$(command -v shellcheck 2>/dev/null || true)"
477
- if [[ -z "$sc_path" ]]; then
478
- print_info "shellcheck not installed — wrapper not needed yet"
479
- return 0
480
- fi
481
-
482
- # Resolve symlinks to get the actual binary path
483
- local sc_resolved
484
- sc_resolved="$(realpath "$sc_path" 2>/dev/null || readlink -f "$sc_path" 2>/dev/null || echo "$sc_path")"
485
-
486
- # Check if the binary is already our wrapper (idempotent)
487
- if head -5 "$sc_resolved" 2>/dev/null | grep -q "shellcheck-wrapper" 2>/dev/null; then
488
- # Already replaced — check that .real exists
489
- local real_path="${sc_resolved}.real"
490
- if [[ ! -x "$real_path" ]]; then
491
- print_warning "shellcheck wrapper installed but .real binary missing at $real_path"
492
- print_info "Reinstall shellcheck (brew reinstall shellcheck) then re-run setup"
493
- return 0
494
- fi
495
-
496
- # Check if the installed wrapper is outdated vs the source
497
- if ! diff -q "$wrapper_src" "$sc_resolved" >/dev/null 2>&1; then
498
- print_info "Updating shellcheck wrapper at $sc_resolved (source is newer)"
499
- if cp "$wrapper_src" "$sc_resolved" 2>/dev/null || sudo cp "$wrapper_src" "$sc_resolved" 2>/dev/null; then
500
- chmod +x "$sc_resolved" 2>/dev/null || sudo chmod +x "$sc_resolved" 2>/dev/null || true
501
- print_success "shellcheck wrapper updated at $sc_resolved"
502
- else
503
- print_warning "Cannot update wrapper — insufficient permissions"
504
- fi
505
- else
506
- print_success "shellcheck wrapper already installed at $sc_resolved"
507
- fi
508
- return 0
509
- fi
510
-
511
- # The binary at sc_resolved is the real shellcheck — replace it
512
- local real_dest="${sc_resolved}.real"
513
-
514
- print_info "Installing shellcheck wrapper at $sc_resolved"
515
- print_info " Real binary will be moved to $real_dest"
516
-
517
- # Move real binary to .real suffix
518
- if ! mv "$sc_resolved" "$real_dest" 2>/dev/null; then
519
- # May need sudo (e.g., /usr/local/bin on some systems)
520
- if ! sudo mv "$sc_resolved" "$real_dest" 2>/dev/null; then
521
- print_warning "Cannot move shellcheck binary — insufficient permissions"
522
- print_info "Run manually: sudo mv '$sc_resolved' '$real_dest'"
523
- return 0
524
- fi
525
- fi
526
-
527
- # Copy wrapper to the original path
528
- if ! cp "$wrapper_src" "$sc_resolved" 2>/dev/null; then
529
- if ! sudo cp "$wrapper_src" "$sc_resolved" 2>/dev/null; then
530
- # Rollback
531
- mv "$real_dest" "$sc_resolved" 2>/dev/null || sudo mv "$real_dest" "$sc_resolved" 2>/dev/null || true
532
- print_warning "Cannot install wrapper — insufficient permissions"
533
- return 0
534
- fi
535
- fi
536
-
537
- # Ensure wrapper is executable
538
- chmod +x "$sc_resolved" 2>/dev/null || sudo chmod +x "$sc_resolved" 2>/dev/null || true
539
-
540
- print_success "shellcheck wrapper installed — --external-sources will be stripped"
541
- print_info " Real binary: $real_dest"
542
- print_info " Wrapper: $sc_resolved"
543
-
544
- return 0
545
- }
546
-
547
- setup_qlty_cli() {
548
- print_info "Setting up Qlty CLI (multi-linter code quality)..."
549
-
550
- local qlty_bin="${HOME}/.qlty/bin/qlty"
551
-
552
- # Check if already installed
553
- if [[ -x "$qlty_bin" ]]; then
554
- local qlty_version
555
- qlty_version=$("$qlty_bin" --version 2>/dev/null | head -1 || echo "unknown")
556
- print_success "Qlty CLI already installed: $qlty_version"
557
- return 0
558
- fi
559
-
560
- # Also check PATH in case it's installed elsewhere
561
- if command -v qlty >/dev/null 2>&1; then
562
- local qlty_version
563
- qlty_version=$(qlty --version 2>/dev/null | head -1 || echo "unknown")
564
- print_success "Qlty CLI found in PATH: $qlty_version"
565
- return 0
566
- fi
567
-
568
- print_info "Qlty provides universal code quality analysis for 40+ languages"
569
- echo " - Runs 70+ static analysis tools (ShellCheck, ESLint, etc.)"
570
- echo " - Detects code smells and maintainability issues"
571
- echo " - Used by the daily code quality sweep (pulse-wrapper.sh)"
572
- echo ""
573
-
574
- local install_qlty
575
- setup_prompt install_qlty "Install Qlty CLI? [Y/n]: " "Y"
576
-
577
- if [[ "$install_qlty" =~ ^[Yy]?$ ]]; then
578
- if command -v curl >/dev/null 2>&1; then
579
- if verified_install "Qlty CLI" "https://qlty.sh"; then
580
- # Verify installation
581
- if [[ -x "$qlty_bin" ]]; then
582
- local qlty_version
583
- qlty_version=$("$qlty_bin" --version 2>/dev/null | head -1 || echo "unknown")
584
- print_success "Qlty CLI installed: $qlty_version"
585
- print_info "Ensure ~/.qlty/bin is in your PATH"
586
- print_info "Documentation: ~/.aidevops/agents/tools/code-review/qlty.md"
587
- elif command -v qlty >/dev/null 2>&1; then
588
- print_success "Qlty CLI installed: $(qlty --version 2>/dev/null | head -1)"
589
- else
590
- print_warning "Qlty CLI install script ran but binary not found at $qlty_bin"
591
- print_info "Try restarting your shell or check ~/.qlty/bin/"
592
- fi
593
- else
594
- print_warning "Qlty CLI installation failed"
595
- print_info "Install manually: curl -fsSL https://qlty.sh | bash"
596
- fi
597
- else
598
- print_warning "curl not found — cannot install Qlty CLI"
599
- print_info "Install manually: curl -fsSL https://qlty.sh | bash"
600
- fi
601
- else
602
- print_info "Skipped Qlty CLI installation"
603
- print_info "Install later: curl -fsSL https://qlty.sh | bash"
604
- fi
605
-
606
- return 0
607
- }
608
-
609
- setup_rosetta_audit() {
610
- # Skip on non-Apple-Silicon or non-macOS
611
- if [[ "$(uname)" != "Darwin" ]] || [[ "$(uname -m)" != "arm64" ]]; then
612
- print_info "Rosetta audit: not applicable (Intel Mac or non-macOS)"
613
- return 0
614
- fi
615
-
616
- # Skip if no dual-brew setup
617
- if [[ ! -x "/usr/local/bin/brew" ]] || [[ ! -x "/opt/homebrew/bin/brew" ]]; then
618
- print_success "Rosetta audit: clean Homebrew setup (no x86 brew detected)"
619
- return 0
620
- fi
621
-
622
- print_info "Detected dual Homebrew (x86 + ARM) — checking for Rosetta overhead..."
623
-
624
- local x86_only_count dup_count
625
- dup_count=$(comm -12 \
626
- <(/usr/local/bin/brew list --formula 2>/dev/null | sort) \
627
- <(/opt/homebrew/bin/brew list --formula 2>/dev/null | sort) | wc -l | tr -d ' ')
628
- x86_only_count=$(comm -23 \
629
- <(/usr/local/bin/brew list --formula 2>/dev/null | sort) \
630
- <(/opt/homebrew/bin/brew list --formula 2>/dev/null | sort) | wc -l | tr -d ' ')
631
-
632
- local total=$((x86_only_count + dup_count))
633
-
634
- if [[ "$total" -eq 0 ]]; then
635
- print_success "No x86 Homebrew packages found — clean ARM setup"
636
- return 0
637
- fi
638
-
639
- print_warning "Found $total x86 Homebrew packages ($x86_only_count x86-only, $dup_count duplicates)"
640
- echo " These run under Rosetta 2 emulation with ~30% performance overhead"
641
- echo ""
642
- echo " To audit: rosetta-audit-helper.sh scan"
643
- echo " To migrate: rosetta-audit-helper.sh migrate --dry-run"
644
- echo " To fix: rosetta-audit-helper.sh migrate"
645
-
646
- return 0
647
- }
648
-
649
- # Install Worktrunk shell integration (enables 'wt switch' to change directories).
650
- _setup_worktrunk_shell_integration() {
651
- print_info "Installing shell integration..."
652
- if wt config shell install; then
653
- print_success "Shell integration installed"
654
- print_info "Restart your terminal or source your shell config"
655
- else
656
- print_warning "Shell integration failed - run manually: wt config shell install"
657
- fi
658
- return 0
659
- }
660
-
661
- # Check and optionally install Worktrunk shell integration when wt is already present.
662
- _check_worktrunk_shell_integration() {
663
- local wt_integrated=false
664
- local rc_file
665
- while IFS= read -r rc_file; do
666
- [[ -z "$rc_file" ]] && continue
667
- if [[ -f "$rc_file" ]] && grep -q "worktrunk" "$rc_file" 2>/dev/null; then
668
- wt_integrated=true
669
- break
670
- fi
671
- done < <(get_all_shell_rcs)
672
-
673
- if [[ "$wt_integrated" == "false" ]]; then
674
- print_info "Shell integration not detected"
675
- local install_shell
676
- setup_prompt install_shell "Install Worktrunk shell integration (enables 'wt switch' to change directories)? [Y/n]: " "Y"
677
- if [[ "$install_shell" =~ ^[Yy]?$ ]]; then
678
- _setup_worktrunk_shell_integration
679
- fi
680
- else
681
- print_success "Shell integration already configured"
682
- fi
683
- return 0
684
- }
685
-
686
- # Install Worktrunk via Homebrew and set up shell integration.
687
- _install_worktrunk_brew() {
688
- local install_wt
689
- setup_prompt install_wt "Install Worktrunk via Homebrew? [Y/n]: " "Y"
690
-
691
- if [[ "$install_wt" =~ ^[Yy]?$ ]]; then
692
- if run_with_spinner "Installing Worktrunk via Homebrew" brew install max-sixty/worktrunk/wt; then
693
- _setup_worktrunk_shell_integration
694
- echo ""
695
- print_info "Quick start:"
696
- echo " wt switch feature/my-feature # Create/switch to worktree"
697
- echo " wt list # List all worktrees"
698
- echo " wt merge # Merge and cleanup"
699
- echo ""
700
- print_info "Documentation: ~/.aidevops/agents/tools/git/worktrunk.md"
701
- else
702
- print_warning "Homebrew installation failed"
703
- echo " Try: cargo install worktrunk && wt config shell install"
704
- fi
705
- else
706
- print_info "Skipped Worktrunk installation"
707
- print_info "Install later: brew install max-sixty/worktrunk/wt"
708
- print_info "Fallback available: ~/.aidevops/agents/scripts/worktree-helper.sh"
709
- fi
710
- return 0
711
- }
712
-
713
- # Install Worktrunk via Cargo and set up shell integration.
714
- _install_worktrunk_cargo() {
715
- local install_wt
716
- setup_prompt install_wt "Install Worktrunk via Cargo? [Y/n]: " "Y"
717
-
718
- if [[ "$install_wt" =~ ^[Yy]?$ ]]; then
719
- if run_with_spinner "Installing Worktrunk via Cargo" cargo install worktrunk; then
720
- _setup_worktrunk_shell_integration
721
- else
722
- print_warning "Cargo installation failed"
723
- fi
724
- else
725
- print_info "Skipped Worktrunk installation"
726
- fi
727
- return 0
728
- }
729
-
730
- setup_worktrunk() {
731
- print_info "Setting up Worktrunk (git worktree management)..."
732
-
733
- # Check if worktrunk (wt) is already installed
734
- if command -v wt >/dev/null 2>&1; then
735
- local wt_version
736
- wt_version=$(wt --version 2>/dev/null | head -1 || echo "unknown")
737
- print_success "Worktrunk already installed: $wt_version"
738
- _check_worktrunk_shell_integration
739
- return 0
740
- fi
741
-
742
- # Worktrunk not installed - offer to install
743
- print_info "Worktrunk makes git worktrees as easy as branches"
744
- echo " • wt switch feat - Switch/create worktree (with cd)"
745
- echo " • wt list - List worktrees with CI status"
746
- echo " • wt merge - Squash/rebase/merge + cleanup"
747
- echo " • Hooks for automated setup (npm install, etc.)"
748
- echo ""
749
- echo " Note: aidevops also includes worktree-helper.sh as a fallback"
750
- echo ""
751
-
752
- local pkg_manager
753
- pkg_manager=$(detect_package_manager)
754
-
755
- if [[ "$pkg_manager" == "brew" ]]; then
756
- _install_worktrunk_brew
757
- elif command -v cargo >/dev/null 2>&1; then
758
- _install_worktrunk_cargo
759
- else
760
- print_warning "Worktrunk not installed"
761
- echo ""
762
- echo " Install options:"
763
- echo " macOS/Linux (Homebrew): brew install max-sixty/worktrunk/wt"
764
- echo " Cargo: cargo install worktrunk"
765
- echo " Windows: winget install max-sixty.worktrunk"
766
- echo ""
767
- echo " After install: wt config shell install"
768
- echo ""
769
- print_info "Fallback available: ~/.aidevops/agents/scripts/worktree-helper.sh"
770
- fi
771
-
772
- return 0
773
- }
774
-
775
- # Trigger OpenCode extension install in Zed via the zed:// URI scheme.
776
- _install_opencode_ext_for_zed() {
777
- local install_opencode_ext
778
- setup_prompt install_opencode_ext "Install OpenCode extension for Zed? [Y/n]: " "Y"
779
- if [[ "$install_opencode_ext" =~ ^[Yy]?$ ]]; then
780
- print_info "Installing OpenCode extension..."
781
- if [[ "$(uname)" == "Darwin" ]]; then
782
- open "zed://extension/opencode" 2>/dev/null
783
- print_success "OpenCode extension install triggered"
784
- print_info "Zed will open and prompt to install the extension"
785
- elif [[ "$(uname)" == "Linux" ]]; then
786
- xdg-open "zed://extension/opencode" 2>/dev/null ||
787
- print_info "Open Zed and install 'opencode' from Extensions (Cmd+Shift+X)"
788
- fi
789
- fi
790
- return 0
791
- }
792
-
793
- # Install Tabby terminal on Linux (x86_64 only via packagecloud; ARM64 manual).
794
- _install_tabby_linux() {
795
- local arch
796
- arch=$(uname -m)
797
- # Tabby packagecloud repo only has x86_64 packages
798
- # ARM64 (aarch64) must use .deb from GitHub releases or skip
799
- if [[ "$arch" == "aarch64" || "$arch" == "arm64" ]]; then
800
- # Clean up stale Tabby packagecloud repo if it exists from a previous run
801
- # (it causes apt-get update failures on ARM64)
802
- if [[ -f /etc/apt/sources.list.d/eugeny_tabby.list ]]; then
803
- print_info "Removing stale Tabby packagecloud repo (not available for ARM64)..."
804
- sudo rm -f /etc/apt/sources.list.d/eugeny_tabby.list
805
- sudo rm -f /etc/apt/sources.list.d/eugeny_tabby.sources
806
- sudo apt-get update -qq 2>/dev/null || true
807
- fi
808
- print_warning "Tabby packages are not available for ARM64 Linux via package manager"
809
- echo " Download ARM64 .deb from: https://github.com/Eugeny/tabby/releases/latest"
810
- echo " Or skip Tabby - it's optional (a modern terminal emulator)"
811
- return 0
812
- fi
813
-
814
- local pkg_manager
815
- pkg_manager=$(detect_package_manager)
816
- case "$pkg_manager" in
817
- apt)
818
- # Add packagecloud repo for Tabby (verified download, not piped to sudo)
819
- # shellcheck disable=SC2034 # Read by verified_install() in setup.sh
820
- VERIFIED_INSTALL_SUDO="true"
821
- if verified_install "Tabby repository (apt)" "https://packagecloud.io/install/repositories/eugeny/tabby/script.deb.sh"; then
822
- if ! sudo apt-get install -y tabby-terminal; then
823
- print_warning "Tabby package not found for this architecture"
824
- echo " Download from: https://github.com/Eugeny/tabby/releases/latest"
825
- fi
826
- fi
827
- ;;
828
- dnf | yum)
829
- # shellcheck disable=SC2034 # Read by verified_install() in setup.sh
830
- VERIFIED_INSTALL_SUDO="true"
831
- if verified_install "Tabby repository (rpm)" "https://packagecloud.io/install/repositories/eugeny/tabby/script.rpm.sh"; then
832
- if ! sudo "$pkg_manager" install -y tabby-terminal; then
833
- print_warning "Tabby package not found for this architecture"
834
- echo " Download from: https://github.com/Eugeny/tabby/releases/latest"
835
- fi
836
- fi
837
- ;;
838
- pacman)
839
- # AUR package
840
- print_info "Tabby available in AUR as 'tabby-bin'"
841
- echo " Install with: yay -S tabby-bin"
842
- ;;
843
- *)
844
- echo " Download manually: https://github.com/Eugeny/tabby/releases/latest"
845
- ;;
846
- esac
847
- return 0
848
- }
849
-
850
- # Offer and perform Tabby terminal installation.
851
- _install_tabby() {
852
- local install_tabby
853
- setup_prompt install_tabby "Install Tabby terminal? [Y/n]: " "Y"
854
-
855
- if [[ "$install_tabby" =~ ^[Yy]?$ ]]; then
856
- if [[ "$(uname)" == "Darwin" ]]; then
857
- if command -v brew >/dev/null 2>&1; then
858
- if run_with_spinner "Installing Tabby" brew install --cask tabby; then
859
- : # Success message handled by spinner
860
- else
861
- print_warning "Failed to install Tabby via Homebrew"
862
- echo " Download manually: https://github.com/Eugeny/tabby/releases/latest"
863
- fi
864
- else
865
- print_warning "Homebrew not found"
866
- echo " Download manually: https://github.com/Eugeny/tabby/releases/latest"
867
- fi
868
- elif [[ "$(uname)" == "Linux" ]]; then
869
- _install_tabby_linux
870
- fi
871
- else
872
- print_info "Skipped Tabby installation"
873
- fi
874
- return 0
875
- }
876
-
877
- # Offer and perform Zed editor installation, then optionally install OpenCode extension.
878
- _install_zed_and_opencode_ext() {
879
- local install_zed
880
- setup_prompt install_zed "Install Zed editor? [Y/n]: " "Y"
881
-
882
- if [[ "$install_zed" =~ ^[Yy]?$ ]]; then
883
- local zed_installed=false
884
- if [[ "$(uname)" == "Darwin" ]]; then
885
- if command -v brew >/dev/null 2>&1; then
886
- if run_with_spinner "Installing Zed" brew install --cask zed; then
887
- zed_installed=true
888
- else
889
- print_warning "Failed to install Zed via Homebrew"
890
- echo " Download manually: https://zed.dev/download"
891
- fi
892
- else
893
- print_warning "Homebrew not found"
894
- echo " Download manually: https://zed.dev/download"
895
- fi
896
- elif [[ "$(uname)" == "Linux" ]]; then
897
- # Zed provides an install script for Linux (verified download)
898
- # shellcheck disable=SC2034 # Read by verified_install() in setup.sh
899
- VERIFIED_INSTALL_SHELL="sh"
900
- if verified_install "Zed" "https://zed.dev/install.sh"; then
901
- zed_installed=true
902
- else
903
- print_warning "Failed to install Zed"
904
- echo " See: https://zed.dev/docs/linux"
905
- fi
906
- fi
907
-
908
- if [[ "$zed_installed" == "true" ]]; then
909
- _install_opencode_ext_for_zed
910
- fi
911
- else
912
- print_info "Skipped Zed installation"
913
- fi
914
- return 0
915
- }
916
-
917
- # Check for OpenCode extension in an existing Zed installation and offer to install.
918
- _check_opencode_ext_existing_zed() {
919
- local zed_extensions_dir=""
920
- if [[ "$(uname)" == "Darwin" ]]; then
921
- zed_extensions_dir="$HOME/Library/Application Support/Zed/extensions/installed"
922
- elif [[ "$(uname)" == "Linux" ]]; then
923
- zed_extensions_dir="$HOME/.local/share/zed/extensions/installed"
924
- fi
925
-
926
- if [[ -d "$zed_extensions_dir" ]]; then
927
- if [[ ! -d "$zed_extensions_dir/opencode" ]]; then
928
- _install_opencode_ext_for_zed
929
- else
930
- print_success "OpenCode extension already installed in Zed"
931
- fi
932
- fi
933
- return 0
934
- }
935
-
936
- setup_recommended_tools() {
937
- print_info "Checking recommended development tools..."
938
-
939
- local missing_tools=()
940
- local missing_names=()
941
-
942
- # Check for Tabby terminal
943
- if [[ "$(uname)" == "Darwin" ]]; then
944
- # macOS - check Applications folder
945
- if [[ ! -d "/Applications/Tabby.app" ]]; then
946
- missing_tools+=("tabby")
947
- missing_names+=("Tabby (modern terminal)")
948
- else
949
- print_success "Tabby terminal found"
950
- fi
951
- elif [[ "$(uname)" == "Linux" ]]; then
952
- # Linux - check if tabby command exists
953
- if ! command -v tabby >/dev/null 2>&1; then
954
- missing_tools+=("tabby")
955
- missing_names+=("Tabby (modern terminal)")
956
- else
957
- print_success "Tabby terminal found"
958
- fi
959
- fi
960
-
961
- # Check for Zed editor
962
- local zed_exists=false
963
- if [[ "$(uname)" == "Darwin" ]]; then
964
- # macOS - check Applications folder
965
- if [[ ! -d "/Applications/Zed.app" ]]; then
966
- missing_tools+=("zed")
967
- missing_names+=("Zed (AI-native editor)")
968
- else
969
- print_success "Zed editor found"
970
- zed_exists=true
971
- fi
972
- elif [[ "$(uname)" == "Linux" ]]; then
973
- # Linux - check if zed command exists
974
- if ! command -v zed >/dev/null 2>&1; then
975
- missing_tools+=("zed")
976
- missing_names+=("Zed (AI-native editor)")
977
- else
978
- print_success "Zed editor found"
979
- zed_exists=true
980
- fi
981
- fi
982
-
983
- # Check for OpenCode extension in existing Zed installation
984
- if [[ "$zed_exists" == "true" ]]; then
985
- _check_opencode_ext_existing_zed
986
- fi
987
-
988
- # Offer to install missing tools
989
- if [[ ${#missing_tools[@]} -gt 0 ]]; then
990
- print_warning "Missing recommended tools: ${missing_names[*]}"
991
- echo " Tabby - Modern terminal with profiles, SSH manager, split panes"
992
- echo " Zed - High-performance AI-native code editor"
993
- echo ""
994
-
995
- # Install Tabby if missing
996
- if [[ " ${missing_tools[*]} " =~ " tabby " ]]; then
997
- _install_tabby
998
- fi
999
-
1000
- # Install Zed if missing
1001
- if [[ " ${missing_tools[*]} " =~ " zed " ]]; then
1002
- _install_zed_and_opencode_ext
1003
- fi
1004
- else
1005
- print_success "All recommended tools installed!"
1006
- fi
1007
-
1008
- # Check for Cursor CLI (agent) — independent of the missing_tools flow
1009
- # since it uses a curl installer, not brew
1010
- setup_cursor_cli
1011
-
1012
- return 0
1013
- }
1014
-
1015
- setup_cursor_cli() {
1016
- print_info "Checking Cursor CLI (agent)..."
1017
-
1018
- if command -v agent >/dev/null 2>&1; then
1019
- local cursor_version
1020
- cursor_version=$(agent --version 2>/dev/null || echo "unknown")
1021
- print_success "Cursor CLI found: $cursor_version"
1022
- return 0
1023
- fi
1024
-
1025
- # Check ~/.local/bin specifically (may not be in PATH yet)
1026
- if [[ -x "$HOME/.local/bin/agent" ]]; then
1027
- local cursor_version
1028
- cursor_version=$("$HOME/.local/bin/agent" --version 2>/dev/null || echo "unknown")
1029
- print_success "Cursor CLI found at ~/.local/bin/agent: $cursor_version"
1030
- print_info "Ensure ~/.local/bin is in your PATH"
1031
- return 0
1032
- fi
1033
-
1034
- echo " Cursor CLI provides access to Cursor's AI models (including Composer 2)"
1035
- echo " from the terminal. Also usable as an OpenCode provider via the"
1036
- echo " opencode-cursor plugin for OAuth-based model access."
1037
- echo ""
1038
-
1039
- local install_cursor
1040
- setup_prompt install_cursor "Install Cursor CLI? [Y/n]: " "Y"
1041
-
1042
- if [[ "$install_cursor" =~ ^[Yy]?$ ]]; then
1043
- print_info "Installing Cursor CLI..."
1044
- if verified_install "Cursor CLI" "https://cursor.com/install"; then
1045
- # Ensure ~/.local/bin is in PATH for this session
1046
- if [[ ":$PATH:" != *":$HOME/.local/bin:"* ]]; then
1047
- export PATH="$HOME/.local/bin:$PATH"
1048
- print_info "Added ~/.local/bin to PATH for this session"
1049
- fi
1050
- print_success "Cursor CLI installed"
1051
- echo ""
1052
- echo " Next steps:"
1053
- echo " agent login # Authenticate with your Cursor account"
1054
- echo " agent models # List available models"
1055
- echo " agent status # Check auth status"
1056
- else
1057
- print_warning "Failed to install Cursor CLI"
1058
- echo " Install manually: curl https://cursor.com/install -fsS | bash"
1059
- fi
1060
- else
1061
- print_info "Skipped Cursor CLI installation"
1062
- echo " Install later: curl https://cursor.com/install -fsS | bash"
1063
- fi
1064
-
1065
- return 0
1066
- }
1067
-
1068
- setup_minisim() {
1069
- # Only available on macOS
1070
- if [[ "$(uname)" != "Darwin" ]]; then
1071
- return 0
1072
- fi
1073
-
1074
- print_info "Setting up MiniSim (iOS/Android emulator launcher)..."
1075
-
1076
- # Check if MiniSim is already installed
1077
- if [[ -d "/Applications/MiniSim.app" ]]; then
1078
- print_success "MiniSim already installed"
1079
- print_info "Global shortcut: Option + Shift + E"
1080
- return 0
1081
- fi
1082
-
1083
- # Check if Xcode or Android Studio is installed (MiniSim needs at least one)
1084
- local has_xcode=false
1085
- local has_android=false
1086
-
1087
- if command -v xcrun >/dev/null 2>&1 && xcrun simctl list devices >/dev/null 2>&1; then
1088
- has_xcode=true
1089
- fi
1090
-
1091
- if [[ -n "${ANDROID_HOME:-}" ]] || [[ -n "${ANDROID_SDK_ROOT:-}" ]] || [[ -d "$HOME/Library/Android/sdk" ]]; then
1092
- has_android=true
1093
- fi
1094
-
1095
- if [[ "$has_xcode" == "false" && "$has_android" == "false" ]]; then
1096
- print_info "MiniSim requires Xcode (iOS) or Android Studio (Android)"
1097
- print_info "Install one of these first, then re-run setup to install MiniSim"
1098
- return 0
1099
- fi
1100
-
1101
- # Show what's available
1102
- local available_for=""
1103
- if [[ "$has_xcode" == "true" ]]; then
1104
- available_for="iOS simulators"
1105
- fi
1106
- if [[ "$has_android" == "true" ]]; then
1107
- if [[ -n "$available_for" ]]; then
1108
- available_for="$available_for and Android emulators"
1109
- else
1110
- available_for="Android emulators"
1111
- fi
1112
- fi
1113
-
1114
- print_info "MiniSim is a menu bar app for launching $available_for"
1115
- echo " Features:"
1116
- echo " - Global shortcut: Option + Shift + E"
1117
- echo " - Launch/manage iOS simulators and Android emulators"
1118
- echo " - Copy device UDID/ADB ID"
1119
- echo " - Cold boot Android emulators"
1120
- echo " - Run Android emulators without audio (saves Bluetooth battery)"
1121
- echo ""
1122
-
1123
- # Check if Homebrew is available
1124
- if ! command -v brew >/dev/null 2>&1; then
1125
- print_warning "Homebrew not found - cannot install MiniSim automatically"
1126
- echo " Install manually: https://github.com/okwasniewski/MiniSim/releases"
1127
- return 0
1128
- fi
1129
-
1130
- local install_minisim
1131
- setup_prompt install_minisim "Install MiniSim? [Y/n]: " "Y"
1132
-
1133
- if [[ "$install_minisim" =~ ^[Yy]?$ ]]; then
1134
- if run_with_spinner "Installing MiniSim" brew install --cask minisim; then
1135
- print_info "Global shortcut: Option + Shift + E"
1136
- print_info "Documentation: ~/.aidevops/agents/tools/mobile/minisim.md"
1137
- else
1138
- print_warning "Failed to install MiniSim via Homebrew"
1139
- echo " Install manually: https://github.com/okwasniewski/MiniSim/releases"
1140
- fi
1141
- else
1142
- print_info "Skipped MiniSim installation"
1143
- print_info "Install later: brew install --cask minisim"
1144
- fi
1145
-
1146
- return 0
1147
- }
1148
-
1149
- setup_claudebar() {
1150
- local claudebar_release_url="https://github.com/tddworks/ClaudeBar/releases/latest"
1151
- # Only available on macOS (native Swift menu bar app)
1152
- if [[ "$(uname)" != "Darwin" ]]; then
1153
- return 0
1154
- fi
1155
-
1156
- print_info "Setting up ClaudeBar (AI quota monitor)..."
1157
-
1158
- # Check if ClaudeBar is already installed
1159
- if [[ -d "/Applications/ClaudeBar.app" ]]; then
1160
- print_success "ClaudeBar already installed"
1161
- return 0
1162
- fi
1163
-
1164
- # Check if Homebrew is available (required for cask install)
1165
- if ! command -v brew >/dev/null 2>&1; then
1166
- print_warning "Homebrew not found - cannot install ClaudeBar automatically"
1167
- echo " Download manually: $claudebar_release_url"
1168
- return 0
1169
- fi
1170
-
1171
- print_info "ClaudeBar monitors AI coding assistant usage quotas in your menu bar"
1172
- echo " Supports: Claude, Codex, Gemini, Copilot, Antigravity, Kimi, Kiro, Amp"
1173
- echo " Features: real-time quota tracking, status notifications, multiple themes"
1174
- echo " Requires: macOS 15+, CLI tools for providers you want to monitor"
1175
- echo ""
1176
-
1177
- local install_claudebar
1178
- setup_prompt install_claudebar "Install ClaudeBar? [Y/n]: " "Y"
1179
-
1180
- if [[ "$install_claudebar" =~ ^[Yy]?$ ]]; then
1181
- if run_with_spinner "Installing ClaudeBar" brew install --cask claudebar; then
1182
- print_success "ClaudeBar installed"
1183
- print_info "Launch from Applications or Spotlight to start monitoring quotas"
1184
- else
1185
- print_warning "Failed to install ClaudeBar via Homebrew"
1186
- echo " Download manually: $claudebar_release_url"
1187
- fi
1188
- else
1189
- print_info "Skipped ClaudeBar installation"
1190
- print_info "Install later: brew install --cask claudebar"
1191
- fi
1192
-
1193
- return 0
1194
- }
1195
-
1196
- setup_ssh_key() {
1197
- print_info "Checking SSH key setup..."
1198
-
1199
- if [[ ! -f ~/.ssh/id_ed25519 ]]; then
1200
- print_warning "Ed25519 SSH key not found"
1201
-
1202
- # SSH key generation requires email input — skip in non-interactive mode
1203
- if [[ "${NON_INTERACTIVE:-false}" == "true" ]] || [[ ! -t 0 ]]; then
1204
- print_info "Skipping SSH key generation (non-interactive mode)"
1205
- return 0
1206
- fi
1207
-
1208
- local generate_key
1209
- setup_prompt generate_key "Generate new Ed25519 SSH key? [Y/n]: " "Y"
1210
-
1211
- if [[ "$generate_key" =~ ^[Yy]?$ ]]; then
1212
- local email
1213
- setup_prompt email "Enter your email address: " ""
1214
- if [[ -z "$email" ]]; then
1215
- print_warning "No email provided — skipping SSH key generation"
1216
- return 0
1217
- fi
1218
- install -d -m 700 ~/.ssh
1219
- ssh-keygen -t ed25519 -C "$email" -f ~/.ssh/id_ed25519
1220
- print_success "SSH key generated"
1221
- else
1222
- print_info "Skipping SSH key generation"
1223
- fi
1224
- else
1225
- print_success "Ed25519 SSH key found"
1226
- fi
1227
- return 0
1228
- }
1229
-
1230
- # Check installed Python version against latest stable available from package manager.
1231
- # Warns if an upgrade is available but never auto-upgrades (GH#5237).
1232
- # Works on macOS (Homebrew) and Linux (apt/dnf).
1233
- # Named check_python_upgrade_available() to avoid collision with the shared
1234
- # check_python_version() in _common.sh (which validates minimum required version).
1235
- check_python_upgrade_available() {
1236
- print_info "Checking Python version..."
1237
-
1238
- # 1. Check currently installed Python
1239
- local python3_bin
1240
- if ! python3_bin=$(find_python3); then
1241
- print_warning "Python 3 not found"
1242
- echo ""
1243
- echo " Install options:"
1244
- if [[ "$PLATFORM_MACOS" == "true" ]]; then
1245
- echo " brew install python3"
1246
- elif command -v apt-get >/dev/null 2>&1; then
1247
- echo " sudo apt install python3"
1248
- elif command -v dnf >/dev/null 2>&1; then
1249
- echo " sudo dnf install python3"
1250
- else
1251
- echo " Install Python 3 via your system package manager"
1252
- fi
1253
- echo ""
1254
- return 0
1255
- fi
1256
-
1257
- local installed_version
1258
- installed_version=$("$python3_bin" --version 2>&1 | cut -d' ' -f2)
1259
- local installed_major installed_minor
1260
- installed_major=$(echo "$installed_version" | cut -d. -f1)
1261
- installed_minor=$(echo "$installed_version" | cut -d. -f2)
1262
-
1263
- # 2. Determine latest stable version from package manager
1264
- local latest_version=""
1265
-
1266
- if [[ "$PLATFORM_MACOS" == "true" ]] && command -v brew >/dev/null 2>&1; then
1267
- # Homebrew: `brew info python3` outputs "python@3.X: 3.X.Y" on the first line
1268
- latest_version=$(brew info --json=v2 python3 2>/dev/null |
1269
- python3 -c "import sys,json; d=json.load(sys.stdin); print(d['formulae'][0]['versions']['stable'])" 2>/dev/null) || latest_version=""
1270
- elif command -v apt-cache >/dev/null 2>&1; then
1271
- # Debian/Ubuntu: get candidate version from apt-cache
1272
- latest_version=$(apt-cache policy python3 2>/dev/null |
1273
- awk '/Candidate:/{print $2}' |
1274
- grep -oE '[0-9]+\.[0-9]+\.[0-9]+') || latest_version=""
1275
- elif command -v dnf >/dev/null 2>&1; then
1276
- # Fedora/RHEL: get available version from dnf
1277
- latest_version=$(dnf info python3 2>/dev/null |
1278
- awk '/^Version/{print $3}') || latest_version=""
1279
- fi
1280
-
1281
- # 3. Compare versions and advise
1282
- if [[ -z "$latest_version" ]]; then
1283
- # Could not determine latest — just report installed version
1284
- print_success "Python $installed_version found"
1285
- return 0
1286
- fi
1287
-
1288
- local latest_major latest_minor
1289
- latest_major=$(echo "$latest_version" | cut -d. -f1)
1290
- latest_minor=$(echo "$latest_version" | cut -d. -f2)
1291
-
1292
- # Compare major.minor (patch differences are not worth warning about)
1293
- if [[ "$installed_major" -lt "$latest_major" ]] ||
1294
- { [[ "$installed_major" -eq "$latest_major" ]] && [[ "$installed_minor" -lt "$latest_minor" ]]; }; then
1295
- print_warning "Python $installed_version installed, but $latest_version is available"
1296
- echo ""
1297
- echo " Some tools and skills require Python 3.10+."
1298
- echo " Upgrade is recommended but not required."
1299
- echo ""
1300
- if [[ "$PLATFORM_MACOS" == "true" ]]; then
1301
- echo " Upgrade command:"
1302
- echo " brew upgrade python3"
1303
- elif command -v apt-get >/dev/null 2>&1; then
1304
- echo " Upgrade command:"
1305
- echo " sudo apt update && sudo apt install python3"
1306
- elif command -v dnf >/dev/null 2>&1; then
1307
- echo " Upgrade command:"
1308
- echo " sudo dnf upgrade python3"
1309
- fi
1310
- echo ""
1311
- else
1312
- print_success "Python $installed_version found (latest stable: $latest_version)"
1313
- fi
1314
-
1315
- return 0
1316
- }
1317
-
1318
- setup_python_env() {
1319
- print_info "Setting up Python environment for DSPy..."
1320
-
1321
- # Check if Python 3 is available
1322
- local python3_bin
1323
- if ! python3_bin=$(find_python3); then
1324
- print_warning "Python 3 not found - DSPy setup skipped"
1325
- print_info "Install Python 3.8+ to enable DSPy integration"
1326
- return
1327
- fi
1328
-
1329
- local python_version
1330
- python_version=$("$python3_bin" --version | cut -d' ' -f2 | cut -d'.' -f1-2)
1331
- local version_check
1332
- version_check=$("$python3_bin" -c "import sys; print(1 if sys.version_info >= (3, 8) else 0)")
1333
-
1334
- if [[ "$version_check" != "1" ]]; then
1335
- print_warning "Python 3.8+ required for DSPy, found $python_version - DSPy setup skipped"
1336
- return
1337
- fi
1338
-
1339
- # Create Python virtual environment
1340
- if [[ ! -d "python-env/dspy-env" ]] || [[ ! -f "python-env/dspy-env/bin/activate" ]]; then
1341
- print_info "Creating Python virtual environment for DSPy..."
1342
- mkdir -p python-env
1343
- # Remove corrupted venv if directory exists but activate script is missing
1344
- if [[ -d "python-env/dspy-env" ]] && [[ ! -f "python-env/dspy-env/bin/activate" ]]; then
1345
- rm -rf python-env/dspy-env
1346
- fi
1347
- if "$python3_bin" -m venv python-env/dspy-env; then
1348
- print_success "Python virtual environment created"
1349
- else
1350
- print_warning "Failed to create Python virtual environment - DSPy setup skipped"
1351
- return
1352
- fi
1353
- else
1354
- print_info "Python virtual environment already exists"
1355
- fi
1356
-
1357
- # Install DSPy dependencies
1358
- print_info "Installing DSPy dependencies..."
1359
- # shellcheck source=/dev/null
1360
- if [[ -f "python-env/dspy-env/bin/activate" ]]; then
1361
- source python-env/dspy-env/bin/activate
1362
- else
1363
- print_warning "Python venv activate script not found - DSPy setup skipped"
1364
- return
1365
- fi
1366
- pip install --upgrade pip >/dev/null 2>&1
1367
-
1368
- if run_with_spinner "Installing DSPy dependencies" pip install -r requirements.txt; then
1369
- : # Success message handled by spinner
1370
- else
1371
- print_info "Check requirements.txt or run manually:"
1372
- print_info " source python-env/dspy-env/bin/activate && pip install -r requirements.txt"
1373
- fi
1374
- }
1375
-
1376
- setup_nodejs_env() {
1377
- print_info "Setting up Node.js environment for DSPyGround..."
1378
-
1379
- # Check if Node.js is available
1380
- if ! command -v node &>/dev/null; then
1381
- print_warning "Node.js not found - DSPyGround setup skipped"
1382
- print_info "Install Node.js 18+ to enable DSPyGround integration"
1383
- return
1384
- fi
1385
-
1386
- local node_version
1387
- node_version=$(node --version 2>/dev/null | cut -d'v' -f2 | cut -d'.' -f1)
1388
- if [[ -z "$node_version" ]] || ! [[ "$node_version" =~ ^[0-9]+$ ]]; then
1389
- print_warning "Could not determine Node.js version - DSPyGround setup skipped"
1390
- return
1391
- fi
1392
- if [[ "$node_version" -lt 18 ]]; then
1393
- print_warning "Node.js 18+ required for DSPyGround, found v$node_version - DSPyGround setup skipped"
1394
- return
1395
- fi
1396
-
1397
- # Check if npm is available
1398
- if ! command -v npm &>/dev/null; then
1399
- print_warning "npm not found - DSPyGround setup skipped"
1400
- return
1401
- fi
1402
-
1403
- # Install DSPyGround globally if not already installed
1404
- if ! command -v dspyground &>/dev/null; then
1405
- if run_with_spinner "Installing DSPyGround" npm_global_install dspyground; then
1406
- : # Success message handled by spinner
1407
- else
1408
- print_warning "Try manually: sudo npm install -g dspyground"
1409
- fi
1410
- else
1411
- print_success "DSPyGround already installed"
1412
- fi
1413
- }
1414
-
1415
- # Install Node.js via apt, preferring NodeSource LTS over the distro package.
1416
- _install_nodejs_apt() {
1417
- # Clean up stale Tabby packagecloud repo if present (causes apt-get update failures)
1418
- if [[ -f /etc/apt/sources.list.d/eugeny_tabby.list ]]; then
1419
- local arch
1420
- arch=$(uname -m)
1421
- if [[ "$arch" == "aarch64" || "$arch" == "arm64" ]]; then
1422
- print_info "Removing stale Tabby repo (not available for ARM64)..."
1423
- sudo rm -f /etc/apt/sources.list.d/eugeny_tabby.list
1424
- sudo rm -f /etc/apt/sources.list.d/eugeny_tabby.sources
1425
- fi
1426
- fi
1427
-
1428
- # Use NodeSource for a recent version (apt default may be old)
1429
- print_info "Installing Node.js (via NodeSource for latest LTS)..."
1430
- if command -v curl >/dev/null 2>&1; then
1431
- # shellcheck disable=SC2034 # Read by verified_install() in setup.sh
1432
- VERIFIED_INSTALL_SUDO="true"
1433
- if verified_install "NodeSource repository" "https://deb.nodesource.com/setup_22.x"; then
1434
- # Install nodejs (NodeSource bundles npm, but distro fallback may not)
1435
- # Include npm explicitly in case NodeSource setup failed silently
1436
- # and apt falls back to the distro nodejs package (which lacks npm)
1437
- if sudo apt-get install -y nodejs npm 2>/dev/null || sudo apt-get install -y nodejs; then
1438
- print_success "Node.js installed: $(node --version)"
1439
- else
1440
- print_warning "Node.js installation failed"
1441
- fi
1442
- else
1443
- # Fallback to distro package
1444
- print_info "Falling back to distro Node.js package..."
1445
- if sudo apt-get install -y nodejs npm; then
1446
- print_success "Node.js installed: $(node --version)"
1447
- else
1448
- print_warning "Node.js installation failed"
1449
- fi
1450
- fi
1451
- else
1452
- if sudo apt-get install -y nodejs npm; then
1453
- print_success "Node.js installed: $(node --version)"
1454
- else
1455
- print_warning "Node.js installation failed"
1456
- fi
1457
- fi
1458
- return 0
1459
- }
1460
-
1461
- # Ensure npm is present when Node.js is already installed (distro packages may omit it).
1462
- _ensure_npm_installed() {
1463
- if command -v npm >/dev/null 2>&1; then
1464
- return 0
1465
- fi
1466
- print_info "npm not found (distro nodejs package may omit it) — installing..."
1467
- local pkg_manager
1468
- pkg_manager=$(detect_package_manager)
1469
- case "$pkg_manager" in
1470
- apt) sudo apt-get install -y npm 2>/dev/null || print_warning "Failed to install npm via apt" ;;
1471
- dnf | yum) sudo "$pkg_manager" install -y npm 2>/dev/null || print_warning "Failed to install npm via $pkg_manager" ;;
1472
- brew) brew install npm 2>/dev/null || print_warning "Failed to install npm via brew" ;;
1473
- *) print_warning "Cannot auto-install npm — install manually" ;;
1474
- esac
1475
- return 0
1476
- }
1477
-
1478
- setup_nodejs() {
1479
- # Check if Node.js is already installed
1480
- if command -v node >/dev/null 2>&1; then
1481
- local node_version
1482
- node_version=$(node --version 2>/dev/null || echo "unknown")
1483
- print_success "Node.js already installed: $node_version"
1484
- _ensure_npm_installed
1485
- return 0
1486
- fi
1487
-
1488
- print_info "Node.js is required for OpenCode, MCP servers, and many tools"
1489
-
1490
- local pkg_manager
1491
- pkg_manager=$(detect_package_manager)
1492
-
1493
- local install_node
1494
- case "$pkg_manager" in
1495
- brew)
1496
- setup_prompt install_node "Install Node.js via Homebrew? [Y/n]: " "Y"
1497
- if [[ "$install_node" =~ ^[Yy]?$ ]]; then
1498
- if run_with_spinner "Installing Node.js" brew install node; then
1499
- print_success "Node.js installed: $(node --version)"
1500
- else
1501
- print_warning "Node.js installation failed"
1502
- fi
1503
- fi
1504
- ;;
1505
- apt)
1506
- setup_prompt install_node "Install Node.js via apt? [Y/n]: " "Y"
1507
- if [[ "$install_node" =~ ^[Yy]?$ ]]; then
1508
- _install_nodejs_apt
1509
- fi
1510
- ;;
1511
- dnf | yum)
1512
- setup_prompt install_node "Install Node.js via $pkg_manager? [Y/n]: " "Y"
1513
- if [[ "$install_node" =~ ^[Yy]?$ ]]; then
1514
- if sudo "$pkg_manager" install -y nodejs npm; then
1515
- print_success "Node.js installed: $(node --version)"
1516
- else
1517
- print_warning "Node.js installation failed"
1518
- fi
1519
- fi
1520
- ;;
1521
- pacman)
1522
- setup_prompt install_node "Install Node.js via pacman? [Y/n]: " "Y"
1523
- if [[ "$install_node" =~ ^[Yy]?$ ]]; then
1524
- if sudo pacman -S --noconfirm nodejs npm; then
1525
- print_success "Node.js installed: $(node --version)"
1526
- else
1527
- print_warning "Node.js installation failed"
1528
- fi
1529
- fi
1530
- ;;
1531
- apk)
1532
- setup_prompt install_node "Install Node.js via apk? [Y/n]: " "Y"
1533
- if [[ "$install_node" =~ ^[Yy]?$ ]]; then
1534
- if sudo apk add nodejs npm; then
1535
- print_success "Node.js installed: $(node --version)"
1536
- else
1537
- print_warning "Node.js installation failed"
1538
- fi
1539
- fi
1540
- ;;
1541
- *)
1542
- print_warning "No supported package manager found for Node.js installation"
1543
- echo " Install manually: https://nodejs.org/"
1544
- ;;
1545
- esac
1546
-
1547
- return 0
1548
- }
1549
-
1550
- # Bound OpenCode setup probes/installers so non-interactive setup cannot hang
1551
- # indefinitely when an opencode shim or package manager blocks.
1552
- _setup_opencode_timeout_cmd() {
1553
- local timeout_seconds="$1"
1554
- shift
1555
-
1556
- [[ "$timeout_seconds" =~ ^[0-9]+$ ]] || timeout_seconds=5
1557
- [[ "$timeout_seconds" -gt 0 ]] || timeout_seconds=5
1558
-
1559
- local command_name="${1:-}"
1560
- if [[ -n "$command_name" ]] && ! declare -F "$command_name" >/dev/null 2>&1; then
1561
- if declare -F timeout_sec >/dev/null 2>&1; then
1562
- timeout_sec "$timeout_seconds" "$@"
1563
- return $?
1564
- fi
1565
-
1566
- if command -v gtimeout >/dev/null 2>&1; then
1567
- gtimeout "$timeout_seconds" "$@"
1568
- return $?
1569
- fi
1570
-
1571
- if command -v timeout >/dev/null 2>&1; then
1572
- timeout "$timeout_seconds" "$@"
1573
- return $?
1574
- fi
1575
- fi
1576
-
1577
- local output_file=""
1578
- output_file=$(mktemp "${TMPDIR:-/tmp}/aidevops-opencode-timeout.XXXXXX" 2>/dev/null || printf '')
1579
- if [[ -z "$output_file" ]]; then
1580
- "$@"
1581
- return $?
1582
- fi
1583
-
1584
- local pid=""
1585
- "$@" >"$output_file" 2>&1 &
1586
- pid=$!
1587
-
1588
- local elapsed=0
1589
- while kill -0 "$pid" 2>/dev/null; do
1590
- if [[ "$elapsed" -ge "$timeout_seconds" ]]; then
1591
- kill "$pid" 2>/dev/null || true
1592
- wait "$pid" 2>/dev/null || true
1593
- rm -f "$output_file" 2>/dev/null || true
1594
- return 124
1595
- fi
1596
- sleep 1
1597
- elapsed=$((elapsed + 1))
1598
- done
1599
-
1600
- local rc=0
1601
- wait "$pid" || rc=$?
1602
- while IFS= read -r line; do
1603
- printf '%s\n' "$line"
1604
- done <"$output_file"
1605
- rm -f "$output_file" 2>/dev/null || true
1606
- return "$rc"
1607
- }
1608
-
1609
- _setup_opencode_version_output() {
1610
- local bin="$1"
1611
- local version_timeout="${AIDEVOPS_OPENCODE_VERSION_TIMEOUT:-5}"
1612
-
1613
- _setup_opencode_timeout_cmd "$version_timeout" "$bin" --version
1614
- return $?
1615
- }
1616
-
1617
- _setup_opencode_first_line() {
1618
- local input="$1"
1619
- local first_line=""
1620
-
1621
- IFS= read -r first_line <<<"$input" || true
1622
- printf '%s\n' "$first_line"
1623
- return 0
1624
- }
1625
-
1626
- # t2891: Validate that an opencode binary is real anomalyco/opencode.
1627
- # Mirrors the t2887 runtime canary validator (headless-runtime-lib.sh) and
1628
- # the t2888 setup module validator (.agents/scripts/setup/_services.sh).
1629
- # Inlined to keep tool-install.sh self-contained — sourced from setup.sh
1630
- # during early bootstrap before headless-runtime-lib.sh is on the path.
1631
- # Returns: 0=valid, 1=wrong package (e.g. claude CLI), 2=missing/unrunnable.
1632
- _setup_validate_opencode_binary() {
1633
- local bin="${1:-}"
1634
- [[ -n "$bin" ]] || return 2
1635
- command -v "$bin" >/dev/null 2>&1 || return 2
1636
-
1637
- local v
1638
- v=$(_setup_opencode_version_output "$bin" 2>/dev/null || printf '')
1639
- [[ -n "$v" ]] || return 2
1640
-
1641
- # Anthropic claude CLI signature — highest-confidence rejection.
1642
- [[ "$v" == *"(Claude Code)"* ]] && return 1
1643
-
1644
- # opencode is at 1.x; any 2.x+ is wrong (claude CLI is 2.1.x).
1645
- [[ "$v" =~ ^[2-9][0-9]*\. ]] && return 1
1646
-
1647
- # Sanity: must look like a semver (X.Y.Z).
1648
- [[ "$v" =~ ^[0-9]+\.[0-9]+\.[0-9]+ ]] || return 1
1649
-
1650
- return 0
1651
- }
1652
-
1653
- # t2891: detect-and-heal when 'opencode' bin name is owned by a wrong
1654
- # package (canonical case: @anthropic-ai/claude-code shadowing
1655
- # anomalyco/opencode after a npm install collision). Auto-installs
1656
- # without prompt because:
1657
- # 1) it's healing a broken state, not first-time setup
1658
- # 2) non-interactive runners (the canonical victim) would auto-Y anyway
1659
- # Idempotent. Fail-open if no installer available.
1660
- _setup_opencode_force_heal() {
1661
- local install_pkg="$1" wrong_bin="$2" wrong_v="$3"
1662
- print_warning "OpenCode binary at '$wrong_bin' is the wrong package ('$wrong_v')"
1663
- print_info "Forcing reinstall of $install_pkg to heal bin collision (t2891)..."
1664
-
1665
- local installer=""
1666
- if command -v bun >/dev/null 2>&1; then
1667
- installer="bun"
1668
- elif command -v npm >/dev/null 2>&1; then
1669
- installer="npm"
1670
- else
1671
- print_warning "Neither bun nor npm found — cannot heal OpenCode binary"
1672
- print_info "Install Node.js or Bun first, then re-run 'aidevops update'"
1673
- return 0
1674
- fi
1675
-
1676
- local install_timeout="${AIDEVOPS_OPENCODE_INSTALL_TIMEOUT:-180}"
1677
- if run_with_spinner "Reinstalling OpenCode (heal)" _setup_opencode_timeout_cmd "$install_timeout" npm_global_install "$install_pkg"; then
1678
- print_success "OpenCode reinstalled via $installer"
1679
- else
1680
- print_warning "Heal install failed via $installer"
1681
- print_info "Try manually: $installer install -g $install_pkg"
1682
- fi
1683
-
1684
- # Re-validate post-heal.
1685
- local new_bin
1686
- new_bin=$(command -v opencode 2>/dev/null || echo "")
1687
- if [[ -n "$new_bin" ]] && _setup_validate_opencode_binary "$new_bin"; then
1688
- local new_v
1689
- new_v=$(_setup_opencode_first_line "$(_setup_opencode_version_output "$new_bin" 2>/dev/null || printf 'unknown')")
1690
- print_success "OpenCode CLI: $new_bin ($new_v)"
1691
- mkdir -p "${HOME}/.aidevops" 2>/dev/null || true
1692
- printf '%s\n' "$new_bin" >"${HOME}/.aidevops/.opencode-bin-resolved" 2>/dev/null || true
1693
- else
1694
- local v_after
1695
- v_after=$(_setup_opencode_first_line "$(_setup_opencode_version_output "$new_bin" 2>/dev/null || printf '<missing>')")
1696
- print_warning "Post-heal validation still failing: '$new_bin' returns '$v_after'"
1697
- print_info "Check PATH: 'which -a opencode' — npm/bun global bin dir must come first"
1698
- fi
1699
- return 0
1700
- }
1701
-
1702
- setup_opencode_cli() {
1703
- print_info "Setting up OpenCode CLI..."
1704
-
1705
- # Respect OPENCODE_PINNED_VERSION from shared-constants.sh if sourced,
1706
- # otherwise fall back to latest.
1707
- local pin_ver="${OPENCODE_PINNED_VERSION:-latest}"
1708
- local install_pkg="opencode-ai@${pin_ver}"
1709
-
1710
- # t2891: validate the resolved binary is anomalyco/opencode, not a
1711
- # wrong package (claude CLI etc) that took the 'opencode' bin name.
1712
- # Without this, alex-solovyev's runner — where command -v opencode
1713
- # resolves to @anthropic-ai/claude-code — silently passes through
1714
- # this function, leaving t2887's runtime canary to throttle the spam
1715
- # without ever healing the binary.
1716
- local current_bin
1717
- current_bin=$(command -v opencode 2>/dev/null || echo "")
1718
- local validate_rc=0
1719
- if [[ -n "$current_bin" ]]; then
1720
- _setup_validate_opencode_binary "$current_bin" || validate_rc=$?
1721
- else
1722
- validate_rc=2
1723
- fi
1724
-
1725
- # Already valid → record + early return.
1726
- if [[ $validate_rc -eq 0 ]]; then
1727
- local oc_version
1728
- oc_version=$(_setup_opencode_first_line "$(_setup_opencode_version_output "$current_bin" 2>/dev/null || printf 'unknown')")
1729
- print_success "OpenCode already installed: $oc_version"
1730
- mkdir -p "${HOME}/.aidevops" 2>/dev/null || true
1731
- printf '%s\n' "$current_bin" >"${HOME}/.aidevops/.opencode-bin-resolved" 2>/dev/null || true
1732
- return 0
1733
- fi
1734
-
1735
- # Wrong package → auto-heal (no prompt).
1736
- if [[ $validate_rc -eq 1 ]]; then
1737
- local wrong_v
1738
- wrong_v=$(_setup_opencode_first_line "$(_setup_opencode_version_output "$current_bin" 2>/dev/null || printf '<unknown>')")
1739
- _setup_opencode_force_heal "$install_pkg" "$current_bin" "$wrong_v"
1740
- return 0
1741
- fi
1742
-
1743
- # Missing → first-time install path (preserves prompt for interactive UX).
1744
- local installer=""
1745
- if command -v bun >/dev/null 2>&1; then
1746
- installer="bun"
1747
- elif command -v npm >/dev/null 2>&1; then
1748
- installer="npm"
1749
- else
1750
- print_warning "Neither bun nor npm found - cannot install OpenCode"
1751
- print_info "Install Node.js first, then re-run setup"
1752
- return 0
1753
- fi
1754
-
1755
- print_info "OpenCode is the AI coding tool that aidevops is built for"
1756
- echo " It provides an AI-powered terminal interface for development tasks."
1757
- echo ""
1758
-
1759
- local install_oc
1760
- setup_prompt install_oc "Install OpenCode via $installer? [Y/n]: " "Y"
1761
- if [[ "$install_oc" =~ ^[Yy]?$ ]]; then
1762
- local install_timeout="${AIDEVOPS_OPENCODE_INSTALL_TIMEOUT:-180}"
1763
- if run_with_spinner "Installing OpenCode" _setup_opencode_timeout_cmd "$install_timeout" npm_global_install "$install_pkg"; then
1764
- print_success "OpenCode installed"
1765
-
1766
- # Persist resolved path on first-time success too (t2891).
1767
- local new_bin
1768
- new_bin=$(command -v opencode 2>/dev/null || echo "")
1769
- if [[ -n "$new_bin" ]] && _setup_validate_opencode_binary "$new_bin"; then
1770
- mkdir -p "${HOME}/.aidevops" 2>/dev/null || true
1771
- printf '%s\n' "$new_bin" >"${HOME}/.aidevops/.opencode-bin-resolved" 2>/dev/null || true
1772
- fi
1773
-
1774
- # Offer authentication
1775
- echo ""
1776
- print_info "OpenCode needs authentication to use AI models."
1777
- print_info "Run 'opencode auth login' to authenticate."
1778
- echo ""
1779
- else
1780
- print_warning "OpenCode installation failed"
1781
- print_info "Try manually: sudo npm install -g $install_pkg"
1782
- fi
1783
- else
1784
- print_info "Skipped OpenCode installation"
1785
- print_info "Install later: $installer install -g $install_pkg"
1786
- fi
1787
-
1788
- return 0
1789
- }
1790
-
1791
- setup_codex_cli() {
1792
- print_info "Setting up OpenAI Codex CLI..."
1793
-
1794
- # Check if Codex is already installed
1795
- if command -v codex >/dev/null 2>&1; then
1796
- local codex_version
1797
- codex_version=$(codex --version 2>/dev/null | head -1 || echo "unknown")
1798
- print_success "Codex already installed: $codex_version"
1799
- # Fix broken MCP_DOCKER if present
1800
- _fix_codex_docker_mcp
1801
- return 0
1802
- fi
1803
-
1804
- # Need either bun or npm to install
1805
- local installer=""
1806
- local install_pkg="@openai/codex@latest"
1807
-
1808
- if command -v bun >/dev/null 2>&1; then
1809
- installer="bun"
1810
- elif command -v npm >/dev/null 2>&1; then
1811
- installer="npm"
1812
- else
1813
- print_warning "Neither bun nor npm found - cannot install Codex"
1814
- print_info "Install Node.js first, then re-run setup"
1815
- return 0
1816
- fi
1817
-
1818
- print_info "Codex is OpenAI's AI coding CLI (terminal-based, agentic)"
1819
- echo " It provides an AI-powered terminal interface using OpenAI models."
1820
- echo ""
1821
-
1822
- local install_codex
1823
- setup_prompt install_codex "Install Codex via $installer? [Y/n]: " "Y"
1824
- if [[ "$install_codex" =~ ^[Yy]?$ ]]; then
1825
- if run_with_spinner "Installing Codex" npm_global_install "$install_pkg"; then
1826
- print_success "Codex installed"
1827
- echo ""
1828
- print_info "Codex needs OpenAI authentication."
1829
- print_info "Run 'codex' and follow the auth prompts."
1830
- echo ""
1831
- # Fix broken MCP_DOCKER if Codex created a default config
1832
- _fix_codex_docker_mcp
1833
- else
1834
- print_warning "Codex installation failed"
1835
- print_info "Try manually: npm install -g $install_pkg"
1836
- fi
1837
- else
1838
- print_info "Skipped Codex installation"
1839
- print_info "Install later: $installer install -g $install_pkg"
1840
- fi
1841
-
1842
- return 0
1843
- }
1844
-
1845
- # P0 fix: Remove broken MCP_DOCKER from Codex config.toml
1846
- # Docker Desktop 4.40+ with MCP Toolkit extension is required for `docker mcp`.
1847
- # OrbStack, Colima, Rancher Desktop do not support it.
1848
- _fix_codex_docker_mcp() {
1849
- local config="$HOME/.codex/config.toml"
1850
- [[ -f "$config" ]] || return 0
1851
-
1852
- # Check if MCP_DOCKER section exists
1853
- if ! grep -q '^\[mcp_servers\.MCP_DOCKER\]' "$config" 2>/dev/null; then
1854
- return 0
1855
- fi
1856
-
1857
- # Check if `docker mcp` subcommand is actually available
1858
- if docker mcp --help >/dev/null 2>&1; then
1859
- return 0
1860
- fi
1861
-
1862
- # Comment out the MCP_DOCKER section (from header to next section or EOF)
1863
- # Use sed to comment out lines from [mcp_servers.MCP_DOCKER] to the next
1864
- # section header or end of file. Portable sed (no -i on macOS without ext).
1865
- local tmp_config
1866
- tmp_config=$(mktemp)
1867
- local in_mcp_docker=false
1868
- while IFS= read -r line || [[ -n "$line" ]]; do
1869
- if [[ "$line" == "[mcp_servers.MCP_DOCKER]" ]]; then
1870
- in_mcp_docker=true
1871
- printf '# %s # Disabled by aidevops: docker mcp not available\n' "$line" >>"$tmp_config"
1872
- continue
1873
- fi
1874
- # If we hit another section header, stop commenting
1875
- if [[ "$in_mcp_docker" == "true" ]] && [[ "$line" == "["* ]]; then
1876
- in_mcp_docker=false
1877
- fi
1878
- if [[ "$in_mcp_docker" == "true" ]]; then
1879
- printf '# %s\n' "$line" >>"$tmp_config"
1880
- else
1881
- printf '%s\n' "$line" >>"$tmp_config"
1882
- fi
1883
- done <"$config"
1884
- mv "$tmp_config" "$config"
1885
- print_info "Disabled MCP_DOCKER in Codex config (docker mcp not available on this system)"
1886
- return 0
1887
- }
1888
-
1889
- setup_droid_cli() {
1890
- print_info "Setting up Factory.AI Droid CLI..."
1891
-
1892
- # Check if Droid is already installed
1893
- if command -v droid >/dev/null 2>&1; then
1894
- local droid_version
1895
- droid_version=$(droid --version 2>/dev/null | head -1 || echo "unknown")
1896
- print_success "Droid already installed: $droid_version"
1897
- return 0
1898
- fi
1899
-
1900
- # Droid uses its own installer — not available via npm/brew
1901
- print_info "Droid (Factory.AI) is an AI coding agent CLI"
1902
- echo " It provides autonomous coding capabilities with Factory.AI models."
1903
- echo ""
1904
-
1905
- local install_droid
1906
- setup_prompt install_droid "Install Droid CLI? [Y/n]: " "Y"
1907
- if [[ "$install_droid" =~ ^[Yy]?$ ]]; then
1908
- print_info "Installing Droid CLI..."
1909
- if command -v curl >/dev/null 2>&1; then
1910
- if curl -fsSL https://app.factory.ai/install.sh | bash 2>/dev/null; then
1911
- print_success "Droid installed"
1912
- echo ""
1913
- print_info "Run 'droid auth login' to authenticate with Factory.AI."
1914
- echo ""
1915
- else
1916
- print_warning "Droid installation failed"
1917
- print_info "Install manually from: https://docs.factory.ai/cli/installation"
1918
- fi
1919
- else
1920
- print_warning "curl not found - cannot install Droid"
1921
- print_info "Install manually from: https://docs.factory.ai/cli/installation"
1922
- fi
1923
- else
1924
- print_info "Skipped Droid installation"
1925
- print_info "Install later: curl -fsSL https://app.factory.ai/install.sh | bash"
1926
- fi
1927
-
1928
- return 0
1929
- }
1930
-
1931
- setup_google_workspace_cli() {
1932
- print_info "Setting up Google Workspace CLI (gws)..."
1933
-
1934
- # Check if gws is already installed
1935
- if command -v gws >/dev/null 2>&1; then
1936
- local gws_version
1937
- gws_version=$(gws --version 2>/dev/null | head -1 || echo "unknown")
1938
- print_success "Google Workspace CLI already installed: $gws_version"
1939
- return 0
1940
- fi
1941
-
1942
- # Need either bun or npm to install
1943
- local installer=""
1944
- local install_pkg="@googleworkspace/cli@latest"
1945
-
1946
- if command -v bun >/dev/null 2>&1; then
1947
- installer="bun"
1948
- elif command -v npm >/dev/null 2>&1; then
1949
- installer="npm"
1950
- else
1951
- print_warning "Neither bun nor npm found - cannot install gws"
1952
- print_info "Install Node.js first, then re-run setup"
1953
- return 0
1954
- fi
1955
-
1956
- print_info "Google Workspace CLI provides Gmail, Calendar, Drive, and all Workspace APIs"
1957
- echo " Used by Email, Business, and Accounts agents for Google Workspace integration."
1958
- echo ""
1959
-
1960
- local install_gws
1961
- setup_prompt install_gws "Install Google Workspace CLI via $installer? [Y/n]: " "Y"
1962
- if [[ "$install_gws" =~ ^[Yy]?$ ]]; then
1963
- if run_with_spinner "Installing Google Workspace CLI" npm_global_install "$install_pkg"; then
1964
- print_success "Google Workspace CLI installed"
1965
-
1966
- echo ""
1967
- print_info "Authentication required before use."
1968
- print_info "Run 'gws auth setup' to authenticate with your Google account."
1969
- print_info "For headless use: set GOOGLE_WORKSPACE_CLI_CREDENTIALS_FILE"
1970
- echo ""
1971
- else
1972
- print_warning "Google Workspace CLI installation failed"
1973
- print_info "Try manually: sudo npm install -g $install_pkg"
1974
- fi
1975
- else
1976
- print_info "Skipped Google Workspace CLI installation"
1977
- print_info "Install later: $installer install -g $install_pkg"
1978
- fi
1979
-
1980
- return 0
1981
- }
1982
-
1983
- setup_orbstack_vm() {
1984
- # Only available on macOS
1985
- if [[ "$(uname)" != "Darwin" ]]; then
1986
- return 0
1987
- fi
1988
-
1989
- # Check if OrbStack is already installed
1990
- if [[ -d "/Applications/OrbStack.app" ]] || command -v orb >/dev/null 2>&1; then
1991
- print_success "OrbStack already installed"
1992
- return 0
1993
- fi
1994
-
1995
- print_info "OrbStack provides fast, lightweight Linux VMs on macOS"
1996
- echo " You can run aidevops in an isolated Linux environment."
1997
- echo " This is optional - aidevops works natively on macOS too."
1998
- echo ""
1999
-
2000
- if ! command -v brew >/dev/null 2>&1; then
2001
- print_info "OrbStack available at: https://orbstack.dev/"
2002
- return 0
2003
- fi
2004
-
2005
- setup_prompt install_orb "Install OrbStack? [y/N]: " "n"
2006
- # shellcheck disable=SC2154 # set indirectly by setup_prompt via read
2007
- if [[ "$install_orb" =~ ^[Yy]$ ]]; then
2008
- if run_with_spinner "Installing OrbStack" brew install --cask orbstack; then
2009
- print_success "OrbStack installed"
2010
- print_info "Create a VM: orb create ubuntu aidevops"
2011
- print_info "Then install aidevops inside: orb run aidevops bash <(curl -fsSL https://aidevops.sh/install)"
2012
- else
2013
- print_warning "OrbStack installation failed"
2014
- print_info "Download manually: https://orbstack.dev/"
2015
- fi
2016
- else
2017
- print_info "Skipped OrbStack installation"
2018
- fi
2019
-
2020
- return 0
2021
- }
2022
-
2023
- setup_ai_orchestration() {
2024
- print_info "Setting up AI orchestration frameworks..."
2025
-
2026
- # Check Python — uses check_python_version from _common.sh to avoid
2027
- # duplicating find_python3 → parse → compare → offer_python_brew_install logic.
2028
- if ! check_python_version "" "AI orchestration" >/dev/null; then
2029
- return 0
2030
- fi
2031
-
2032
- # Create orchestration directory
2033
- mkdir -p "$HOME/.aidevops/orchestration"
2034
-
2035
- # Info about available frameworks
2036
- print_info "AI Orchestration Frameworks available:"
2037
- echo " - Langflow: Visual flow builder (localhost:7860)"
2038
- echo " - CrewAI: Multi-agent teams (localhost:8501)"
2039
- echo " - AutoGen: Microsoft agentic AI (localhost:8081)"
2040
- echo ""
2041
- print_info "Setup individual frameworks with:"
2042
- echo " bash .agents/scripts/langflow-helper.sh setup"
2043
- echo " bash .agents/scripts/crewai-helper.sh setup"
2044
- echo " bash .agents/scripts/autogen-helper.sh setup"
2045
- echo ""
2046
- print_info "See .agents/tools/ai-orchestration/overview.md for comparison"
2047
-
2048
- return 0
2049
- }
2050
-
2051
- # Prompt to install Ollama when the knowledge plane is enabled.
2052
- # Called from _setup_run_interactive when the user opts in.
2053
- # Installs Ollama and pulls the recommended fast model (llama3.1:8b).
2054
- # The reasoning model (llama3.1:70b) and embed model are suggested but not
2055
- # pulled automatically due to their large size (39 GB and 274 MB respectively).
2056
- setup_ollama_for_knowledge() {
2057
- print_info "Ollama — local LLM for knowledge plane (pii/sensitive/privileged tiers)"
2058
- print_info "Required for: tier:pii, tier:sensitive, tier:privileged routing."
2059
-
2060
- # Check if Ollama is already installed
2061
- if command -v ollama >/dev/null 2>&1; then
2062
- local version
2063
- version=$(ollama --version 2>/dev/null | grep -o '[0-9][0-9.]*' | head -1) || version="unknown"
2064
- print_success "Ollama already installed (version: ${version})"
2065
- else
2066
- print_info "Ollama not found."
2067
- if [[ "$(uname -s)" == "Darwin" ]] && command -v brew >/dev/null 2>&1; then
2068
- print_info "Installing Ollama via Homebrew..."
2069
- if brew install ollama 2>/dev/null; then
2070
- print_success "Ollama installed via Homebrew"
2071
- else
2072
- print_warning "Homebrew install failed. Download from https://ollama.com"
2073
- return 0
2074
- fi
2075
- else
2076
- print_info "Install Ollama manually from https://ollama.com"
2077
- print_info "Then re-run this setup to pull the recommended models."
2078
- return 0
2079
- fi
2080
- fi
2081
-
2082
- # Deploy the bundle config template
2083
- local bundle_dir="$HOME/.aidevops/configs"
2084
- local bundle_dest="${bundle_dir}/ollama-bundle.json"
2085
- local bundle_src
2086
- bundle_src="${BASH_SOURCE[0]%/*}/../.agents/templates/ollama-bundle.json"
2087
- if [[ ! -f "$bundle_src" ]]; then
2088
- bundle_src="$HOME/.aidevops/agents/templates/ollama-bundle.json"
2089
- fi
2090
- if [[ -f "$bundle_src" ]] && [[ ! -f "$bundle_dest" ]]; then
2091
- mkdir -p "$bundle_dir"
2092
- cp "$bundle_src" "$bundle_dest"
2093
- print_success "Ollama bundle config deployed: ${bundle_dest}"
2094
- fi
2095
-
2096
- # Start Ollama service if not running
2097
- if ! curl -sf "http://localhost:11434/api/tags" >/dev/null 2>&1; then
2098
- print_info "Starting Ollama service..."
2099
- ollama serve >/dev/null 2>&1 &
2100
- local i=0
2101
- while [[ $i -lt 10 ]]; do
2102
- if curl -sf "http://localhost:11434/api/tags" >/dev/null 2>&1; then
2103
- break
2104
- fi
2105
- sleep 1
2106
- i=$((i + 1))
2107
- done
2108
- fi
2109
-
2110
- # Pull the fast model (minimum required for pii/sensitive tiers)
2111
- print_info "Pulling minimum required model: llama3.1:8b (~4.9 GB)"
2112
- print_info "This is required for tier:pii and tier:sensitive routing."
2113
- if ollama pull llama3.1:8b 2>/dev/null; then
2114
- print_success "llama3.1:8b pulled successfully"
2115
- else
2116
- print_warning "Failed to pull llama3.1:8b. Run manually: ollama pull llama3.1:8b"
2117
- fi
2118
-
2119
- # Pull the embed model (small — pull automatically)
2120
- print_info "Pulling embed model: nomic-embed-text (~274 MB)"
2121
- if ollama pull nomic-embed-text 2>/dev/null; then
2122
- print_success "nomic-embed-text pulled successfully"
2123
- else
2124
- print_warning "Failed to pull nomic-embed-text. Run manually: ollama pull nomic-embed-text"
2125
- fi
2126
-
2127
- print_info ""
2128
- print_info "Optional: pull the reasoning model for tier:privileged (~39 GB, requires 48+ GB RAM):"
2129
- print_info " ollama pull llama3.1:70b"
2130
- print_info ""
2131
- print_info "Verify: ollama-helper.sh health"
2132
-
2133
- return 0
2134
- }