aid-installer 0.7.5

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/bin/aid ADDED
@@ -0,0 +1,931 @@
1
+ #!/usr/bin/env bash
2
+ # aid - AID CLI dispatcher (Bash side).
3
+ #
4
+ # Purpose:
5
+ # Persistent global command installed at $AID_HOME/bin/aid. Parses
6
+ # subcommands and dispatches to the shared install-core engine located at
7
+ # $AID_HOME/lib/aid-install-core.sh. Operates on the current working
8
+ # directory (--target / AID_TARGET overrides).
9
+ #
10
+ # Usage:
11
+ # aid Show the dashboard
12
+ # aid -h | --help Show help
13
+ # aid version Print the CLI version
14
+ # aid status Show AID state of the current project
15
+ # aid add <tool>[,...] Add tool(s) to the current project
16
+ # aid update [<tool>... | self] Update to latest; no arg = all tools; 'self' = the aid CLI
17
+ # aid remove [<tool>... | self] Remove; no arg = ALL AID from project; 'self' = the aid CLI
18
+ # aid <command> -h | --help Per-command help
19
+ #
20
+ # Flags (shared across subcommands where applicable):
21
+ # --from-bundle <path> Offline install from a pre-downloaded tarball / dir.
22
+ # --version <v> Pin to a specific release version (e.g. 0.7.0).
23
+ # --force Overwrite differing files / skip confirmation prompts.
24
+ # --target <dir> Project root (default: current directory).
25
+ # --verbose Print per-file detail (default: concise summary).
26
+ # --no-path (bootstrap / update self only) Skip PATH wiring.
27
+
28
+ set -uo pipefail
29
+
30
+ # ---------------------------------------------------------------------------
31
+ # Bootstrap URL - single place to update when the branch merges to master.
32
+ # Override with AID_INSTALL_URL env var for tests.
33
+ # ---------------------------------------------------------------------------
34
+ AID_INSTALL_URL="${AID_INSTALL_URL:-https://raw.githubusercontent.com/AndreVianna/aid-methodology/worktree-work-002-auto-installer/install.sh}"
35
+
36
+ # ---------------------------------------------------------------------------
37
+ # Locate $AID_HOME. The installed dispatcher lives at $AID_HOME/bin/aid.
38
+ # When invoked directly (not via PATH), BASH_SOURCE[0] is the absolute path.
39
+ # ---------------------------------------------------------------------------
40
+ _AID_SELF="${BASH_SOURCE[0]:-}"
41
+ if [[ -n "$_AID_SELF" && -f "$_AID_SELF" ]]; then
42
+ # Resolve symlinks so we get the real directory.
43
+ _AID_SELF_REAL="$(cd "$(dirname "$_AID_SELF")" && pwd -P)/$(basename "$_AID_SELF")"
44
+ AID_HOME="${AID_HOME:-$(dirname "$(dirname "$_AID_SELF_REAL")")}"
45
+ else
46
+ AID_HOME="${AID_HOME:-${HOME}/.aid}"
47
+ fi
48
+
49
+ # ---------------------------------------------------------------------------
50
+ # Source the shared install core from $AID_HOME/lib/.
51
+ # ---------------------------------------------------------------------------
52
+ _AID_CORE="${AID_HOME}/lib/aid-install-core.sh"
53
+ if [[ ! -f "$_AID_CORE" ]]; then
54
+ echo "ERROR: aid: install core not found at ${_AID_CORE}. Re-run the AID bootstrap to repair." >&2
55
+ exit 1
56
+ fi
57
+ # shellcheck source=../lib/aid-install-core.sh
58
+ source "$_AID_CORE"
59
+
60
+ # Defensive guard: verify the required core function was defined by the sourced lib.
61
+ # This catches an upgrade that left a stale aid-install-core.sh (missing new functions).
62
+ if ! declare -F aid_status_body >/dev/null 2>&1; then
63
+ echo "ERROR: aid: CLI core is stale or incomplete at ${_AID_CORE}. Re-run the installer (or 'aid update self')." >&2
64
+ exit 1
65
+ fi
66
+
67
+ # ---------------------------------------------------------------------------
68
+ # Usage helper.
69
+ # ---------------------------------------------------------------------------
70
+ _aid_usage() {
71
+ local sub="${1:-}"
72
+ case "$sub" in
73
+ status)
74
+ printf 'aid status [--verbose] [--target <dir>]\n'
75
+ printf ' Show AID state of the current project (default: cwd).\n'
76
+ printf ' Exit 7 when no AID install is found.\n'
77
+ ;;
78
+ add)
79
+ printf 'aid add <tool>[,<tool>...] [--version <v>] [--from-bundle <path>]\n'
80
+ printf ' [--force] [--verbose] [--target <dir>]\n'
81
+ printf ' Add tool(s) to the current project.\n'
82
+ printf ' Tools: claude-code, codex, cursor, copilot-cli, antigravity\n'
83
+ ;;
84
+ remove)
85
+ printf 'aid remove [<tool>[,<tool>...] | self] [--force] [--verbose] [--target <dir>]\n'
86
+ printf ' Remove tool(s) from the current project (manifest-driven).\n'
87
+ printf ' No args: remove ALL AID from the project (asks for confirmation).\n'
88
+ printf ' self: remove the aid CLI itself (asks for confirmation).\n'
89
+ ;;
90
+ update)
91
+ printf 'aid update [<tool>... | self] [--version <v>] [--from-bundle <path>]\n'
92
+ printf ' [--force] [--verbose] [--target <dir>]\n'
93
+ printf ' Update to latest. No args: update all installed tools.\n'
94
+ printf ' self: update the aid CLI itself.\n'
95
+ ;;
96
+ version)
97
+ printf 'aid version\n'
98
+ printf ' Print the installed aid CLI version and exit 0.\n'
99
+ ;;
100
+ *)
101
+ printf 'aid - AID CLI\n'
102
+ printf '\n'
103
+ printf 'Usage:\n'
104
+ printf ' aid Show the dashboard\n'
105
+ printf ' aid -h | --help Show this help\n'
106
+ printf ' aid version Print the CLI version\n'
107
+ printf ' aid status Show AID state of the current project\n'
108
+ printf ' aid add <tool>[,...] Add tool(s) to the current project\n'
109
+ printf ' aid update [<tool>... | self] Update to latest; no arg = all tools\n'
110
+ printf ' aid remove [<tool>... | self] Remove; no arg = ALL AID from project\n'
111
+ printf ' aid <command> -h | --help Per-command help\n'
112
+ printf '\n'
113
+ printf 'Flags: --from-bundle, --version, --force, --target, --verbose\n'
114
+ printf "Run 'aid <command> -h' for details.\n"
115
+ ;;
116
+ esac
117
+ }
118
+
119
+ # ---------------------------------------------------------------------------
120
+ # Error helper.
121
+ # ---------------------------------------------------------------------------
122
+ _aid_die() {
123
+ echo "ERROR: aid: $1" >&2
124
+ exit "${2:-1}"
125
+ }
126
+
127
+ # ---------------------------------------------------------------------------
128
+ # Locate the bootstrap install.sh to delegate add/remove/update.
129
+ # Prefers the sibling ../install.sh (if aid is run from the release tree),
130
+ # then a resolved bootstrap relative to AID_HOME.
131
+ # ---------------------------------------------------------------------------
132
+ _find_install_sh() {
133
+ # Sibling of the bin/ dir: AID_HOME/../install.sh would be the release root.
134
+ # But installed layout is: AID_HOME/bin/aid + AID_HOME/lib/aid-install-core.sh
135
+ # The install.sh is NOT shipped inside AID_HOME - we use the core functions directly.
136
+ # Return empty string - callers will use the engine functions directly.
137
+ echo ""
138
+ }
139
+
140
+ # ---------------------------------------------------------------------------
141
+ # Update check (throttled, cached, non-blocking, opt-out).
142
+ # ---------------------------------------------------------------------------
143
+
144
+ # _aid_check_update
145
+ # Compares the installed CLI version ($AID_HOME/VERSION) against the latest
146
+ # GitHub release. Prints ONE notice line when a newer version is available.
147
+ # Fail-silent: any error (no curl, network down, bad JSON) is suppressed.
148
+ # Throttle: re-fetches at most once per 24h; caches result in $AID_HOME/.update-check.
149
+ # Opt-out: AID_NO_UPDATE_CHECK=1 -> skip entirely.
150
+ # Test hook: AID_UPDATE_CHECK_URL overrides the fetch URL (and bypasses throttle).
151
+ _aid_check_update() {
152
+ # Opt-out.
153
+ [[ "${AID_NO_UPDATE_CHECK:-0}" == "1" ]] && return 0
154
+
155
+ # Read installed version.
156
+ local installed_version=""
157
+ local ver_file="${AID_HOME}/VERSION"
158
+ if [[ -f "$ver_file" ]]; then
159
+ installed_version="$(tr -d '[:space:]' < "$ver_file")"
160
+ fi
161
+ [[ -z "$installed_version" ]] && return 0
162
+
163
+ local cache_file="${AID_HOME}/.update-check"
164
+ local now
165
+ now="$(date +%s 2>/dev/null)" || return 0
166
+ local throttle_secs=86400 # 24 hours
167
+
168
+ # Determine the fetch URL (test override or real GitHub API).
169
+ local check_url="${AID_UPDATE_CHECK_URL:-}"
170
+ local use_throttle=1
171
+ if [[ -n "$check_url" ]]; then
172
+ # Test override: bypass throttle so tests run on first invocation.
173
+ use_throttle=0
174
+ else
175
+ check_url="${AID_API_BASE}/releases/latest"
176
+ fi
177
+
178
+ # Try to read cache.
179
+ local cached_ts=0
180
+ local cached_latest=""
181
+ if [[ -f "$cache_file" ]]; then
182
+ cached_ts="$(awk 'NR==1{print $1}' "$cache_file" 2>/dev/null)" || cached_ts=0
183
+ cached_latest="$(awk 'NR==2{print $1}' "$cache_file" 2>/dev/null)" || cached_latest=""
184
+ fi
185
+
186
+ # Decide whether to fetch.
187
+ local latest_version=""
188
+ local need_fetch=1
189
+ if [[ "$use_throttle" -eq 1 && -n "$cached_latest" ]]; then
190
+ local age=$(( now - ${cached_ts:-0} ))
191
+ if [[ "$age" -lt "$throttle_secs" ]]; then
192
+ need_fetch=0
193
+ latest_version="$cached_latest"
194
+ fi
195
+ fi
196
+
197
+ if [[ "$need_fetch" -eq 1 ]]; then
198
+ # Fetch latest release tag - hard 2s timeout, fail-silent.
199
+ local response=""
200
+ if command -v curl >/dev/null 2>&1; then
201
+ response="$(curl --max-time 2 -fsS "$check_url" 2>/dev/null)" || return 0
202
+ else
203
+ return 0
204
+ fi
205
+
206
+ # Parse tag_name; strip leading 'v'.
207
+ local tag
208
+ tag="$(printf '%s' "$response" | grep '"tag_name"' | head -1 | \
209
+ sed 's/.*"tag_name"[[:space:]]*:[[:space:]]*"\([^"]*\)".*/\1/')" || return 0
210
+ [[ -z "$tag" ]] && return 0
211
+ latest_version="${tag#v}"
212
+
213
+ # Update cache.
214
+ printf '%s\n%s\n' "$now" "$latest_version" > "$cache_file" 2>/dev/null || true
215
+ fi
216
+
217
+ [[ -z "$latest_version" ]] && return 0
218
+
219
+ # Compare: show notice only when latest > installed.
220
+ if _semver_lt "$installed_version" "$latest_version"; then
221
+ local _update_cmd
222
+ case "${AID_INSTALL_CHANNEL:-}" in
223
+ npm) _update_cmd="npm i -g aid-installer@latest" ;;
224
+ pypi) _update_cmd="pipx upgrade aid-installer (or: pip install --user -U aid-installer)" ;;
225
+ *) _update_cmd="aid update self" ;;
226
+ esac
227
+ printf 'A newer aid CLI is available: v%s (you have v%s). Run: %s\n' \
228
+ "$latest_version" "$installed_version" "$_update_cmd"
229
+ fi
230
+ return 0
231
+ }
232
+
233
+ # ---------------------------------------------------------------------------
234
+ # update self command (formerly self-update).
235
+ # ---------------------------------------------------------------------------
236
+ _cmd_update_self() {
237
+ # AID_INSTALL_CHANNEL guard: npm/pypi channels print a package-manager hint
238
+ # and exit 0 instead of re-bootstrapping (which would overwrite the channel).
239
+ case "${AID_INSTALL_CHANNEL:-}" in
240
+ npm)
241
+ printf 'Updating the aid CLI: run npm i -g aid-installer@latest\n'
242
+ return 0
243
+ ;;
244
+ pypi)
245
+ printf 'Updating the aid CLI: run pipx upgrade aid-installer (or: pip install --user -U aid-installer)\n'
246
+ return 0
247
+ ;;
248
+ esac
249
+ printf 'Updating the aid CLI...\n'
250
+ if command -v curl >/dev/null 2>&1; then
251
+ curl -fsSL "${AID_INSTALL_URL}" | bash
252
+ return $?
253
+ else
254
+ echo "ERROR: aid: curl not found; cannot update self" >&2
255
+ return 3
256
+ fi
257
+ }
258
+
259
+ # ---------------------------------------------------------------------------
260
+ # Path-wiring helpers (Unix).
261
+ # ---------------------------------------------------------------------------
262
+
263
+ # _wire_one_profile <bin_dir> <profile_file>
264
+ # Idempotently write the fenced PATH block into a single profile file.
265
+ _wire_one_profile() {
266
+ local bin_dir="$1"
267
+ local profile="$2"
268
+
269
+ # Create the profile file if it doesn't exist.
270
+ if [[ ! -f "$profile" ]]; then
271
+ touch "$profile" 2>/dev/null || {
272
+ echo "WARN: aid: could not create ${profile}; PATH not wired." >&2
273
+ printf 'Add "%s" to your PATH manually.\n' "$bin_dir"
274
+ return 0
275
+ }
276
+ fi
277
+
278
+ local fence_start='# >>> aid CLI >>>'
279
+ local fence_end='# <<< aid CLI <<<'
280
+ # Duplicate-guarded export: safe when multiple rc files are sourced.
281
+ local path_line="case \":\$PATH:\" in *\":${bin_dir}:\"*) ;; *) export PATH=\"${bin_dir}:\$PATH\" ;; esac"
282
+
283
+ if grep -qF "$fence_start" "$profile" 2>/dev/null; then
284
+ # Replace the existing block in-place.
285
+ local tmp_profile
286
+ tmp_profile="$(mktemp "${profile}.aid-tmp.XXXXXX")"
287
+ awk -v fs="$fence_start" -v fe="$fence_end" -v pl="$path_line" '
288
+ BEGIN { skip=0 }
289
+ $0 == fs { skip=1; print fs; print pl; print fe; next }
290
+ skip && $0 == fe { skip=0; next }
291
+ skip { next }
292
+ { print }
293
+ ' "$profile" > "$tmp_profile"
294
+ mv "$tmp_profile" "$profile"
295
+ echo "PATH wiring updated in ${profile}."
296
+ else
297
+ # Append the block.
298
+ printf '\n%s\n%s\n%s\n' "$fence_start" "$path_line" "$fence_end" >> "$profile"
299
+ echo "PATH wiring added to ${profile}."
300
+ fi
301
+ }
302
+
303
+ # _wire_path_unix <aid_bin_dir> [--no-path] [--profile-file <file>]
304
+ # Idempotently add $aid_bin_dir to PATH via a fenced block.
305
+ # Without --profile-file, wires ALL standard rc files that exist (rustup/nvm pattern).
306
+ # When --no-path is given, print the manual instruction and return.
307
+ _wire_path_unix() {
308
+ local bin_dir="$1"
309
+ local no_path=0
310
+ local profile_override=""
311
+
312
+ shift
313
+ while [[ $# -gt 0 ]]; do
314
+ case "$1" in
315
+ --no-path) no_path=1; shift ;;
316
+ --profile-file) profile_override="$2"; shift 2 ;;
317
+ *) shift ;;
318
+ esac
319
+ done
320
+
321
+ if [[ "$no_path" -eq 1 ]]; then
322
+ printf 'Add "%s" to your PATH manually.\n' "$bin_dir"
323
+ return 0
324
+ fi
325
+
326
+ if [[ -n "$profile_override" ]]; then
327
+ _wire_one_profile "$bin_dir" "$profile_override"
328
+ echo "Open a new shell, or run: export PATH=\"${bin_dir}:\$PATH\" (or: source ${profile_override})"
329
+ return 0
330
+ fi
331
+
332
+ # Wire every standard rc file that already exists.
333
+ local _wp_candidates=(
334
+ "${ZDOTDIR:-${HOME}}/.zshrc"
335
+ "${HOME}/.bashrc"
336
+ "${HOME}/.bash_profile"
337
+ "${HOME}/.profile"
338
+ )
339
+ local _wp_wired=()
340
+ local _wp_rc
341
+ for _wp_rc in "${_wp_candidates[@]}"; do
342
+ if [[ -f "$_wp_rc" ]]; then
343
+ _wire_one_profile "$bin_dir" "$_wp_rc"
344
+ _wp_wired+=("$_wp_rc")
345
+ fi
346
+ done
347
+ # If none exist, create and wire ~/.profile.
348
+ if [[ "${#_wp_wired[@]}" -eq 0 ]]; then
349
+ _wire_one_profile "$bin_dir" "${HOME}/.profile"
350
+ _wp_wired+=("${HOME}/.profile")
351
+ fi
352
+ # Summarise.
353
+ local _wp_display=""
354
+ local _wp_w
355
+ for _wp_w in "${_wp_wired[@]}"; do
356
+ local _wp_rel="${_wp_w/#${HOME}/~}"
357
+ _wp_display="${_wp_display:+${_wp_display}, }${_wp_rel}"
358
+ done
359
+ echo "PATH wiring added to: ${_wp_display}"
360
+ echo "Open a new shell to pick up the updated PATH."
361
+ }
362
+
363
+ # _unwire_path_unix [--profile-file <file>]
364
+ # Remove the fenced PATH block from all standard rc files (or a single explicit file).
365
+ _unwire_path_unix() {
366
+ local profile_override=""
367
+ while [[ $# -gt 0 ]]; do
368
+ case "$1" in
369
+ --profile-file) profile_override="$2"; shift 2 ;;
370
+ *) shift ;;
371
+ esac
372
+ done
373
+
374
+ local fence_start='# >>> aid CLI >>>'
375
+
376
+ _unwire_one() {
377
+ local _uw_f="$1"
378
+ if [[ ! -f "$_uw_f" ]]; then
379
+ return 0
380
+ fi
381
+ if ! grep -qF "$fence_start" "$_uw_f" 2>/dev/null; then
382
+ return 0
383
+ fi
384
+ local tmp_profile
385
+ tmp_profile="$(mktemp "${_uw_f}.aid-tmp.XXXXXX")"
386
+ awk -v start="$fence_start" -v end='# <<< aid CLI <<<' '
387
+ BEGIN { skip=0 }
388
+ $0 == start { skip=1; next }
389
+ skip && $0 == end { skip=0; next }
390
+ skip { next }
391
+ { print }
392
+ ' "$_uw_f" > "$tmp_profile"
393
+ mv "$tmp_profile" "$_uw_f"
394
+ echo "PATH wiring removed from ${_uw_f}."
395
+ }
396
+
397
+ if [[ -n "$profile_override" ]]; then
398
+ _unwire_one "$profile_override"
399
+ return 0
400
+ fi
401
+
402
+ # Remove from all standard rc files.
403
+ local _uw_rc
404
+ for _uw_rc in \
405
+ "${ZDOTDIR:-${HOME}}/.zshrc" \
406
+ "${HOME}/.bashrc" \
407
+ "${HOME}/.bash_profile" \
408
+ "${HOME}/.profile"
409
+ do
410
+ _unwire_one "$_uw_rc"
411
+ done
412
+ }
413
+
414
+ # ---------------------------------------------------------------------------
415
+ # Global CLI install helpers.
416
+ # ---------------------------------------------------------------------------
417
+
418
+ # _install_global_cli <version> <src_bin_aid> <src_lib_core>
419
+ # Stage then atomic-move into $AID_HOME.
420
+ _install_global_cli() {
421
+ local version="$1"
422
+ local src_bin_aid="$2"
423
+ local src_lib_core="$3"
424
+
425
+ local aid_home="${AID_HOME:-${HOME}/.aid}"
426
+ local bin_dir="${aid_home}/bin"
427
+ local lib_dir="${aid_home}/lib"
428
+
429
+ mkdir -p "$bin_dir" "$lib_dir"
430
+
431
+ # Copy the dispatcher.
432
+ cp "$src_bin_aid" "${bin_dir}/aid"
433
+ chmod +x "${bin_dir}/aid"
434
+
435
+ # Copy the core lib.
436
+ cp "$src_lib_core" "${lib_dir}/aid-install-core.sh"
437
+
438
+ # Write the VERSION file.
439
+ printf '%s\n' "$version" > "${aid_home}/VERSION"
440
+
441
+ echo "aid CLI v${version} installed to ${aid_home}."
442
+ }
443
+
444
+ # ---------------------------------------------------------------------------
445
+ # remove self (formerly self-uninstall).
446
+ # ---------------------------------------------------------------------------
447
+
448
+ _cmd_remove_self() {
449
+ local force=0
450
+ local no_path=0
451
+ local profile_file=""
452
+
453
+ while [[ $# -gt 0 ]]; do
454
+ case "$1" in
455
+ --force|-y) force=1; shift ;;
456
+ --no-path) no_path=1; shift ;;
457
+ --profile-file) profile_file="$2"; shift 2 ;;
458
+ -h|--help) _aid_usage remove; exit 0 ;;
459
+ *) _aid_die "unknown flag for 'remove self': $1" 2 ;;
460
+ esac
461
+ done
462
+
463
+ # Apply AID_FORCE env-var fallback.
464
+ if [[ "$force" -eq 0 && ( "${AID_FORCE:-0}" == "1" || "${AID_FORCE:-0}" == "true" ) ]]; then
465
+ force=1
466
+ fi
467
+
468
+ local aid_home="${AID_HOME:-${HOME}/.aid}"
469
+
470
+ if [[ "$force" -eq 0 ]]; then
471
+ # Skip prompt when non-interactive (piped or no tty).
472
+ if [[ ! -t 0 ]]; then
473
+ force=1
474
+ else
475
+ printf 'Remove the aid CLI from %s? [y/N] ' "$aid_home"
476
+ local answer
477
+ if [[ -e /dev/tty ]]; then
478
+ read -r answer < /dev/tty
479
+ else
480
+ read -r answer
481
+ fi
482
+ if [[ "$answer" != "y" && "$answer" != "Y" && "$answer" != "yes" && "$answer" != "YES" ]]; then
483
+ echo "Aborted."
484
+ exit 0
485
+ fi
486
+ fi
487
+ fi
488
+
489
+ local partial=0
490
+
491
+ # Remove PATH wiring first (while we still have the profile-detect logic).
492
+ if [[ "$no_path" -eq 0 ]]; then
493
+ if [[ -n "$profile_file" ]]; then
494
+ _unwire_path_unix --profile-file "$profile_file" || partial=1
495
+ else
496
+ _unwire_path_unix || partial=1
497
+ fi
498
+ fi
499
+
500
+ # Remove $AID_HOME.
501
+ if [[ -d "$aid_home" ]]; then
502
+ rm -rf "$aid_home" || {
503
+ echo "ERROR: aid: failed to remove ${aid_home}" >&2
504
+ partial=1
505
+ }
506
+ fi
507
+
508
+ if [[ "$partial" -eq 1 ]]; then
509
+ echo "aid CLI partially removed. Check the messages above for what remained."
510
+ exit 1
511
+ fi
512
+
513
+ echo "aid CLI removed. Per-project AID installs are unaffected; run 'aid remove' in a project before removing the CLI if you also want to remove those."
514
+ exit 0
515
+ }
516
+
517
+ # ---------------------------------------------------------------------------
518
+ # Parse subcommand and dispatch.
519
+ # ---------------------------------------------------------------------------
520
+
521
+ # Shared flag buckets (populated during subcommand-specific arg parsing).
522
+ _AID_TOOL_ARG=""
523
+ _AID_VERSION_ARG=""
524
+ _AID_FROM_BUNDLE=""
525
+ _AID_FORCE=0
526
+ _AID_TARGET=""
527
+ _AID_VERBOSE="${AID_VERBOSE:-0}"
528
+ _AID_NO_PATH=0
529
+
530
+ # ---------------------------------------------------------------------------
531
+ # Dashboard (bare 'aid' - no arguments).
532
+ # ---------------------------------------------------------------------------
533
+ _cmd_dashboard() {
534
+ # Block 1 + 2: Header + description.
535
+ local cli_version="unknown"
536
+ local ver_file="${AID_HOME}/VERSION"
537
+ if [[ -f "$ver_file" ]]; then
538
+ cli_version="$(tr -d '[:space:]' < "$ver_file")"
539
+ fi
540
+ printf 'AID v%s - Agentic Iterative Development\n' "$cli_version"
541
+ printf 'Install, update, and manage AID across your repositories.\n'
542
+
543
+ # Block 3: Installed tools for cwd.
544
+ printf '\n'
545
+ aid_status_body "."
546
+
547
+ # Block 4: Usage/help.
548
+ printf '\n'
549
+ _aid_usage
550
+
551
+ # Block 5: Update check notice (final line, non-blocking).
552
+ _aid_check_update
553
+ }
554
+
555
+ # Early help check.
556
+ if [[ $# -eq 0 ]]; then
557
+ # Bare 'aid' -> dashboard landing screen.
558
+ _cmd_dashboard
559
+ exit 0
560
+ fi
561
+
562
+ case "$1" in
563
+ -h|--help)
564
+ _aid_usage
565
+ exit 0
566
+ ;;
567
+ esac
568
+
569
+ SUBCMD="$1"
570
+ shift
571
+
572
+ # ---- version ----------------------------------------------------------------
573
+ if [[ "$SUBCMD" == "version" ]]; then
574
+ local_version_file="${AID_HOME}/VERSION"
575
+ if [[ -f "$local_version_file" ]]; then
576
+ cat "$local_version_file"
577
+ else
578
+ echo "unknown (VERSION file not found at ${local_version_file})"
579
+ fi
580
+ exit 0
581
+ fi
582
+
583
+ # ---- help -------------------------------------------------------------------
584
+ if [[ "$SUBCMD" == "help" || "$SUBCMD" == "-h" || "$SUBCMD" == "--help" ]]; then
585
+ _aid_usage
586
+ exit 0
587
+ fi
588
+
589
+ # ---- status -----------------------------------------------------------------
590
+ if [[ "$SUBCMD" == "status" ]]; then
591
+ # Parse flags for status.
592
+ while [[ $# -gt 0 ]]; do
593
+ case "$1" in
594
+ --target)
595
+ [[ $# -lt 2 ]] && _aid_die "--target requires a value" 2
596
+ _AID_TARGET="$2"; shift 2 ;;
597
+ --verbose) _AID_VERBOSE=1; shift ;;
598
+ -h|--help) _aid_usage status; exit 0 ;;
599
+ -*) _aid_die "unknown flag for status: $1" 2 ;;
600
+ *) _aid_die "unexpected argument for status: $1" 2 ;;
601
+ esac
602
+ done
603
+ # Apply env-var fallbacks.
604
+ [[ -z "$_AID_TARGET" && -n "${AID_TARGET:-}" ]] && _AID_TARGET="$AID_TARGET"
605
+ _AID_TARGET="${_AID_TARGET:-.}"
606
+ export AID_VERBOSE="$_AID_VERBOSE"
607
+ aid_status "$_AID_TARGET"
608
+ _status_rc=$?
609
+ # Update check notice appended after status output (non-blocking).
610
+ _aid_check_update
611
+ exit $_status_rc
612
+ fi
613
+
614
+ # ---- update -----------------------------------------------------------------
615
+ if [[ "$SUBCMD" == "update" ]]; then
616
+ # Check for 'update self' as first positional arg.
617
+ if [[ $# -gt 0 && "$1" == "self" ]]; then
618
+ shift
619
+ # Consume any flags after 'self'.
620
+ while [[ $# -gt 0 ]]; do
621
+ case "$1" in
622
+ --force|-y) shift ;; # no-op for update self
623
+ -h|--help) _aid_usage update; exit 0 ;;
624
+ *) _aid_die "unknown flag for 'update self': $1" 2 ;;
625
+ esac
626
+ done
627
+ _cmd_update_self
628
+ exit $?
629
+ fi
630
+ # Fall through to the shared add/update handler below.
631
+ fi
632
+
633
+ # ---- remove -----------------------------------------------------------------
634
+ if [[ "$SUBCMD" == "remove" ]]; then
635
+ # Check for 'remove self' as first positional arg.
636
+ if [[ $# -gt 0 && "$1" == "self" ]]; then
637
+ shift
638
+ _cmd_remove_self "$@"
639
+ # _cmd_remove_self always exits.
640
+ fi
641
+
642
+ # Check for 'remove' with no tool args (remove ALL from project).
643
+ # We do this check after parsing flags below, so continue to parse first.
644
+ fi
645
+
646
+ # ---- add / remove / update --------------------------------------------------
647
+ # These subcommands all share flag parsing; we then call the engine functions
648
+ # (install_tool / uninstall_tool) directly through a per-tool loop, exactly as
649
+ # install.sh does. We build a temporary staging area for install/update, and
650
+ # reuse the same prepare_tool_staging + install_tool / uninstall_tool pattern.
651
+
652
+ # First, validate the subcommand.
653
+ case "$SUBCMD" in
654
+ add|remove|update) ;;
655
+ *)
656
+ echo "ERROR: aid: unknown command: ${SUBCMD} (see 'aid -h')" >&2
657
+ exit 2
658
+ ;;
659
+ esac
660
+
661
+
662
+ # Collect positional tool args (comma-separated or space-separated before flags).
663
+ _AID_POSITIONAL_TOOLS=""
664
+ _AID_REMOVE_FORCE=0
665
+
666
+ while [[ $# -gt 0 ]]; do
667
+ case "$1" in
668
+ --from-bundle)
669
+ [[ $# -lt 2 ]] && _aid_die "--from-bundle requires a value" 2
670
+ _AID_FROM_BUNDLE="$2"; shift 2 ;;
671
+ --version)
672
+ [[ $# -lt 2 ]] && _aid_die "--version requires a value" 2
673
+ _AID_VERSION_ARG="$2"; shift 2 ;;
674
+ --force|-y) _AID_FORCE=1; _AID_REMOVE_FORCE=1; shift ;;
675
+ --verbose) _AID_VERBOSE=1; shift ;;
676
+ --target)
677
+ [[ $# -lt 2 ]] && _aid_die "--target requires a value" 2
678
+ _AID_TARGET="$2"; shift 2 ;;
679
+ --no-path) _AID_NO_PATH=1; shift ;;
680
+ -h|--help) _aid_usage "$SUBCMD"; exit 0 ;;
681
+ -*) _aid_die "unknown flag: $1" 2 ;;
682
+ *)
683
+ # Positional arg: tool name(s).
684
+ if [[ -z "$_AID_POSITIONAL_TOOLS" ]]; then
685
+ _AID_POSITIONAL_TOOLS="$1"
686
+ else
687
+ # Additional space-separated tools: append as comma-list.
688
+ _AID_POSITIONAL_TOOLS="${_AID_POSITIONAL_TOOLS},$1"
689
+ fi
690
+ shift ;;
691
+ esac
692
+ done
693
+
694
+ # Apply env-var fallbacks.
695
+ [[ -z "$_AID_TOOL_ARG" && -n "$_AID_POSITIONAL_TOOLS" ]] && _AID_TOOL_ARG="$_AID_POSITIONAL_TOOLS"
696
+ [[ -z "$_AID_TOOL_ARG" && -n "${AID_TOOL:-}" ]] && _AID_TOOL_ARG="$AID_TOOL"
697
+ [[ -z "$_AID_VERSION_ARG" && -n "${AID_VERSION:-}" ]] && _AID_VERSION_ARG="$AID_VERSION"
698
+ [[ -z "$_AID_TARGET" && -n "${AID_TARGET:-}" ]] && _AID_TARGET="$AID_TARGET"
699
+ if [[ "$_AID_FORCE" -eq 0 && ( "${AID_FORCE:-0}" == "1" || "${AID_FORCE:-0}" == "true" ) ]]; then
700
+ _AID_FORCE=1
701
+ _AID_REMOVE_FORCE=1
702
+ fi
703
+ export AID_VERBOSE="$_AID_VERBOSE"
704
+
705
+ _AID_TARGET="${_AID_TARGET:-.}"
706
+ # Validate target dir.
707
+ if [[ ! -d "$_AID_TARGET" ]]; then
708
+ _aid_die "target directory does not exist: ${_AID_TARGET}" 2
709
+ fi
710
+ _AID_TARGET="$(cd "$_AID_TARGET" && pwd)"
711
+
712
+ # Strip leading 'v' from version.
713
+ _AID_VERSION_ARG="${_AID_VERSION_ARG#v}"
714
+
715
+ # --from-bundle and --version are mutually exclusive.
716
+ if [[ -n "$_AID_FROM_BUNDLE" && -n "$_AID_VERSION_ARG" ]]; then
717
+ _aid_die "--from-bundle and --version are mutually exclusive" 2
718
+ fi
719
+
720
+ # For 'remove' with no tool arg: confirm, then remove all.
721
+ if [[ "$SUBCMD" == "remove" && -z "$_AID_TOOL_ARG" ]]; then
722
+ # Confirmation required (unless --force or non-interactive).
723
+ if [[ "$_AID_REMOVE_FORCE" -eq 0 ]]; then
724
+ if [[ ! -t 0 ]]; then
725
+ # Non-interactive: auto-proceed (don't hang CI).
726
+ _AID_REMOVE_FORCE=1
727
+ else
728
+ printf 'Remove ALL AID from %s? [y/N] ' "$_AID_TARGET"
729
+ _AID_RM_ANSWER=""
730
+ if [[ -e /dev/tty ]]; then
731
+ read -r _AID_RM_ANSWER < /dev/tty
732
+ else
733
+ read -r _AID_RM_ANSWER
734
+ fi
735
+ if [[ "$_AID_RM_ANSWER" != "y" && "$_AID_RM_ANSWER" != "Y" && "$_AID_RM_ANSWER" != "yes" && "$_AID_RM_ANSWER" != "YES" ]]; then
736
+ echo "Aborted."
737
+ exit 0
738
+ fi
739
+ fi
740
+ fi
741
+ # Proceed: fall through to resolve all tools from manifest.
742
+ fi
743
+
744
+ # Validate constraints per subcommand.
745
+ case "$SUBCMD" in
746
+ add)
747
+ # Tool is required (or must be auto-detectable / env-var set).
748
+ : ;; # handled below in _resolve_tools_for_aid
749
+ remove)
750
+ : ;; # tool optional (empty = all installed, confirmed above)
751
+ update)
752
+ : ;; # tool optional (empty = all installed)
753
+ esac
754
+
755
+ # ---------------------------------------------------------------------------
756
+ # Resolve tool list (reuses the same logic as install.sh _resolve_tools).
757
+ # ---------------------------------------------------------------------------
758
+ _AID_MANIFEST="${_AID_TARGET}/.aid/.aid-manifest.json"
759
+
760
+ _resolve_tools_for_aid() {
761
+ local raw="$1" subcmd="$2" outfile="$3"
762
+
763
+ if [[ -z "$raw" ]]; then
764
+ if [[ "$subcmd" == "update" || "$subcmd" == "remove" ]]; then
765
+ # No tool specified -> all tools in manifest.
766
+ if [[ ! -f "$_AID_MANIFEST" ]]; then
767
+ return 0
768
+ fi
769
+ manifest_list_tools "$_AID_MANIFEST" >> "$outfile"
770
+ return 0
771
+ fi
772
+ # auto-detect for 'add'.
773
+ local detected
774
+ detected="$(detect_tool "$_AID_TARGET")"
775
+ local _rc=$?
776
+ if [[ "$_rc" -ne 0 ]]; then
777
+ return "$_rc"
778
+ fi
779
+ echo "$detected" >> "$outfile"
780
+ return 0
781
+ fi
782
+
783
+ # Split on comma.
784
+ local -a raw_tools=()
785
+ IFS=',' read -ra raw_tools <<< "$raw"
786
+ for t in "${raw_tools[@]}"; do
787
+ t="$(echo "$t" | tr -d '[:space:]')"
788
+ local canonical
789
+ canonical="$(normalize_tool "$t")"
790
+ local _rc=$?
791
+ if [[ "$_rc" -ne 0 ]]; then
792
+ return "$_rc"
793
+ fi
794
+ echo "$canonical" >> "$outfile"
795
+ done
796
+ return 0
797
+ }
798
+
799
+ # Set up staging area.
800
+ _AID_STAGING_BASE="$(mktemp -d /tmp/aid-XXXXXX)"
801
+ trap 'rm -rf "$_AID_STAGING_BASE"' EXIT
802
+
803
+ _TOOLS_TMP="$(mktemp "${_AID_STAGING_BASE}/tools.XXXXXX")"
804
+ _resolve_tools_for_aid "$_AID_TOOL_ARG" "$SUBCMD" "$_TOOLS_TMP"
805
+ _RESOLVE_RC=$?
806
+ if [[ "$_RESOLVE_RC" -ne 0 ]]; then
807
+ rm -rf "$_AID_STAGING_BASE"
808
+ exit "$_RESOLVE_RC"
809
+ fi
810
+
811
+ mapfile -t _AID_TOOLS < "$_TOOLS_TMP"
812
+
813
+ if [[ "${#_AID_TOOLS[@]}" -eq 0 ]]; then
814
+ case "$SUBCMD" in
815
+ remove)
816
+ echo "ERROR: aid: no manifest at ${_AID_TARGET}/.aid/.aid-manifest.json (exit 6)" >&2
817
+ rm -rf "$_AID_STAGING_BASE"
818
+ exit 6
819
+ ;;
820
+ update)
821
+ echo "ERROR: aid: no manifest at ${_AID_TARGET}/.aid/.aid-manifest.json; nothing to update (exit 6)" >&2
822
+ rm -rf "$_AID_STAGING_BASE"
823
+ exit 6
824
+ ;;
825
+ add)
826
+ echo "ERROR: aid: cannot auto-detect host tool; pass tool name as argument (e.g. aid add codex)" >&2
827
+ rm -rf "$_AID_STAGING_BASE"
828
+ exit 2
829
+ ;;
830
+ esac
831
+ fi
832
+
833
+ # ---------------------------------------------------------------------------
834
+ # Prepare staging for install/update (mirrors install.sh prepare_tool_staging).
835
+ # ---------------------------------------------------------------------------
836
+ _AID_RESOLVED_VERSION=""
837
+ _AID_STAGING_DIR=""
838
+
839
+ _prepare_tool_staging_aid() {
840
+ local tool="$1" version="$2" from_bundle="$3"
841
+
842
+ local tool_staging
843
+ tool_staging="$(mktemp -d "${_AID_STAGING_BASE}/staging-${tool}-XXXXXX")"
844
+
845
+ if [[ -n "$from_bundle" ]]; then
846
+ local tarball="$from_bundle"
847
+ if [[ -d "$from_bundle" ]]; then
848
+ tarball="$(ls "${from_bundle}"/aid-${tool}-v*.tar.gz 2>/dev/null | head -1)"
849
+ if [[ -z "$tarball" ]]; then
850
+ echo "ERROR: aid: no tarball found for tool '${tool}' in bundle directory: ${from_bundle}" >&2
851
+ exit 1
852
+ fi
853
+ fi
854
+ if [[ ! -f "$tarball" ]]; then
855
+ echo "ERROR: aid: bundle file not found: ${tarball}" >&2
856
+ exit 1
857
+ fi
858
+ verify_bundle_checksum "$tarball" || exit $?
859
+ local tbase
860
+ tbase="$(basename "$tarball")"
861
+ _AID_RESOLVED_VERSION="$(echo "$tbase" | sed "s/aid-${tool}-v//" | sed 's/\.tar\.gz$//')"
862
+ [[ -z "$_AID_RESOLVED_VERSION" ]] && _AID_RESOLVED_VERSION="${version:-unknown}"
863
+ extract_tarball "$tarball" "$tool_staging" || exit $?
864
+ else
865
+ if [[ -z "$version" ]]; then
866
+ _AID_RESOLVED_VERSION="$(resolve_version)" || exit $?
867
+ else
868
+ _AID_RESOLVED_VERSION="$version"
869
+ fi
870
+ local dl_dir
871
+ dl_dir="$(mktemp -d "${_AID_STAGING_BASE}/download-${tool}-XXXXXX")"
872
+ fetch_tarball "$tool" "$_AID_RESOLVED_VERSION" "$dl_dir" || exit $?
873
+ local tarball="${dl_dir}/aid-${tool}-v${_AID_RESOLVED_VERSION}.tar.gz"
874
+ extract_tarball "$tarball" "$tool_staging" || exit $?
875
+ fi
876
+
877
+ _AID_STAGING_DIR="$tool_staging"
878
+ }
879
+
880
+ # ---------------------------------------------------------------------------
881
+ # Dispatch to engine.
882
+ # ---------------------------------------------------------------------------
883
+ _AID_OVERALL_BLOCKED=0
884
+
885
+ case "$SUBCMD" in
886
+ add|update)
887
+ for _tool in "${_AID_TOOLS[@]}"; do
888
+ echo ""
889
+ _prepare_tool_staging_aid "$_tool" "$_AID_VERSION_ARG" "$_AID_FROM_BUNDLE"
890
+ echo "Installing ${_tool} v${_AID_RESOLVED_VERSION} -> ${_AID_TARGET}"
891
+ install_tool "$_AID_STAGING_DIR" "$_tool" "$_AID_TARGET" "$_AID_RESOLVED_VERSION" "$_AID_FORCE" || {
892
+ _RC=$?
893
+ if [[ "$_RC" -eq 5 ]]; then
894
+ _AID_OVERALL_BLOCKED=1
895
+ else
896
+ exit "$_RC"
897
+ fi
898
+ }
899
+ done
900
+
901
+ echo ""
902
+ if [[ "$_AID_OVERALL_BLOCKED" -eq 1 ]]; then
903
+ echo "Install complete with warnings: one or more root agent files were not overwritten."
904
+ echo "Review the *.aid-new file(s) and merge, or re-run with --force to overwrite."
905
+ exit 5
906
+ fi
907
+ echo "Done. AID ${_AID_RESOLVED_VERSION:-} installed into: ${_AID_TARGET}"
908
+ exit 0
909
+ ;;
910
+
911
+ remove)
912
+ manifest_exists "$_AID_MANIFEST" || {
913
+ echo "ERROR: aid: no manifest at ${_AID_TARGET}/.aid/.aid-manifest.json; nothing to uninstall" >&2
914
+ exit 6
915
+ }
916
+
917
+ for _tool in "${_AID_TOOLS[@]}"; do
918
+ echo ""
919
+ echo "Uninstalling ${_tool} from ${_AID_TARGET}"
920
+ uninstall_tool "$_AID_MANIFEST" "$_tool" "$_AID_TARGET" || {
921
+ _RC=$?
922
+ [[ "$_RC" -eq 6 ]] && exit 6
923
+ exit "$_RC"
924
+ }
925
+ done
926
+
927
+ echo ""
928
+ echo "Uninstall complete."
929
+ exit 0
930
+ ;;
931
+ esac