aid-installer 1.0.0 → 1.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +4 -2
- package/VERSION +1 -1
- package/bin/aid +2444 -193
- package/bin/aid.ps1 +2360 -105
- package/dashboard/home.html +3321 -0
- package/dashboard/index.html +987 -0
- package/dashboard/reader/__init__.py +56 -0
- package/dashboard/reader/derivation.py +892 -0
- package/dashboard/reader/locator.py +228 -0
- package/dashboard/reader/models.py +408 -0
- package/dashboard/reader/parsers.py +2105 -0
- package/dashboard/reader/reader.py +1196 -0
- package/dashboard/server/__init__.py +3 -0
- package/dashboard/server/reader.mjs +3699 -0
- package/dashboard/server/server.mjs +780 -0
- package/dashboard/server/server.py +1004 -0
- package/lib/AidInstallCore.psm1 +446 -43
- package/lib/aid-install-core.sh +405 -48
- package/package.json +5 -2
- package/scripts/postinstall.js +106 -0
- package/scripts/vendor.js +98 -0
package/bin/aid
CHANGED
|
@@ -2,9 +2,9 @@
|
|
|
2
2
|
# aid - AID CLI dispatcher (Bash side).
|
|
3
3
|
#
|
|
4
4
|
# Purpose:
|
|
5
|
-
# Persistent global command installed at $
|
|
5
|
+
# Persistent global command installed at $AID_CODE_HOME/bin/aid. Parses
|
|
6
6
|
# subcommands and dispatches to the shared install-core engine located at
|
|
7
|
-
# $
|
|
7
|
+
# $AID_CODE_HOME/lib/aid-install-core.sh. Operates on the current working
|
|
8
8
|
# directory (--target / AID_TARGET overrides).
|
|
9
9
|
#
|
|
10
10
|
# Usage:
|
|
@@ -15,6 +15,8 @@
|
|
|
15
15
|
# aid add <tool>[,...] Add tool(s) to the current project
|
|
16
16
|
# aid update [<tool>... | self] Update to latest; no arg = all tools; 'self' = the aid CLI
|
|
17
17
|
# aid remove [<tool>... | self] Remove; no arg = ALL AID from project; 'self' = the aid CLI
|
|
18
|
+
# aid projects [list|add|remove|help] [path] [--local|--shared] [--verbose]
|
|
19
|
+
# List, register, or unregister AID projects
|
|
18
20
|
# aid <command> -h | --help Per-command help
|
|
19
21
|
#
|
|
20
22
|
# Flags (shared across subcommands where applicable):
|
|
@@ -34,24 +36,74 @@ set -uo pipefail
|
|
|
34
36
|
AID_INSTALL_URL="${AID_INSTALL_URL:-https://raw.githubusercontent.com/AndreVianna/aid-methodology/master/install.sh}"
|
|
35
37
|
|
|
36
38
|
# ---------------------------------------------------------------------------
|
|
37
|
-
#
|
|
38
|
-
#
|
|
39
|
+
# AID_CODE_HOME: self-locate the read-only code payload (parent of bin/).
|
|
40
|
+
# NEVER overridden by an env var. Error-out if unresolvable (Q1 fail-safe).
|
|
39
41
|
# ---------------------------------------------------------------------------
|
|
40
42
|
_AID_SELF="${BASH_SOURCE[0]:-}"
|
|
41
43
|
if [[ -n "$_AID_SELF" && -f "$_AID_SELF" ]]; then
|
|
42
|
-
# Resolve symlinks so we get the real directory.
|
|
44
|
+
# Resolve symlinks so we get the real payload root directory.
|
|
43
45
|
_AID_SELF_REAL="$(cd "$(dirname "$_AID_SELF")" && pwd -P)/$(basename "$_AID_SELF")"
|
|
44
|
-
|
|
46
|
+
AID_CODE_HOME="$(dirname "$(dirname "$_AID_SELF_REAL")")"
|
|
45
47
|
else
|
|
46
|
-
|
|
48
|
+
echo "ERROR: aid: cannot locate the AID code payload (AID_CODE_HOME unresolved). Re-run the AID bootstrap to repair." >&2
|
|
49
|
+
exit 1
|
|
50
|
+
fi
|
|
51
|
+
|
|
52
|
+
# ---------------------------------------------------------------------------
|
|
53
|
+
# Scope derivation: global iff AID_CODE_HOME is not writable by the current
|
|
54
|
+
# user. Reuses the _aid_priv_run writability approach (no second test).
|
|
55
|
+
# AID_STATE_HOME: mutable state home, env-overridable via AID_HOME.
|
|
56
|
+
# ---------------------------------------------------------------------------
|
|
57
|
+
if [[ -n "$AID_CODE_HOME" && -e "$AID_CODE_HOME" && ! -w "$AID_CODE_HOME" && "$(id -u)" -ne 0 ]]; then
|
|
58
|
+
_AID_SCOPE="global"
|
|
59
|
+
AID_STATE_HOME="${AID_HOME:-${AID_SHARED_STATE_HOME:-/var/lib/aid}}"
|
|
60
|
+
else
|
|
61
|
+
_AID_SCOPE="user"
|
|
62
|
+
AID_STATE_HOME="${AID_HOME:-${HOME}/.aid}"
|
|
47
63
|
fi
|
|
48
64
|
|
|
49
65
|
# ---------------------------------------------------------------------------
|
|
50
|
-
#
|
|
66
|
+
# _aid_is_project_dir <dir>
|
|
67
|
+
# Return 0 (true) iff <dir> has a .aid/ subdirectory AND that subdirectory is
|
|
68
|
+
# NOT the CLI state home. Treats the CLI state home as a non-project dir so
|
|
69
|
+
# running 'aid' from $HOME (or any dir whose .aid/ == AID_STATE_HOME) does not
|
|
70
|
+
# falsely auto-register or trigger the format gate.
|
|
71
|
+
#
|
|
72
|
+
# Guard: resolves <dir>/.aid to a real path (tolerates non-existent) and
|
|
73
|
+
# compares against realpath($AID_STATE_HOME) and realpath($HOME/.aid).
|
|
74
|
+
# ASCII-safe: uses bash built-ins only.
|
|
75
|
+
# ---------------------------------------------------------------------------
|
|
76
|
+
_aid_is_project_dir() {
|
|
77
|
+
local _dir="$1"
|
|
78
|
+
# Fast out: no .aid/ subdirectory at all.
|
|
79
|
+
[[ -d "${_dir}/.aid" ]] || return 1
|
|
80
|
+
# Resolve to canonical real path, tolerating paths that may not fully exist.
|
|
81
|
+
local _aid_real
|
|
82
|
+
_aid_real="$(cd "${_dir}/.aid" 2>/dev/null && pwd -P)" || _aid_real="${_dir}/.aid"
|
|
83
|
+
# Resolve the two state-home paths.
|
|
84
|
+
local _sh_real _hd_real
|
|
85
|
+
_sh_real="$(cd "${AID_STATE_HOME}" 2>/dev/null && pwd -P)" || _sh_real="${AID_STATE_HOME}"
|
|
86
|
+
_hd_real="$(cd "${HOME}/.aid" 2>/dev/null && pwd -P)" || _hd_real="${HOME}/.aid"
|
|
87
|
+
# If .aid/ resolves to either state-home, this is NOT a project dir.
|
|
88
|
+
if [[ "${_aid_real}" == "${_sh_real}" || "${_aid_real}" == "${_hd_real}" ]]; then
|
|
89
|
+
return 1
|
|
90
|
+
fi
|
|
91
|
+
return 0
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
# ---------------------------------------------------------------------------
|
|
95
|
+
# C1: Per-repo format stamp constant.
|
|
96
|
+
# The current .aid/ layout version. Bumped ONLY on a breaking layout change,
|
|
97
|
+
# never on every CLI release. Defined exactly once; all comparisons read this.
|
|
98
|
+
# ---------------------------------------------------------------------------
|
|
99
|
+
readonly AID_SUPPORTED_FORMAT=1
|
|
100
|
+
|
|
101
|
+
# ---------------------------------------------------------------------------
|
|
102
|
+
# Source the shared install core from AID_CODE_HOME/lib/.
|
|
51
103
|
# ---------------------------------------------------------------------------
|
|
52
|
-
_AID_CORE="${
|
|
104
|
+
_AID_CORE="${AID_CODE_HOME}/lib/aid-install-core.sh"
|
|
53
105
|
if [[ ! -f "$_AID_CORE" ]]; then
|
|
54
|
-
echo "ERROR: aid:
|
|
106
|
+
echo "ERROR: aid: cannot locate the AID code payload (AID_CODE_HOME unresolved). Re-run the AID bootstrap to repair." >&2
|
|
55
107
|
exit 1
|
|
56
108
|
fi
|
|
57
109
|
# shellcheck source=../lib/aid-install-core.sh
|
|
@@ -82,21 +134,57 @@ _aid_usage() {
|
|
|
82
134
|
printf ' Tools: claude-code, codex, cursor, copilot-cli, antigravity\n'
|
|
83
135
|
;;
|
|
84
136
|
remove)
|
|
85
|
-
printf 'aid remove [<tool>[,<tool>...]
|
|
137
|
+
printf 'aid remove [<tool>[,<tool>...]] [--force] [--verbose] [--target <dir>]\n'
|
|
138
|
+
printf 'aid remove self [--force] [--dry-run]\n'
|
|
86
139
|
printf ' Remove tool(s) from the current project (manifest-driven).\n'
|
|
87
140
|
printf ' No args: remove ALL AID from the project (asks for confirmation).\n'
|
|
88
|
-
printf ' self: remove the aid CLI
|
|
141
|
+
printf ' self: COMPLETELY remove the aid CLI, channel-aware (asks for confirmation):\n'
|
|
142
|
+
printf ' npm -> npm uninstall -g | pypi -> pipx uninstall | curl -> rm $AID_HOME + unwire PATH.\n'
|
|
143
|
+
printf ' Auto-elevates with sudo only when the install location needs root.\n'
|
|
144
|
+
printf ' --dry-run: print the exact command(s) it would run, then exit (no changes).\n'
|
|
89
145
|
;;
|
|
90
146
|
update)
|
|
91
|
-
printf 'aid update [<tool>...
|
|
92
|
-
printf '
|
|
147
|
+
printf 'aid update [<tool>...] [--version <v>] [--from-bundle <path>] [--force] [--target <dir>]\n'
|
|
148
|
+
printf 'aid update self [--from-bundle <path>] [--dry-run]\n'
|
|
93
149
|
printf ' Update to latest. No args: update all installed tools.\n'
|
|
94
|
-
printf ' self: update the aid CLI
|
|
150
|
+
printf ' self: COMPLETELY update the aid CLI, channel-aware:\n'
|
|
151
|
+
printf ' npm -> npm i -g | pypi -> pipx upgrade | curl -> re-bootstrap install.sh.\n'
|
|
152
|
+
printf ' Auto-elevates with sudo only when the install location needs root.\n'
|
|
153
|
+
printf ' --from-bundle <path>: install the CLI from a local artifact instead of @latest\n'
|
|
154
|
+
printf ' (npm .tgz | pypi .whl | curl release-staging dir with install.sh).\n'
|
|
155
|
+
printf ' --dry-run: print the exact command(s) it would run, then exit (no changes).\n'
|
|
95
156
|
;;
|
|
96
157
|
version)
|
|
97
158
|
printf 'aid version\n'
|
|
98
159
|
printf ' Print the installed aid CLI version and exit 0.\n'
|
|
99
160
|
;;
|
|
161
|
+
dashboard)
|
|
162
|
+
printf 'aid dashboard start <node|python> [--remote] [--port <n>]\n'
|
|
163
|
+
printf 'aid dashboard stop\n'
|
|
164
|
+
printf ' Start or stop the machine-level pipeline dashboard (serves all registered projects).\n'
|
|
165
|
+
printf ' <node|python> select the server runtime to launch.\n'
|
|
166
|
+
printf ' --remote also expose it to authorized users over a private channel (never public);\n'
|
|
167
|
+
printf ' fails clearly if that mechanism is unavailable -- never binds publicly.\n'
|
|
168
|
+
printf ' --port <n> listen port on 127.0.0.1 (default 8787).\n'
|
|
169
|
+
printf ' The dashboard binds to 127.0.0.1 only. '"'"'stop'"'"' is idempotent and also tears down --remote.\n'
|
|
170
|
+
printf ' Works from any directory (not tied to the current project).\n'
|
|
171
|
+
;;
|
|
172
|
+
projects)
|
|
173
|
+
printf 'aid projects [list] [--local|--shared] [--verbose]\n'
|
|
174
|
+
printf 'aid projects add [<path>] [--local|--shared]\n'
|
|
175
|
+
printf 'aid projects remove [<path>]\n'
|
|
176
|
+
printf ' List, register, or unregister AID projects in the registry.\n'
|
|
177
|
+
printf ' list (default): show all registered projects with state, tools, and tier.\n'
|
|
178
|
+
printf ' The current directory is marked with "*" in the leading marker column.\n'
|
|
179
|
+
printf ' Unregistered cwd with .aid/ present is shown as a footnote.\n'
|
|
180
|
+
printf ' add [path=cwd]: register a project (requires .aid/ to exist); tracking only,\n'
|
|
181
|
+
printf ' no tools are installed. Idempotent. Prints the tier written.\n'
|
|
182
|
+
printf ' remove [path=cwd]: unregister a project from the registry; no files removed.\n'
|
|
183
|
+
printf ' Works on stale/missing/no-aid entries. Idempotent.\n'
|
|
184
|
+
printf ' --local force user tier for add\n'
|
|
185
|
+
printf ' --shared force shared tier for add\n'
|
|
186
|
+
printf ' --verbose print extra detail\n'
|
|
187
|
+
;;
|
|
100
188
|
*)
|
|
101
189
|
printf 'aid - AID CLI\n'
|
|
102
190
|
printf '\n'
|
|
@@ -108,9 +196,11 @@ _aid_usage() {
|
|
|
108
196
|
printf ' aid add <tool>[,...] Add tool(s) to the current project\n'
|
|
109
197
|
printf ' aid update [<tool>... | self] Update to latest; no arg = all tools\n'
|
|
110
198
|
printf ' aid remove [<tool>... | self] Remove; no arg = ALL AID from project\n'
|
|
199
|
+
printf ' aid dashboard start|stop ... Start/stop the local dashboard\n'
|
|
200
|
+
printf ' aid projects [list|add|remove] List/register/unregister AID projects\n'
|
|
111
201
|
printf ' aid <command> -h | --help Per-command help\n'
|
|
112
202
|
printf '\n'
|
|
113
|
-
printf 'Flags: --from-bundle, --version, --force, --target, --verbose\n'
|
|
203
|
+
printf 'Flags: --from-bundle, --version, --force, --dry-run, --target, --verbose\n'
|
|
114
204
|
printf "Run 'aid <command> -h' for details.\n"
|
|
115
205
|
;;
|
|
116
206
|
esac
|
|
@@ -142,10 +232,10 @@ _find_install_sh() {
|
|
|
142
232
|
# ---------------------------------------------------------------------------
|
|
143
233
|
|
|
144
234
|
# _aid_check_update
|
|
145
|
-
# Compares the installed CLI version ($
|
|
235
|
+
# Compares the installed CLI version ($AID_CODE_HOME/VERSION) against the latest
|
|
146
236
|
# GitHub release. Prints ONE notice line when a newer version is available.
|
|
147
237
|
# Fail-silent: any error (no curl, network down, bad JSON) is suppressed.
|
|
148
|
-
# Throttle: re-fetches at most once per 24h; caches result in
|
|
238
|
+
# Throttle: re-fetches at most once per 24h; caches result in ~/.aid/.update-check.
|
|
149
239
|
# Opt-out: AID_NO_UPDATE_CHECK=1 -> skip entirely.
|
|
150
240
|
# Test hook: AID_UPDATE_CHECK_URL overrides the fetch URL (and bypasses throttle).
|
|
151
241
|
_aid_check_update() {
|
|
@@ -154,13 +244,13 @@ _aid_check_update() {
|
|
|
154
244
|
|
|
155
245
|
# Read installed version.
|
|
156
246
|
local installed_version=""
|
|
157
|
-
local ver_file="${
|
|
247
|
+
local ver_file="${AID_CODE_HOME}/VERSION"
|
|
158
248
|
if [[ -f "$ver_file" ]]; then
|
|
159
249
|
installed_version="$(tr -d '[:space:]' < "$ver_file")"
|
|
160
250
|
fi
|
|
161
251
|
[[ -z "$installed_version" ]] && return 0
|
|
162
252
|
|
|
163
|
-
local cache_file="${
|
|
253
|
+
local cache_file="${HOME}/.aid/.update-check"
|
|
164
254
|
local now
|
|
165
255
|
now="$(date +%s 2>/dev/null)" || return 0
|
|
166
256
|
local throttle_secs=86400 # 24 hours
|
|
@@ -218,35 +308,104 @@ _aid_check_update() {
|
|
|
218
308
|
|
|
219
309
|
# Compare: show notice only when latest > installed.
|
|
220
310
|
if _semver_lt "$installed_version" "$latest_version"; then
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
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"
|
|
311
|
+
# `aid update self` is now channel-aware and self-contained (it runs the
|
|
312
|
+
# right package manager + applies migrations), so point at it for every
|
|
313
|
+
# channel instead of a per-channel manual command.
|
|
314
|
+
printf 'A newer aid CLI is available: v%s (you have v%s). Run: aid update self\n' \
|
|
315
|
+
"$latest_version" "$installed_version"
|
|
229
316
|
fi
|
|
230
317
|
return 0
|
|
231
318
|
}
|
|
232
319
|
|
|
320
|
+
|
|
233
321
|
# ---------------------------------------------------------------------------
|
|
234
322
|
# update self command (formerly self-update).
|
|
235
323
|
# ---------------------------------------------------------------------------
|
|
324
|
+
# _aid_priv_run <writability-probe-dir> <cmd...>
|
|
325
|
+
# Run <cmd>, auto-elevating with sudo ONLY when <probe-dir> exists and is not
|
|
326
|
+
# writable by the current user (and we are not already root) -- e.g. a
|
|
327
|
+
# root-owned npm global prefix. A user-level prefix / pipx venv stays sudo-free.
|
|
328
|
+
# Honors _SELF_DRYRUN=1 (print the resolved command, prefixed with sudo when it
|
|
329
|
+
# would elevate, and do nothing). Returns the command's exit code, or 13 when
|
|
330
|
+
# elevation is needed but sudo is unavailable.
|
|
331
|
+
_aid_priv_run() {
|
|
332
|
+
local probe="$1"; shift
|
|
333
|
+
local need_root=0
|
|
334
|
+
if [[ -n "$probe" && -e "$probe" && ! -w "$probe" && "$(id -u)" -ne 0 ]]; then
|
|
335
|
+
need_root=1
|
|
336
|
+
fi
|
|
337
|
+
if [[ "${_SELF_DRYRUN:-0}" == "1" ]]; then
|
|
338
|
+
if [[ "$need_root" -eq 1 ]]; then printf '+ sudo %s\n' "$*"; else printf '+ %s\n' "$*"; fi
|
|
339
|
+
return 0
|
|
340
|
+
fi
|
|
341
|
+
if [[ "$need_root" -eq 1 ]]; then
|
|
342
|
+
if command -v sudo >/dev/null 2>&1; then
|
|
343
|
+
printf 'aid: %s is not writable -- elevating this step via sudo...\n' "$probe" >&2
|
|
344
|
+
sudo "$@"; return $?
|
|
345
|
+
fi
|
|
346
|
+
printf 'ERROR: aid: %s is not writable and sudo is unavailable. Run manually:\n %s\n' "$probe" "$*" >&2
|
|
347
|
+
return 13
|
|
348
|
+
fi
|
|
349
|
+
"$@"
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
# Channel-aware, self-contained CLI self-update. Reads the channel from
|
|
353
|
+
# AID_INSTALL_CHANNEL (injected by the npm/pypi shims); the curl/default channel
|
|
354
|
+
# re-bootstraps via install.sh. Honors _SELF_FROM_BUNDLE (a local CLI artifact:
|
|
355
|
+
# npm .tgz / pypi .whl / curl bundle dir) and _SELF_DRYRUN. The post-update
|
|
356
|
+
# migration scan runs in the caller's (user) context -- never under the sudo
|
|
357
|
+
# used here for the privileged install step.
|
|
236
358
|
_cmd_update_self() {
|
|
237
|
-
#
|
|
238
|
-
# and
|
|
239
|
-
|
|
359
|
+
# AID_SKIP_SELF_INSTALL: the package manager already (re)installed the CLI
|
|
360
|
+
# (npm postinstall) and only wants the post-update migration to run. Skip the
|
|
361
|
+
# re-install step.
|
|
362
|
+
if [[ "${AID_SKIP_SELF_INSTALL:-0}" == "1" ]]; then
|
|
363
|
+
return 0
|
|
364
|
+
fi
|
|
365
|
+
local channel="${AID_INSTALL_CHANNEL:-}"
|
|
366
|
+
local bundle="${_SELF_FROM_BUNDLE:-}"
|
|
367
|
+
case "$channel" in
|
|
240
368
|
npm)
|
|
241
|
-
|
|
242
|
-
|
|
369
|
+
command -v npm >/dev/null 2>&1 || { echo "ERROR: aid: npm not found; cannot update the npm-channel CLI" >&2; return 3; }
|
|
370
|
+
local gdir pkg
|
|
371
|
+
gdir="$(npm root -g 2>/dev/null)"
|
|
372
|
+
pkg="aid-installer@latest"; [[ -n "$bundle" ]] && pkg="$bundle"
|
|
373
|
+
printf 'Updating the aid CLI (npm channel)...\n'
|
|
374
|
+
_aid_priv_run "$gdir" npm install -g "$pkg"
|
|
375
|
+
return $?
|
|
243
376
|
;;
|
|
244
377
|
pypi)
|
|
245
|
-
|
|
246
|
-
|
|
378
|
+
command -v pipx >/dev/null 2>&1 || { echo "ERROR: aid: pipx not found; cannot update the pypi-channel CLI" >&2; return 3; }
|
|
379
|
+
printf 'Updating the aid CLI (pypi/pipx channel)...\n'
|
|
380
|
+
if [[ -n "$bundle" ]]; then
|
|
381
|
+
_aid_priv_run "" pipx install --force "$bundle"
|
|
382
|
+
else
|
|
383
|
+
_aid_priv_run "" pipx upgrade aid-installer
|
|
384
|
+
fi
|
|
385
|
+
return $?
|
|
247
386
|
;;
|
|
248
387
|
esac
|
|
388
|
+
# curl / default channel -- re-bootstrap install.sh.
|
|
249
389
|
printf 'Updating the aid CLI...\n'
|
|
390
|
+
if [[ -n "$bundle" ]]; then
|
|
391
|
+
# --from-bundle <dir> on the curl channel: a release-staging dir that
|
|
392
|
+
# carries install.sh + the CLI bundle + SHA256SUMS. Run it offline.
|
|
393
|
+
if [[ -f "${bundle%/}/install.sh" ]]; then
|
|
394
|
+
if [[ "${_SELF_DRYRUN:-0}" == "1" ]]; then
|
|
395
|
+
printf '+ AID_CLI_BUNDLE_BASE=file://%s AID_LIB_BASE=file://%s bash %s/install.sh\n' "${bundle%/}" "${bundle%/}" "${bundle%/}"
|
|
396
|
+
return 0
|
|
397
|
+
fi
|
|
398
|
+
AID_CLI_BUNDLE_BASE="file://${bundle%/}" AID_LIB_BASE="file://${bundle%/}" \
|
|
399
|
+
bash "${bundle%/}/install.sh"
|
|
400
|
+
return $?
|
|
401
|
+
fi
|
|
402
|
+
echo "ERROR: aid: --from-bundle <dir> for the curl channel must contain install.sh (got: ${bundle})" >&2
|
|
403
|
+
return 2
|
|
404
|
+
fi
|
|
405
|
+
if [[ "${_SELF_DRYRUN:-0}" == "1" ]]; then
|
|
406
|
+
printf '+ curl -fsSL %s | bash\n' "${AID_INSTALL_URL}"
|
|
407
|
+
return 0
|
|
408
|
+
fi
|
|
250
409
|
if command -v curl >/dev/null 2>&1; then
|
|
251
410
|
curl -fsSL "${AID_INSTALL_URL}" | bash
|
|
252
411
|
return $?
|
|
@@ -256,6 +415,73 @@ _cmd_update_self() {
|
|
|
256
415
|
fi
|
|
257
416
|
}
|
|
258
417
|
|
|
418
|
+
# ---------------------------------------------------------------------------
|
|
419
|
+
# _aid_update_self_if_stale (FF-3 preamble / CLI-2 / task-079)
|
|
420
|
+
# Self-update-if-needed preamble for the 'aid update [<tool>]' reach.
|
|
421
|
+
# Reuses _cmd_update_self's channel logic gated by a skip-if-current check
|
|
422
|
+
# (OQ-6 resolved simplest-correct: compare installed $AID_CODE_HOME/VERSION against
|
|
423
|
+
# the cached .update-check latest; if stale -> call _cmd_update_self; if
|
|
424
|
+
# current or unknown -> silent no-op).
|
|
425
|
+
#
|
|
426
|
+
# Safety notes (to prevent re-bootstrap/loop hazards):
|
|
427
|
+
# - This is called BEFORE the tool-install loop on the 'update' reach only
|
|
428
|
+
# (not 'update self', not 'add') -- no recursion possible.
|
|
429
|
+
# - _cmd_update_self is channel-aware and self-contained: npm runs
|
|
430
|
+
# `npm install -g`, pypi runs `pipx install/upgrade` (may sudo-prompt only
|
|
431
|
+
# when the install location needs root), curl re-runs the bootstrap (which
|
|
432
|
+
# replaces bin/aid on disk). In every case the current process keeps running
|
|
433
|
+
# the already-loaded script, so the subsequent migration runs under the
|
|
434
|
+
# current code as the invoking user -- acceptable (same pattern as
|
|
435
|
+
# 'aid update self' + post-update scan).
|
|
436
|
+
# - WARN-not-fail: a self-update failure is logged and the tool-install
|
|
437
|
+
# continues (NFR12).
|
|
438
|
+
# ---------------------------------------------------------------------------
|
|
439
|
+
_aid_update_self_if_stale() {
|
|
440
|
+
# Read installed version (same pattern as _aid_check_update).
|
|
441
|
+
local _installed=""
|
|
442
|
+
local _ver_file="${AID_CODE_HOME}/VERSION"
|
|
443
|
+
if [[ -f "${_ver_file}" ]]; then
|
|
444
|
+
_installed="$(tr -d '[:space:]' < "${_ver_file}")"
|
|
445
|
+
fi
|
|
446
|
+
[[ -z "${_installed}" ]] && return 0 # no installed version known -> skip
|
|
447
|
+
|
|
448
|
+
# Read cached latest version from .update-check (line 2 of the cache file).
|
|
449
|
+
local _cache_file="${HOME}/.aid/.update-check"
|
|
450
|
+
local _cached_latest=""
|
|
451
|
+
if [[ -f "${_cache_file}" ]]; then
|
|
452
|
+
_cached_latest="$(awk 'NR==2{print $1}' "${_cache_file}" 2>/dev/null)" || _cached_latest=""
|
|
453
|
+
fi
|
|
454
|
+
[[ -z "${_cached_latest}" ]] && return 0 # no cached latest known -> skip (no network call here)
|
|
455
|
+
|
|
456
|
+
# Offline / explicit install: when the caller supplied a local bundle, do NOT
|
|
457
|
+
# phone the package channel to self-update. The bundle is the source of truth
|
|
458
|
+
# for this install; reaching out to the registry would defeat an air-gapped or
|
|
459
|
+
# pre-release install (and could replace the running CLI behind the user's back).
|
|
460
|
+
[[ -n "${_AID_FROM_BUNDLE:-}" ]] && return 0
|
|
461
|
+
|
|
462
|
+
# Skip if already current.
|
|
463
|
+
if [[ "${_installed}" == "${_cached_latest}" ]]; then
|
|
464
|
+
return 0
|
|
465
|
+
fi
|
|
466
|
+
|
|
467
|
+
# Only self-update when the installed CLI is strictly OLDER than the latest
|
|
468
|
+
# (semver-aware). A newer installed version (e.g. an unreleased dev build) must
|
|
469
|
+
# never be downgraded to "latest". sort -V puts the lower version first.
|
|
470
|
+
local _lower
|
|
471
|
+
_lower="$(printf '%s\n%s\n' "${_installed}" "${_cached_latest}" | sort -V | head -1)"
|
|
472
|
+
if [[ "${_lower}" != "${_installed}" ]]; then
|
|
473
|
+
return 0 # installed >= latest -> nothing to do (never downgrade)
|
|
474
|
+
fi
|
|
475
|
+
|
|
476
|
+
# Stale: call the channel-appropriate self-update logic.
|
|
477
|
+
# WARN-not-fail: failure here must not abort the tool-update.
|
|
478
|
+
printf 'aid update: CLI is not current (installed: %s, available: %s); self-updating before tool install...\n' \
|
|
479
|
+
"${_installed}" "${_cached_latest}"
|
|
480
|
+
_cmd_update_self || \
|
|
481
|
+
echo "WARN: aid: self-update failed (continuing with tool install)" >&2
|
|
482
|
+
return 0
|
|
483
|
+
}
|
|
484
|
+
|
|
259
485
|
# ---------------------------------------------------------------------------
|
|
260
486
|
# Path-wiring helpers (Unix).
|
|
261
487
|
# ---------------------------------------------------------------------------
|
|
@@ -416,15 +642,14 @@ _unwire_path_unix() {
|
|
|
416
642
|
# ---------------------------------------------------------------------------
|
|
417
643
|
|
|
418
644
|
# _install_global_cli <version> <src_bin_aid> <src_lib_core>
|
|
419
|
-
# Stage then atomic-move into
|
|
645
|
+
# Stage then atomic-move into AID_CODE_HOME (the read-only code payload root).
|
|
420
646
|
_install_global_cli() {
|
|
421
647
|
local version="$1"
|
|
422
648
|
local src_bin_aid="$2"
|
|
423
649
|
local src_lib_core="$3"
|
|
424
650
|
|
|
425
|
-
local
|
|
426
|
-
local
|
|
427
|
-
local lib_dir="${aid_home}/lib"
|
|
651
|
+
local bin_dir="${AID_CODE_HOME}/bin"
|
|
652
|
+
local lib_dir="${AID_CODE_HOME}/lib"
|
|
428
653
|
|
|
429
654
|
mkdir -p "$bin_dir" "$lib_dir"
|
|
430
655
|
|
|
@@ -436,43 +661,63 @@ _install_global_cli() {
|
|
|
436
661
|
cp "$src_lib_core" "${lib_dir}/aid-install-core.sh"
|
|
437
662
|
|
|
438
663
|
# Write the VERSION file.
|
|
439
|
-
printf '%s\n' "$version" > "${
|
|
664
|
+
printf '%s\n' "$version" > "${AID_CODE_HOME}/VERSION"
|
|
440
665
|
|
|
441
|
-
echo "aid CLI v${version} installed to ${
|
|
666
|
+
echo "aid CLI v${version} installed to ${AID_CODE_HOME}."
|
|
442
667
|
}
|
|
443
668
|
|
|
444
669
|
# ---------------------------------------------------------------------------
|
|
445
670
|
# remove self (formerly self-uninstall).
|
|
446
671
|
# ---------------------------------------------------------------------------
|
|
447
672
|
|
|
673
|
+
# Channel-aware, self-contained CLI removal. npm/pypi installs are owned by the
|
|
674
|
+
# package manager, so removing only $AID_HOME left the wrapper + bin shim behind
|
|
675
|
+
# (a dangling entry point). Now each channel does the COMPLETE removal:
|
|
676
|
+
# npm -> npm uninstall -g aid-installer (package + vendored tree + shim)
|
|
677
|
+
# pypi -> pipx uninstall aid-installer (venv + entry point)
|
|
678
|
+
# curl -> rm -rf $AID_HOME + unwire PATH (unchanged)
|
|
679
|
+
# Privileged step (root-owned npm global) auto-elevates via _aid_priv_run.
|
|
680
|
+
# Honors --dry-run.
|
|
448
681
|
_cmd_remove_self() {
|
|
449
682
|
local force=0
|
|
450
683
|
local no_path=0
|
|
451
684
|
local profile_file=""
|
|
685
|
+
local dryrun=0
|
|
452
686
|
|
|
453
687
|
while [[ $# -gt 0 ]]; do
|
|
454
688
|
case "$1" in
|
|
455
689
|
--force|-y) force=1; shift ;;
|
|
456
690
|
--no-path) no_path=1; shift ;;
|
|
457
691
|
--profile-file) profile_file="$2"; shift 2 ;;
|
|
692
|
+
--dry-run) dryrun=1; shift ;;
|
|
458
693
|
-h|--help) _aid_usage remove; exit 0 ;;
|
|
459
694
|
*) _aid_die "unknown flag for 'remove self': $1" 2 ;;
|
|
460
695
|
esac
|
|
461
696
|
done
|
|
697
|
+
_SELF_DRYRUN="$dryrun"; export _SELF_DRYRUN
|
|
462
698
|
|
|
463
699
|
# Apply AID_FORCE env-var fallback.
|
|
464
700
|
if [[ "$force" -eq 0 && ( "${AID_FORCE:-0}" == "1" || "${AID_FORCE:-0}" == "true" ) ]]; then
|
|
465
701
|
force=1
|
|
466
702
|
fi
|
|
467
703
|
|
|
704
|
+
local channel="${AID_INSTALL_CHANNEL:-}"
|
|
468
705
|
local aid_home="${AID_HOME:-${HOME}/.aid}"
|
|
469
706
|
|
|
470
|
-
|
|
707
|
+
# Channel-aware description of what will be removed (NFR transparency).
|
|
708
|
+
local what
|
|
709
|
+
case "$channel" in
|
|
710
|
+
npm) what="the npm global package 'aid-installer' (npm uninstall -g)" ;;
|
|
711
|
+
pypi) what="the pipx app 'aid-installer' (pipx uninstall)" ;;
|
|
712
|
+
*) what="${aid_home} and its PATH wiring" ;;
|
|
713
|
+
esac
|
|
714
|
+
|
|
715
|
+
if [[ "$force" -eq 0 && "$dryrun" -ne 1 ]]; then
|
|
471
716
|
# Skip prompt when non-interactive (piped or no tty).
|
|
472
717
|
if [[ ! -t 0 ]]; then
|
|
473
718
|
force=1
|
|
474
719
|
else
|
|
475
|
-
printf 'Remove the aid CLI
|
|
720
|
+
printf 'Remove the aid CLI -- %s? [y/N] ' "$what"
|
|
476
721
|
local answer
|
|
477
722
|
if [[ -e /dev/tty ]]; then
|
|
478
723
|
read -r answer < /dev/tty
|
|
@@ -487,24 +732,46 @@ _cmd_remove_self() {
|
|
|
487
732
|
fi
|
|
488
733
|
|
|
489
734
|
local partial=0
|
|
735
|
+
case "$channel" in
|
|
736
|
+
npm)
|
|
737
|
+
command -v npm >/dev/null 2>&1 || { echo "ERROR: aid: npm not found; cannot remove the npm-channel CLI" >&2; exit 3; }
|
|
738
|
+
local gdir; gdir="$(npm root -g 2>/dev/null)"
|
|
739
|
+
_aid_priv_run "$gdir" npm uninstall -g aid-installer || partial=1
|
|
740
|
+
;;
|
|
741
|
+
pypi)
|
|
742
|
+
command -v pipx >/dev/null 2>&1 || { echo "ERROR: aid: pipx not found; cannot remove the pypi-channel CLI" >&2; exit 3; }
|
|
743
|
+
if [[ "$dryrun" -eq 1 ]]; then
|
|
744
|
+
printf '+ pipx uninstall aid-installer\n'
|
|
745
|
+
else
|
|
746
|
+
pipx uninstall aid-installer || partial=1
|
|
747
|
+
fi
|
|
748
|
+
;;
|
|
749
|
+
*)
|
|
750
|
+
# curl / default channel -- the AID_HOME tree + shell-profile PATH wiring.
|
|
751
|
+
if [[ "$dryrun" -eq 1 ]]; then
|
|
752
|
+
[[ "$no_path" -eq 0 ]] && printf '+ (unwire %s/bin from your shell profile)\n' "$aid_home"
|
|
753
|
+
printf '+ rm -rf %s\n' "$aid_home"
|
|
754
|
+
else
|
|
755
|
+
if [[ "$no_path" -eq 0 ]]; then
|
|
756
|
+
if [[ -n "$profile_file" ]]; then
|
|
757
|
+
_unwire_path_unix --profile-file "$profile_file" || partial=1
|
|
758
|
+
else
|
|
759
|
+
_unwire_path_unix || partial=1
|
|
760
|
+
fi
|
|
761
|
+
fi
|
|
762
|
+
if [[ -d "$aid_home" ]]; then
|
|
763
|
+
rm -rf "$aid_home" || {
|
|
764
|
+
echo "ERROR: aid: failed to remove ${aid_home}" >&2
|
|
765
|
+
partial=1
|
|
766
|
+
}
|
|
767
|
+
fi
|
|
768
|
+
fi
|
|
769
|
+
;;
|
|
770
|
+
esac
|
|
490
771
|
|
|
491
|
-
|
|
492
|
-
|
|
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
|
-
}
|
|
772
|
+
if [[ "$dryrun" -eq 1 ]]; then
|
|
773
|
+
exit 0
|
|
506
774
|
fi
|
|
507
|
-
|
|
508
775
|
if [[ "$partial" -eq 1 ]]; then
|
|
509
776
|
echo "aid CLI partially removed. Check the messages above for what remained."
|
|
510
777
|
exit 1
|
|
@@ -515,151 +782,2071 @@ _cmd_remove_self() {
|
|
|
515
782
|
}
|
|
516
783
|
|
|
517
784
|
# ---------------------------------------------------------------------------
|
|
518
|
-
#
|
|
785
|
+
# Remote exposure helpers (feature-005 / LC-EXP-B).
|
|
786
|
+
# SEC-1: These helpers invoke ONLY 'tailscale serve' (tailnet-only). The public
|
|
787
|
+
# exposure verb is never used -- a bare grep for it returns nothing
|
|
788
|
+
# anywhere in this file (structural never-public, C1).
|
|
789
|
+
# SEC-6: --remote exposes the CLI home (all registered repos, OQ5/DR-4): a
|
|
790
|
+
# granted tailnet identity sees the full registered-repo list + each
|
|
791
|
+
# repo's home.html/kb.html/api/model. This is the accepted OQ5 trade-off
|
|
792
|
+
# -- a grantee is already a trusted operator of this host. The helpers
|
|
793
|
+
# below, the bind, and the teardown are UNCHANGED; only what the port
|
|
794
|
+
# serves changed (DR-2/task-047). Never-public (C1) and host/user-ACL
|
|
795
|
+
# scoping (C3) hold exactly as before.
|
|
519
796
|
# ---------------------------------------------------------------------------
|
|
520
797
|
|
|
521
|
-
#
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
798
|
+
# _aid_remote_expose <port>
|
|
799
|
+
# Bring up tailscale serve (tailnet-only) for a loopback port.
|
|
800
|
+
# stdout (exit 0): two lines: handle (tailscale-serve:<port>) + https URL.
|
|
801
|
+
# stderr: human messages, errors, FR18 ACL-grant guidance.
|
|
802
|
+
# exit: 0=ok 10=mechanism absent 11=non-loopback target 12=serve failed
|
|
803
|
+
_aid_remote_expose() {
|
|
804
|
+
local port="$1"
|
|
805
|
+
|
|
806
|
+
# Step 1: Re-assert the loopback target (belt-and-suspenders, SEC-1).
|
|
807
|
+
# This function only accepts a bare port number (caller always passes 127.0.0.1:<port>
|
|
808
|
+
# as the server's bind, but exposes only via a port token). If someone passes a
|
|
809
|
+
# non-numeric or IP-prefixed token the contract is violated.
|
|
810
|
+
if [[ -z "$port" ]] || ! [[ "$port" =~ ^[0-9]+$ ]]; then
|
|
811
|
+
echo "ERROR: aid: dashboard: expose target must be 127.0.0.1 (got: ${port})" >&2
|
|
812
|
+
return 11
|
|
813
|
+
fi
|
|
529
814
|
|
|
530
|
-
#
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
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")"
|
|
815
|
+
# Step 2a: availability -- tailscale on PATH?
|
|
816
|
+
if ! command -v tailscale >/dev/null 2>&1; then
|
|
817
|
+
echo "ERROR: aid: dashboard: --remote requested but tailscale is not on PATH; --remote is unavailable" >&2
|
|
818
|
+
return 10
|
|
539
819
|
fi
|
|
540
|
-
printf 'AID v%s - Agentic Iterative Development\n' "$cli_version"
|
|
541
|
-
printf 'Install, update, and manage AID across your repositories.\n'
|
|
542
820
|
|
|
543
|
-
#
|
|
544
|
-
|
|
545
|
-
|
|
821
|
+
# Step 2b: availability -- node logged in and Running?
|
|
822
|
+
local ts_status_out
|
|
823
|
+
ts_status_out="$(tailscale status 2>&1)" || true
|
|
824
|
+
# 'tailscale status' exits nonzero and prints "not running" or similar when the
|
|
825
|
+
# daemon is stopped, or when not logged in.
|
|
826
|
+
if echo "$ts_status_out" | grep -qiE '(not running|logged out|Stopped|NeedsLogin|NoState|not logged in)'; then
|
|
827
|
+
echo "ERROR: aid: dashboard: --remote requested but tailscale is not running or not logged in (tailscale status: ${ts_status_out}); --remote is unavailable" >&2
|
|
828
|
+
return 10
|
|
829
|
+
fi
|
|
830
|
+
# Also check for error/failure exit where output may be empty.
|
|
831
|
+
if [[ -z "$ts_status_out" ]]; then
|
|
832
|
+
echo "ERROR: aid: dashboard: --remote requested but tailscale status returned no output; --remote is unavailable" >&2
|
|
833
|
+
return 10
|
|
834
|
+
fi
|
|
546
835
|
|
|
547
|
-
#
|
|
548
|
-
|
|
549
|
-
|
|
836
|
+
# Step 3: Bring up Serve (tailnet-only; the public exposure verb is never invoked -- SEC-1).
|
|
837
|
+
local serve_err
|
|
838
|
+
serve_err="$(tailscale serve --bg "$port" 2>&1)"
|
|
839
|
+
local serve_rc=$?
|
|
840
|
+
if [[ "$serve_rc" -ne 0 ]]; then
|
|
841
|
+
echo "ERROR: aid: dashboard: tailscale serve failed (rc=${serve_rc}): ${serve_err}" >&2
|
|
842
|
+
# Revert: take down the 443 frontend mapping (if it was partially set).
|
|
843
|
+
tailscale serve --bg --https=443 off >/dev/null 2>&1 || true
|
|
844
|
+
return 12
|
|
845
|
+
fi
|
|
550
846
|
|
|
551
|
-
#
|
|
552
|
-
|
|
847
|
+
# Step 4: Resolve the private URL from tailscale's Self.DNSName (the MagicDNS name).
|
|
848
|
+
# NOTE: 'tailscale status --json' is PRETTY-PRINTED ("DNSName": "host.tailnet.ts.net."),
|
|
849
|
+
# so the match MUST tolerate whitespace after the colon. '--peers=false' isolates Self so
|
|
850
|
+
# a peer's DNSName is never picked up by mistake. We must NEVER fall back to the machine's
|
|
851
|
+
# own hostname/FQDN for this URL: that resolves to the local/corporate DNS domain (e.g.
|
|
852
|
+
# host.example.com), not the tailnet -- producing a URL that does not work and leaking the
|
|
853
|
+
# wrong domain into the ACL guidance below.
|
|
854
|
+
local ts_json node_fqdn private_url
|
|
855
|
+
ts_json="$(tailscale status --json --peers=false 2>/dev/null)" || ts_json=""
|
|
856
|
+
if [[ -z "$ts_json" ]]; then
|
|
857
|
+
ts_json="$(tailscale status --json 2>/dev/null)" || ts_json=""
|
|
858
|
+
fi
|
|
859
|
+
node_fqdn=""
|
|
860
|
+
if [[ -n "$ts_json" ]]; then
|
|
861
|
+
# Scope the parse to the Self object so a peer DNSName can never be selected
|
|
862
|
+
# regardless of JSON ordering. sed -n prints from "Self": to the first "Peer":
|
|
863
|
+
# line (exclusive); when --peers=false was used there is no "Peer" line so the
|
|
864
|
+
# range runs to EOF -- which is Self-only, the correct and safe result.
|
|
865
|
+
local self_block
|
|
866
|
+
self_block="$(printf '%s' "$ts_json" \
|
|
867
|
+
| sed -n '/"Self"[[:space:]]*:/,/"Peer"[[:space:]]*:/p')"
|
|
868
|
+
[[ -z "$self_block" ]] && self_block="$ts_json"
|
|
869
|
+
node_fqdn="$(printf '%s' "$self_block" \
|
|
870
|
+
| grep -oE '"DNSName"[[:space:]]*:[[:space:]]*"[^"]*"' \
|
|
871
|
+
| head -1 \
|
|
872
|
+
| sed -E 's/.*"DNSName"[[:space:]]*:[[:space:]]*"//; s/"$//; s/\.$//')"
|
|
873
|
+
fi
|
|
874
|
+
if [[ -z "$node_fqdn" ]]; then
|
|
875
|
+
# Defensive fallback: a *.ts.net host reported by 'tailscale serve status --json'.
|
|
876
|
+
local serve_json
|
|
877
|
+
serve_json="$(tailscale serve status --json 2>/dev/null)" || serve_json=""
|
|
878
|
+
if [[ -n "$serve_json" ]]; then
|
|
879
|
+
node_fqdn="$(printf '%s' "$serve_json" \
|
|
880
|
+
| grep -oE '[a-z0-9-]+(\.[a-z0-9-]+)*\.ts\.net' | head -1)"
|
|
881
|
+
fi
|
|
882
|
+
fi
|
|
883
|
+
if [[ -n "$node_fqdn" ]]; then
|
|
884
|
+
private_url="https://${node_fqdn}/"
|
|
885
|
+
else
|
|
886
|
+
# Could not resolve the tailnet MagicDNS name. Do NOT fabricate a public-domain URL.
|
|
887
|
+
private_url="(unresolved: run 'tailscale status' to find this host's .ts.net name)"
|
|
888
|
+
fi
|
|
889
|
+
|
|
890
|
+
# Resolve display values for the ACL-grant guidance. The grant *src* (who may reach the
|
|
891
|
+
# host) is an identity only you can choose -- your login, a group:, or a tag: -- and a DNS
|
|
892
|
+
# domain is NOT a valid grant selector, so AID shows a placeholder rather than guessing it.
|
|
893
|
+
# The *dst* is correctly THIS host's tailnet short-name.
|
|
894
|
+
local node_short
|
|
895
|
+
node_short="$(printf '%s' "$node_fqdn" | cut -d. -f1)"
|
|
896
|
+
if [[ -z "$node_short" && -n "$ts_json" ]]; then
|
|
897
|
+
# Same Self-scoping applied to HostName extraction to prevent a peer hostname
|
|
898
|
+
# from being selected when the Self-first ordering assumption does not hold.
|
|
899
|
+
local self_block_hn
|
|
900
|
+
self_block_hn="$(printf '%s' "$ts_json" \
|
|
901
|
+
| sed -n '/"Self"[[:space:]]*:/,/"Peer"[[:space:]]*:/p')"
|
|
902
|
+
[[ -z "$self_block_hn" ]] && self_block_hn="$ts_json"
|
|
903
|
+
node_short="$(printf '%s' "$self_block_hn" \
|
|
904
|
+
| grep -oE '"HostName"[[:space:]]*:[[:space:]]*"[^"]*"' \
|
|
905
|
+
| head -1 \
|
|
906
|
+
| sed -E 's/.*"HostName"[[:space:]]*:[[:space:]]*"//; s/"$//' \
|
|
907
|
+
| tr 'A-Z' 'a-z')"
|
|
908
|
+
fi
|
|
909
|
+
|
|
910
|
+
# Step 5: Print FR18 ACL-grant guidance to STDERR (informational only).
|
|
911
|
+
local src_placeholder dst_placeholder
|
|
912
|
+
src_placeholder="<you@example.com>"
|
|
913
|
+
dst_placeholder="${node_short:-<this-host>}"
|
|
914
|
+
|
|
915
|
+
cat >&2 <<GUIDANCE_EOF
|
|
916
|
+
|
|
917
|
+
Remote exposure is UP (tailnet-private). Every device on your tailnet can now reach this host.
|
|
918
|
+
To restrict access to only you, add a deny-by-default ACL grant in the tailnet policy file:
|
|
919
|
+
https://login.tailscale.com/admin/acls/file
|
|
920
|
+
{"grants":[{"src":["${src_placeholder}"],"dst":["${dst_placeholder}"],"ip":["tcp:443"]}]}
|
|
921
|
+
Note: granted identities see all registered project paths/names. See 'aid dashboard --help'.
|
|
922
|
+
|
|
923
|
+
GUIDANCE_EOF
|
|
924
|
+
|
|
925
|
+
# Step 6: Emit handle + URL on stdout, exit 0.
|
|
926
|
+
printf 'tailscale-serve:%s\n' "$port"
|
|
927
|
+
printf '%s\n' "$private_url"
|
|
928
|
+
return 0
|
|
553
929
|
}
|
|
554
930
|
|
|
555
|
-
#
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
fi
|
|
931
|
+
# _aid_remote_teardown <handle>
|
|
932
|
+
# Revert the tailscale serve mapping created by _aid_remote_expose.
|
|
933
|
+
# exit: 0=ok/idempotent 13=revert warned
|
|
934
|
+
_aid_remote_teardown() {
|
|
935
|
+
local handle="${1:-}"
|
|
561
936
|
|
|
562
|
-
|
|
563
|
-
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
937
|
+
# Step 1: Parse the handle; malformed/empty -> idempotent exit 0.
|
|
938
|
+
if [[ -z "$handle" ]]; then
|
|
939
|
+
return 0
|
|
940
|
+
fi
|
|
941
|
+
if ! [[ "$handle" =~ ^tailscale-serve:([0-9]+)$ ]]; then
|
|
942
|
+
# Malformed handle -- nothing to tear down.
|
|
943
|
+
return 0
|
|
944
|
+
fi
|
|
945
|
+
# We don't use the port for teardown (we target the HTTPS:443 frontend, not the backend port).
|
|
568
946
|
|
|
569
|
-
|
|
570
|
-
|
|
947
|
+
# Step 2: If tailscale is gone now -> WARN, exit 0.
|
|
948
|
+
if ! command -v tailscale >/dev/null 2>&1; then
|
|
949
|
+
echo "WARN: aid: dashboard: tailscale not found; cannot revert serve mapping (handle: ${handle})" >&2
|
|
950
|
+
return 0
|
|
951
|
+
fi
|
|
571
952
|
|
|
572
|
-
#
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
953
|
+
# Step 3: Revert the HTTPS:443 frontend mapping (not a backend port off).
|
|
954
|
+
local off_err
|
|
955
|
+
off_err="$(tailscale serve --bg --https=443 off 2>&1)"
|
|
956
|
+
local off_rc=$?
|
|
957
|
+
if [[ "$off_rc" -ne 0 ]]; then
|
|
958
|
+
# Fallback: check if serve status shows no other mappings; if so, reset.
|
|
959
|
+
local srv_status
|
|
960
|
+
srv_status="$(tailscale serve status 2>/dev/null)" || srv_status=""
|
|
961
|
+
# Count serve entries. If there's nothing else to protect, do a reset.
|
|
962
|
+
local mapping_count
|
|
963
|
+
mapping_count="$(echo "$srv_status" | grep -cE '(https?://|tcp://)' 2>/dev/null || echo "0")"
|
|
964
|
+
if [[ "$mapping_count" -le 1 ]]; then
|
|
965
|
+
tailscale serve reset >/dev/null 2>&1 || true
|
|
966
|
+
# After reset, exit 0 -- best effort.
|
|
967
|
+
return 0
|
|
968
|
+
fi
|
|
969
|
+
echo "WARN: aid: dashboard: tailscale serve --https=443 off failed (rc=${off_rc}): ${off_err}" >&2
|
|
970
|
+
return 13
|
|
579
971
|
fi
|
|
580
|
-
exit 0
|
|
581
|
-
fi
|
|
582
972
|
|
|
583
|
-
#
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
973
|
+
# Step 4: exit 0 on clean revert.
|
|
974
|
+
return 0
|
|
975
|
+
}
|
|
976
|
+
|
|
977
|
+
# ---------------------------------------------------------------------------
|
|
978
|
+
# Dashboard control (aid dashboard start|stop).
|
|
979
|
+
# ---------------------------------------------------------------------------
|
|
980
|
+
_cmd_dashboard_ctl() {
|
|
981
|
+
local verb="${1:-}"
|
|
982
|
+
[[ $# -gt 0 ]] && shift
|
|
983
|
+
|
|
984
|
+
# Top-level help.
|
|
985
|
+
if [[ "$verb" == "-h" || "$verb" == "--help" ]]; then
|
|
986
|
+
_aid_usage dashboard
|
|
987
|
+
exit 0
|
|
988
|
+
fi
|
|
989
|
+
|
|
990
|
+
if [[ "$verb" != "start" && "$verb" != "stop" ]]; then
|
|
991
|
+
if [[ -z "$verb" ]]; then
|
|
992
|
+
echo "ERROR: aid: dashboard requires a verb: start or stop (e.g. aid dashboard start python)" >&2
|
|
993
|
+
exit 2
|
|
994
|
+
fi
|
|
995
|
+
echo "ERROR: aid: dashboard: unknown verb '${verb}' (expected: start or stop)" >&2
|
|
996
|
+
exit 2
|
|
997
|
+
fi
|
|
998
|
+
|
|
999
|
+
# --- shared arg parsing ---
|
|
1000
|
+
local _dc_verbose=0
|
|
1001
|
+
local _dc_port=8787
|
|
1002
|
+
local _dc_remote=0
|
|
1003
|
+
local _dc_runtime=""
|
|
1004
|
+
|
|
1005
|
+
if [[ "$verb" == "start" ]]; then
|
|
1006
|
+
# First positional after verb is runtime.
|
|
1007
|
+
if [[ $# -gt 0 && "$1" != -* ]]; then
|
|
1008
|
+
_dc_runtime="$1"
|
|
1009
|
+
shift
|
|
1010
|
+
fi
|
|
1011
|
+
fi
|
|
588
1012
|
|
|
589
|
-
# ---- status -----------------------------------------------------------------
|
|
590
|
-
if [[ "$SUBCMD" == "status" ]]; then
|
|
591
|
-
# Parse flags for status.
|
|
592
1013
|
while [[ $# -gt 0 ]]; do
|
|
593
1014
|
case "$1" in
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
1015
|
+
-h|--help)
|
|
1016
|
+
_aid_usage dashboard
|
|
1017
|
+
exit 0
|
|
1018
|
+
;;
|
|
1019
|
+
--verbose) _dc_verbose=1; shift ;;
|
|
1020
|
+
--remote)
|
|
1021
|
+
if [[ "$verb" == "stop" ]]; then
|
|
1022
|
+
echo "ERROR: aid: dashboard: unknown flag: $1" >&2; exit 2
|
|
1023
|
+
fi
|
|
1024
|
+
_dc_remote=1; shift ;;
|
|
1025
|
+
--port)
|
|
1026
|
+
if [[ "$verb" == "stop" ]]; then
|
|
1027
|
+
echo "ERROR: aid: dashboard: unknown flag: $1" >&2; exit 2
|
|
1028
|
+
fi
|
|
1029
|
+
[[ $# -lt 2 ]] && _aid_die "dashboard: --port requires a value" 2
|
|
1030
|
+
_dc_port="$2"; shift 2
|
|
1031
|
+
# Validate port: integer in 1024..65535.
|
|
1032
|
+
if ! [[ "$_dc_port" =~ ^[0-9]+$ ]] || [[ "$_dc_port" -lt 1024 || "$_dc_port" -gt 65535 ]]; then
|
|
1033
|
+
echo "ERROR: aid: dashboard: --port must be an integer in 1024..65535" >&2
|
|
1034
|
+
exit 2
|
|
1035
|
+
fi
|
|
1036
|
+
;;
|
|
1037
|
+
-*)
|
|
1038
|
+
echo "ERROR: aid: dashboard: unknown flag: $1" >&2; exit 2 ;;
|
|
1039
|
+
*)
|
|
1040
|
+
if [[ "$verb" == "stop" ]]; then
|
|
1041
|
+
echo "ERROR: aid: dashboard: unknown flag: $1" >&2; exit 2
|
|
1042
|
+
fi
|
|
1043
|
+
# Stray positional on start after runtime was consumed.
|
|
1044
|
+
echo "ERROR: aid: dashboard: unknown flag: $1" >&2; exit 2 ;;
|
|
601
1045
|
esac
|
|
602
1046
|
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
1047
|
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
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 $?
|
|
1048
|
+
if [[ "$verb" == "start" ]]; then
|
|
1049
|
+
_dc_start "$_dc_runtime" "$_dc_port" "$_dc_remote" "$_dc_verbose"
|
|
1050
|
+
else
|
|
1051
|
+
_dc_stop "$_dc_verbose"
|
|
629
1052
|
fi
|
|
630
|
-
|
|
631
|
-
fi
|
|
1053
|
+
}
|
|
632
1054
|
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
1055
|
+
_dc_start() {
|
|
1056
|
+
local runtime="$1"
|
|
1057
|
+
local port="$2"
|
|
1058
|
+
local remote="$3"
|
|
1059
|
+
local verbose="$4"
|
|
1060
|
+
|
|
1061
|
+
# Step 1: validate runtime.
|
|
1062
|
+
if [[ -z "$runtime" ]]; then
|
|
1063
|
+
echo "ERROR: aid: dashboard start requires a runtime: node or python (e.g. aid dashboard start python)" >&2
|
|
1064
|
+
exit 2
|
|
1065
|
+
fi
|
|
1066
|
+
if [[ "$runtime" != "node" && "$runtime" != "python" ]]; then
|
|
1067
|
+
echo "ERROR: aid: dashboard: unknown runtime '${runtime}' (expected: node or python)" >&2
|
|
1068
|
+
exit 2
|
|
640
1069
|
fi
|
|
641
1070
|
|
|
642
|
-
#
|
|
643
|
-
#
|
|
644
|
-
|
|
1071
|
+
# pid/log live in the per-user state home (.temp), always writable.
|
|
1072
|
+
# FR10 precedent: always per-user $HOME/.aid, never AID_STATE_HOME on global installs.
|
|
1073
|
+
local pid_file="${HOME}/.aid/.temp/dashboard.pid"
|
|
1074
|
+
local log_file="${HOME}/.aid/.temp/dashboard.log"
|
|
1075
|
+
|
|
1076
|
+
# Step 4: already-running guard (stale-record reclaim included).
|
|
1077
|
+
if [[ -f "$pid_file" ]]; then
|
|
1078
|
+
local existing_pid existing_port existing_runtime
|
|
1079
|
+
existing_pid="$(grep '"pid"' "$pid_file" | sed 's/[^0-9]*\([0-9]*\).*/\1/')"
|
|
1080
|
+
existing_port="$(grep '"port"' "$pid_file" | sed 's/[^0-9]*\([0-9]*\).*/\1/')"
|
|
1081
|
+
existing_runtime="$(grep '"runtime"' "$pid_file" | sed 's/.*"runtime": *"\([^"]*\)".*/\1/')"
|
|
1082
|
+
if [[ -n "$existing_pid" ]] && kill -0 "$existing_pid" 2>/dev/null; then
|
|
1083
|
+
echo "aid: dashboard already running (runtime ${existing_runtime}, http://127.0.0.1:${existing_port}); run 'aid dashboard stop' first."
|
|
1084
|
+
exit 8
|
|
1085
|
+
else
|
|
1086
|
+
# Stale record: reclaim silently (or verbosely).
|
|
1087
|
+
[[ "$verbose" -eq 1 ]] && echo "aid: dashboard: reclaiming stale record (pid ${existing_pid} is dead)" >&2
|
|
1088
|
+
rm -f "$pid_file" "$log_file"
|
|
1089
|
+
fi
|
|
1090
|
+
fi
|
|
645
1091
|
|
|
646
|
-
#
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
1092
|
+
# Step 5: check runtime on PATH.
|
|
1093
|
+
local interp
|
|
1094
|
+
if [[ "$runtime" == "python" ]]; then
|
|
1095
|
+
interp="python3"
|
|
1096
|
+
if ! command -v python3 >/dev/null 2>&1; then
|
|
1097
|
+
echo "ERROR: aid: dashboard: python3 not found on PATH (install it, or try: aid dashboard start node)" >&2
|
|
1098
|
+
exit 9
|
|
1099
|
+
fi
|
|
1100
|
+
else
|
|
1101
|
+
interp="node"
|
|
1102
|
+
if ! command -v node >/dev/null 2>&1; then
|
|
1103
|
+
echo "ERROR: aid: dashboard: node not found on PATH (install it, or try: aid dashboard start python)" >&2
|
|
1104
|
+
exit 9
|
|
1105
|
+
fi
|
|
1106
|
+
fi
|
|
651
1107
|
|
|
652
|
-
#
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
1108
|
+
# Step 6: locate the server entry point.
|
|
1109
|
+
# <assets> = $AID_CODE_HOME/dashboard (the co-vendored server+reader unit in the install tree).
|
|
1110
|
+
local assets_dir="${AID_CODE_HOME}/dashboard"
|
|
1111
|
+
local entry_point
|
|
1112
|
+
if [[ "$runtime" == "python" ]]; then
|
|
1113
|
+
entry_point="${assets_dir}/server/server.py"
|
|
1114
|
+
else
|
|
1115
|
+
entry_point="${assets_dir}/server/server.mjs"
|
|
1116
|
+
fi
|
|
1117
|
+
if [[ ! -f "$entry_point" ]]; then
|
|
1118
|
+
echo "ERROR: aid: dashboard: the dashboard server is missing from the install tree (${runtime} entry-point not found at ${entry_point}); run 'aid update' or reinstall aid" >&2
|
|
1119
|
+
exit 7
|
|
1120
|
+
fi
|
|
660
1121
|
|
|
1122
|
+
# Ensure log dir exists (per-user state home, always writable).
|
|
1123
|
+
mkdir -p "${HOME}/.aid/.temp"
|
|
1124
|
+
|
|
1125
|
+
# Step 7: spawn the server child in a new session (clean process-group kill on stop).
|
|
1126
|
+
# SEC-1: literal 127.0.0.1 -- never read from input/config/env.
|
|
1127
|
+
# The multi-repo server (feature-010) serves every registered repo from the
|
|
1128
|
+
# registry under AID_STATE_HOME; export AID_HOME=AID_STATE_HOME so the server
|
|
1129
|
+
# resolves the registry via its legacy AID_HOME env var (delivery-008 seam).
|
|
1130
|
+
AID_HOME="$AID_STATE_HOME" setsid "$interp" "$entry_point" --host 127.0.0.1 --port "$port" \
|
|
1131
|
+
>"$log_file" 2>&1 &
|
|
1132
|
+
local child_pid=$!
|
|
1133
|
+
|
|
1134
|
+
[[ "$verbose" -eq 1 ]] && echo "aid: dashboard: spawned ${runtime} server (pid ${child_pid}, port ${port})" >&2
|
|
1135
|
+
|
|
1136
|
+
# Step 8: bounded readiness wait (~5s, poll TCP socket).
|
|
1137
|
+
local ready=0
|
|
1138
|
+
local attempts=0
|
|
1139
|
+
local max_attempts=50 # 50 x 0.1s = 5s
|
|
1140
|
+
while [[ "$attempts" -lt "$max_attempts" ]]; do
|
|
1141
|
+
# Check child is still alive.
|
|
1142
|
+
if ! kill -0 "$child_pid" 2>/dev/null; then
|
|
1143
|
+
# Child exited early.
|
|
1144
|
+
echo "ERROR: aid: dashboard: server failed to start; last log lines:" >&2
|
|
1145
|
+
tail -n 10 "$log_file" >&2 2>/dev/null || true
|
|
1146
|
+
rm -f "$log_file"
|
|
1147
|
+
exit 3
|
|
1148
|
+
fi
|
|
1149
|
+
# Try TCP connect to 127.0.0.1:<port>.
|
|
1150
|
+
if (: < /dev/tcp/127.0.0.1/"$port") 2>/dev/null; then
|
|
1151
|
+
ready=1
|
|
1152
|
+
break
|
|
1153
|
+
fi
|
|
1154
|
+
sleep 0.1
|
|
1155
|
+
attempts=$((attempts + 1))
|
|
1156
|
+
done
|
|
661
1157
|
|
|
662
|
-
#
|
|
1158
|
+
# Check if child is still alive even if not ready (timeout case).
|
|
1159
|
+
if [[ "$ready" -eq 0 ]]; then
|
|
1160
|
+
if ! kill -0 "$child_pid" 2>/dev/null; then
|
|
1161
|
+
echo "ERROR: aid: dashboard: server failed to start; last log lines:" >&2
|
|
1162
|
+
tail -n 10 "$log_file" >&2 2>/dev/null || true
|
|
1163
|
+
rm -f "$log_file"
|
|
1164
|
+
exit 3
|
|
1165
|
+
fi
|
|
1166
|
+
# Timeout but pid alive: warn and continue (child may be slow on a large repo).
|
|
1167
|
+
echo "WARN: aid: dashboard: server started but not yet responding on :${port}; check ${log_file}" >&2
|
|
1168
|
+
fi
|
|
1169
|
+
|
|
1170
|
+
# Step 9: write dashboard.pid JSON record (DM-1) with remote=false initially.
|
|
1171
|
+
local started_at
|
|
1172
|
+
started_at="$(date -u +%Y-%m-%dT%H:%M:%SZ 2>/dev/null || echo "unknown")"
|
|
1173
|
+
cat > "$pid_file" <<EOF
|
|
1174
|
+
{
|
|
1175
|
+
"schema": 1,
|
|
1176
|
+
"pid": ${child_pid},
|
|
1177
|
+
"runtime": "${runtime}",
|
|
1178
|
+
"port": ${port},
|
|
1179
|
+
"bind": "127.0.0.1",
|
|
1180
|
+
"remote": false,
|
|
1181
|
+
"remote_handle": null,
|
|
1182
|
+
"started_at": "${started_at}",
|
|
1183
|
+
"logfile": "${log_file}"
|
|
1184
|
+
}
|
|
1185
|
+
EOF
|
|
1186
|
+
|
|
1187
|
+
# Step 10: --remote: invoke _aid_remote_expose; update record on success.
|
|
1188
|
+
if [[ "$remote" -eq 1 ]]; then
|
|
1189
|
+
# Capture stdout into a temp file; let stderr (guidance + errors) flow to the user.
|
|
1190
|
+
local _expose_tmp expose_rc expose_handle expose_url
|
|
1191
|
+
_expose_tmp="$(mktemp)"
|
|
1192
|
+
_aid_remote_expose "$port" >"$_expose_tmp"
|
|
1193
|
+
expose_rc=$?
|
|
1194
|
+
if [[ "$expose_rc" -ne 0 ]]; then
|
|
1195
|
+
rm -f "$_expose_tmp"
|
|
1196
|
+
# All expose failures (10/11/12) map to user-facing exit 10.
|
|
1197
|
+
# dashboard stays local-only (server remains running).
|
|
1198
|
+
echo "ERROR: aid: dashboard: --remote requested but the secure remote-exposure mechanism is not available on this host; the dashboard is NOT exposed. Local server still running at http://127.0.0.1:${port}." >&2
|
|
1199
|
+
exit 10
|
|
1200
|
+
fi
|
|
1201
|
+
expose_handle="$(head -1 "$_expose_tmp")"
|
|
1202
|
+
expose_url="$(sed -n '2p' "$_expose_tmp")"
|
|
1203
|
+
rm -f "$_expose_tmp"
|
|
1204
|
+
# Update the record with remote=true and the handle.
|
|
1205
|
+
cat > "$pid_file" <<EOF
|
|
1206
|
+
{
|
|
1207
|
+
"schema": 1,
|
|
1208
|
+
"pid": ${child_pid},
|
|
1209
|
+
"runtime": "${runtime}",
|
|
1210
|
+
"port": ${port},
|
|
1211
|
+
"bind": "127.0.0.1",
|
|
1212
|
+
"remote": true,
|
|
1213
|
+
"remote_handle": "${expose_handle}",
|
|
1214
|
+
"started_at": "${started_at}",
|
|
1215
|
+
"logfile": "${log_file}"
|
|
1216
|
+
}
|
|
1217
|
+
EOF
|
|
1218
|
+
# Step 11 (remote success): print local URL + remote URL.
|
|
1219
|
+
echo "Dashboard (${runtime}) running at http://127.0.0.1:${port} -- stop with: aid dashboard stop"
|
|
1220
|
+
if [[ "${expose_url}" == https://* ]]; then
|
|
1221
|
+
echo "Remote (private): ${expose_url}"
|
|
1222
|
+
else
|
|
1223
|
+
echo "Remote exposure is UP (tailnet-private), but the .ts.net URL could not be auto-detected -- run 'tailscale status' on this host to find it."
|
|
1224
|
+
fi
|
|
1225
|
+
exit 0
|
|
1226
|
+
fi
|
|
1227
|
+
|
|
1228
|
+
# Step 11: print success (local-only).
|
|
1229
|
+
echo "Dashboard (${runtime}) running at http://127.0.0.1:${port} -- stop with: aid dashboard stop"
|
|
1230
|
+
exit 0
|
|
1231
|
+
}
|
|
1232
|
+
|
|
1233
|
+
_dc_stop() {
|
|
1234
|
+
local verbose="$1"
|
|
1235
|
+
|
|
1236
|
+
local pid_file="${HOME}/.aid/.temp/dashboard.pid"
|
|
1237
|
+
|
|
1238
|
+
# Step 3: read record; absent or stale -> idempotent exit 0.
|
|
1239
|
+
if [[ ! -f "$pid_file" ]]; then
|
|
1240
|
+
echo "aid: dashboard: not running (nothing to stop)."
|
|
1241
|
+
exit 0
|
|
1242
|
+
fi
|
|
1243
|
+
|
|
1244
|
+
local existing_pid
|
|
1245
|
+
existing_pid="$(grep '"pid"' "$pid_file" | sed 's/[^0-9]*\([0-9]*\).*/\1/')"
|
|
1246
|
+
local log_file
|
|
1247
|
+
log_file="$(grep '"logfile"' "$pid_file" | sed 's/.*"logfile": *"\([^"]*\)".*/\1/')"
|
|
1248
|
+
|
|
1249
|
+
if [[ -z "$existing_pid" ]] || ! kill -0 "$existing_pid" 2>/dev/null; then
|
|
1250
|
+
[[ "$verbose" -eq 1 ]] && echo "aid: dashboard: record exists but pid ${existing_pid} is dead; cleaning up." >&2
|
|
1251
|
+
rm -f "$pid_file" "$log_file"
|
|
1252
|
+
echo "aid: dashboard: not running (nothing to stop)."
|
|
1253
|
+
exit 0
|
|
1254
|
+
fi
|
|
1255
|
+
|
|
1256
|
+
# Step 4: --remote teardown (if the record says remote=true, call _aid_remote_teardown).
|
|
1257
|
+
local existing_remote existing_handle
|
|
1258
|
+
existing_remote="$(grep '"remote":' "$pid_file" | grep -v '"remote_handle"' | sed 's/.*"remote":[[:space:]]*//' | tr -d '", ')"
|
|
1259
|
+
# Extract handle: matches quoted string value; if unquoted (null), returns empty.
|
|
1260
|
+
existing_handle="$(grep '"remote_handle"' "$pid_file" | sed -n 's/.*"remote_handle":[[:space:]]*"\([^"]*\)".*/\1/p')"
|
|
1261
|
+
if [[ "$existing_remote" == "true" && -n "$existing_handle" ]]; then
|
|
1262
|
+
[[ "$verbose" -eq 1 ]] && echo "aid: dashboard: tearing down remote exposure (handle: ${existing_handle})" >&2
|
|
1263
|
+
_aid_remote_teardown "$existing_handle"
|
|
1264
|
+
local teardown_rc=$?
|
|
1265
|
+
if [[ "$teardown_rc" -eq 13 ]]; then
|
|
1266
|
+
echo "WARN: aid: dashboard: remote teardown reported a warning; continuing server shutdown" >&2
|
|
1267
|
+
fi
|
|
1268
|
+
fi
|
|
1269
|
+
|
|
1270
|
+
# Step 5: terminate the process group cleanly.
|
|
1271
|
+
[[ "$verbose" -eq 1 ]] && echo "aid: dashboard: sending SIGTERM to process group ${existing_pid}" >&2
|
|
1272
|
+
kill -TERM -"$existing_pid" 2>/dev/null || true
|
|
1273
|
+
|
|
1274
|
+
# Wait up to ~5s for exit.
|
|
1275
|
+
local waited=0
|
|
1276
|
+
while kill -0 "$existing_pid" 2>/dev/null && [[ "$waited" -lt 50 ]]; do
|
|
1277
|
+
sleep 0.1
|
|
1278
|
+
waited=$((waited + 1))
|
|
1279
|
+
done
|
|
1280
|
+
|
|
1281
|
+
# Escalate to SIGKILL if still alive.
|
|
1282
|
+
if kill -0 "$existing_pid" 2>/dev/null; then
|
|
1283
|
+
[[ "$verbose" -eq 1 ]] && echo "aid: dashboard: escalating to SIGKILL on process group ${existing_pid}" >&2
|
|
1284
|
+
kill -KILL -"$existing_pid" 2>/dev/null || true
|
|
1285
|
+
fi
|
|
1286
|
+
|
|
1287
|
+
# Step 6: remove record and logfile, print success.
|
|
1288
|
+
rm -f "$pid_file" "$log_file"
|
|
1289
|
+
echo "aid: dashboard stopped."
|
|
1290
|
+
exit 0
|
|
1291
|
+
}
|
|
1292
|
+
|
|
1293
|
+
# ---------------------------------------------------------------------------
|
|
1294
|
+
# Registry helpers (DR-1 / FF-1 / FR29).
|
|
1295
|
+
# Implements DM-1 schema, DD-3 atomic write, DD-REG-FMT line-scan.
|
|
1296
|
+
# ---------------------------------------------------------------------------
|
|
1297
|
+
|
|
1298
|
+
# _registry_read_repos <reg-path>
|
|
1299
|
+
# Print newline-delimited canonical repo paths recorded in registry.yml.
|
|
1300
|
+
# Returns nothing (empty) when the file is absent or has no items.
|
|
1301
|
+
_registry_read_repos() {
|
|
1302
|
+
local reg="$1"
|
|
1303
|
+
[[ -f "$reg" ]] || return 0
|
|
1304
|
+
grep -E '^[[:space:]]*-[[:space:]]+' "$reg" 2>/dev/null \
|
|
1305
|
+
| sed -E 's/^[[:space:]]*-[[:space:]]+//' \
|
|
1306
|
+
| sed -E 's/[[:space:]]+$//'
|
|
1307
|
+
}
|
|
1308
|
+
|
|
1309
|
+
# _registry_read_union
|
|
1310
|
+
# Return the deduped sort -u union of the primary tier ($AID_STATE_HOME/registry.yml,
|
|
1311
|
+
# which honors the AID_HOME override via the startup scope derivation) and, when
|
|
1312
|
+
# $AID_STATE_HOME differs from $HOME/.aid, also the $HOME/.aid/registry.yml
|
|
1313
|
+
# fallback tier (entries that may have been written there when AID_STATE_HOME was
|
|
1314
|
+
# non-writable). Prunes stale entries quietly: a path is emitted only if
|
|
1315
|
+
# [[ -d "$p/.aid" ]]. Never writes or mutates any registry file on read.
|
|
1316
|
+
#
|
|
1317
|
+
# Per-user collapse: when $AID_STATE_HOME == $HOME/.aid the two paths are the
|
|
1318
|
+
# same file -- the union degenerates to a single-tier read (no double-read, no
|
|
1319
|
+
# elevation).
|
|
1320
|
+
_registry_read_union() {
|
|
1321
|
+
local _primary_reg="${AID_STATE_HOME}/registry.yml"
|
|
1322
|
+
local _raw
|
|
1323
|
+
if [[ "$AID_STATE_HOME" == "${HOME}/.aid" ]]; then
|
|
1324
|
+
# Per-user collapse: single-tier; primary == fallback, no double-read.
|
|
1325
|
+
_raw="$(_registry_read_repos "$_primary_reg")"
|
|
1326
|
+
else
|
|
1327
|
+
# Distinct paths: union of primary ($AID_STATE_HOME) and fallback ($HOME/.aid).
|
|
1328
|
+
local _fallback_reg="${HOME}/.aid/registry.yml"
|
|
1329
|
+
_raw="$({ _registry_read_repos "$_primary_reg"; _registry_read_repos "$_fallback_reg"; } \
|
|
1330
|
+
| sed '/^$/d' | sort -u)"
|
|
1331
|
+
fi
|
|
1332
|
+
# Quiet-prune: emit only paths whose .aid/ still exists.
|
|
1333
|
+
while IFS= read -r p; do
|
|
1334
|
+
[[ -n "$p" ]] || continue
|
|
1335
|
+
[[ -d "${p}/.aid" ]] && printf '%s\n' "$p"
|
|
1336
|
+
done <<< "$_raw"
|
|
1337
|
+
}
|
|
1338
|
+
|
|
1339
|
+
# _registry_read_raw_union
|
|
1340
|
+
# Like _registry_read_union but WITHOUT the [[ -d "$p/.aid" ]] quiet-prune.
|
|
1341
|
+
# Returns EVERY registered path (deduped union of $AID_STATE_HOME/registry.yml
|
|
1342
|
+
# and the $HOME/.aid/registry.yml fallback, with per-user collapse), including
|
|
1343
|
+
# paths whose .aid/ is absent or the directory does not exist.
|
|
1344
|
+
# Used by 'aid projects list' to render no-aid/missing/untracked states.
|
|
1345
|
+
# Never writes or mutates any registry file on read.
|
|
1346
|
+
_registry_read_raw_union() {
|
|
1347
|
+
local _primary_reg="${AID_STATE_HOME}/registry.yml"
|
|
1348
|
+
local _raw
|
|
1349
|
+
if [[ "$AID_STATE_HOME" == "${HOME}/.aid" ]]; then
|
|
1350
|
+
# Per-user collapse: single-tier; primary == fallback, no double-read.
|
|
1351
|
+
_raw="$(_registry_read_repos "$_primary_reg")"
|
|
1352
|
+
else
|
|
1353
|
+
# Distinct paths: union of primary ($AID_STATE_HOME) and fallback ($HOME/.aid).
|
|
1354
|
+
local _fallback_reg="${HOME}/.aid/registry.yml"
|
|
1355
|
+
_raw="$({ _registry_read_repos "$_primary_reg"; _registry_read_repos "$_fallback_reg"; } \
|
|
1356
|
+
| sed '/^$/d' | sort -u)"
|
|
1357
|
+
fi
|
|
1358
|
+
# Emit every non-empty path (no prune -- no-aid/missing paths are included).
|
|
1359
|
+
while IFS= read -r p; do
|
|
1360
|
+
[[ -n "$p" ]] || continue
|
|
1361
|
+
printf '%s\n' "$p"
|
|
1362
|
+
done <<< "$_raw"
|
|
1363
|
+
}
|
|
1364
|
+
|
|
1365
|
+
# _aid_resolve_tier <canon-path>
|
|
1366
|
+
# Deterministic, non-interactive tier selection for 'aid projects add' (FR6/AC6).
|
|
1367
|
+
# Returns "user" or "shared" on stdout.
|
|
1368
|
+
#
|
|
1369
|
+
# Auto rule:
|
|
1370
|
+
# - Returns "user" if $_AID_SCOPE != "global" (per-user install), OR if the
|
|
1371
|
+
# path is under $HOME (any install type).
|
|
1372
|
+
# - Otherwise (global install AND path outside $HOME): returns "shared".
|
|
1373
|
+
#
|
|
1374
|
+
# Override convention (set before calling; cleared by caller):
|
|
1375
|
+
# _AID_TIER_OVERRIDE="" no override, use auto rule (default)
|
|
1376
|
+
# _AID_TIER_OVERRIDE="--local" force "user" regardless of install type/path
|
|
1377
|
+
# _AID_TIER_OVERRIDE="--shared" force "shared"; but on a per-user install
|
|
1378
|
+
# ($AID_STATE_HOME == $HOME/.aid) there is no
|
|
1379
|
+
# separate shared tier -- returns "user" and
|
|
1380
|
+
# prints a one-line notice to stderr.
|
|
1381
|
+
#
|
|
1382
|
+
# Never prompts; never blocks; always returns 0.
|
|
1383
|
+
_aid_resolve_tier() {
|
|
1384
|
+
local _canon_path="$1"
|
|
1385
|
+
|
|
1386
|
+
# Detect per-user install (no separate shared tier).
|
|
1387
|
+
local _per_user=0
|
|
1388
|
+
[[ "$AID_STATE_HOME" == "${HOME}/.aid" ]] && _per_user=1
|
|
1389
|
+
|
|
1390
|
+
# Handle explicit override flags.
|
|
1391
|
+
case "${_AID_TIER_OVERRIDE:-}" in
|
|
1392
|
+
--local)
|
|
1393
|
+
printf 'user\n'
|
|
1394
|
+
return 0
|
|
1395
|
+
;;
|
|
1396
|
+
--shared)
|
|
1397
|
+
if [[ "$_per_user" -eq 1 ]]; then
|
|
1398
|
+
printf 'no shared tier under a per-user install; using user tier\n' >&2
|
|
1399
|
+
printf 'user\n'
|
|
1400
|
+
else
|
|
1401
|
+
printf 'shared\n'
|
|
1402
|
+
fi
|
|
1403
|
+
return 0
|
|
1404
|
+
;;
|
|
1405
|
+
esac
|
|
1406
|
+
|
|
1407
|
+
# Auto rule: user if per-user install OR path is under $HOME.
|
|
1408
|
+
local _in_home=0
|
|
1409
|
+
case "$_canon_path" in
|
|
1410
|
+
"${HOME}/"*|"${HOME}") _in_home=1 ;;
|
|
1411
|
+
esac
|
|
1412
|
+
|
|
1413
|
+
if [[ "$_AID_SCOPE" != "global" || "$_in_home" -eq 1 ]]; then
|
|
1414
|
+
printf 'user\n'
|
|
1415
|
+
else
|
|
1416
|
+
printf 'shared\n'
|
|
1417
|
+
fi
|
|
1418
|
+
return 0
|
|
1419
|
+
}
|
|
1420
|
+
|
|
1421
|
+
# _aid_project_state <path>
|
|
1422
|
+
# Print the state of an AID project directory:
|
|
1423
|
+
# "missing" -- the directory does not exist
|
|
1424
|
+
# "no-aid" -- directory exists but has no .aid/ subdirectory
|
|
1425
|
+
# "untracked" -- .aid/ exists but no .aid/.aid-manifest.json is present
|
|
1426
|
+
# "vX.Y.Z" -- tracked; semver version string from .aid/.aid-manifest.json
|
|
1427
|
+
# (key "aid_version"), falling back to .aid/.aid-version
|
|
1428
|
+
# The version is extracted by the same semver regex on both paths -- a malformed
|
|
1429
|
+
# aid_version value in the manifest falls through to untracked (not returned raw).
|
|
1430
|
+
# Never errors; always returns 0.
|
|
1431
|
+
_aid_project_state() {
|
|
1432
|
+
local _path="$1"
|
|
1433
|
+
if [[ ! -d "$_path" ]]; then
|
|
1434
|
+
printf 'missing\n'
|
|
1435
|
+
return 0
|
|
1436
|
+
fi
|
|
1437
|
+
if [[ ! -d "${_path}/.aid" ]]; then
|
|
1438
|
+
printf 'no-aid\n'
|
|
1439
|
+
return 0
|
|
1440
|
+
fi
|
|
1441
|
+
local _manifest="${_path}/.aid/.aid-manifest.json"
|
|
1442
|
+
local _ver_file="${_path}/.aid/.aid-version"
|
|
1443
|
+
if [[ -f "$_manifest" ]]; then
|
|
1444
|
+
local _ver
|
|
1445
|
+
# Extract aid_version value; then validate as semver (same guard as .aid-version branch).
|
|
1446
|
+
_ver="$(grep -o '"aid_version"[[:space:]]*:[[:space:]]*"[^"]*"' "$_manifest" 2>/dev/null \
|
|
1447
|
+
| sed -E 's/.*"([^"]+)"[[:space:]]*$/\1/' \
|
|
1448
|
+
| grep -Eo '[0-9]+\.[0-9]+\.[0-9]+[^[:space:]]*' \
|
|
1449
|
+
| head -1)"
|
|
1450
|
+
if [[ -n "$_ver" ]]; then
|
|
1451
|
+
printf '%s\n' "$_ver"
|
|
1452
|
+
return 0
|
|
1453
|
+
fi
|
|
1454
|
+
fi
|
|
1455
|
+
# Fallback: .aid/.aid-version plain-text file.
|
|
1456
|
+
if [[ -f "$_ver_file" ]]; then
|
|
1457
|
+
local _vf_ver
|
|
1458
|
+
_vf_ver="$(grep -Eo '[0-9]+\.[0-9]+\.[0-9]+[^[:space:]]*' "$_ver_file" 2>/dev/null | head -1)"
|
|
1459
|
+
if [[ -n "$_vf_ver" ]]; then
|
|
1460
|
+
printf '%s\n' "$_vf_ver"
|
|
1461
|
+
return 0
|
|
1462
|
+
fi
|
|
1463
|
+
fi
|
|
1464
|
+
printf 'untracked\n'
|
|
1465
|
+
return 0
|
|
1466
|
+
}
|
|
1467
|
+
|
|
1468
|
+
# _aid_project_tools <path>
|
|
1469
|
+
# Print a comma-separated list of tool names installed in an AID project, as
|
|
1470
|
+
# recorded in <path>/.aid/.aid-manifest.json under the "tools" object.
|
|
1471
|
+
# The manifest schema is: "tools": { "<tool-name>": { ... }, ... } (object keyed
|
|
1472
|
+
# by tool name, NO "name" field inside). This is the schema written by every
|
|
1473
|
+
# canonical writer in lib/aid-install-core.sh (see the awk extractor at ~:1019).
|
|
1474
|
+
# Prints an empty string when the manifest is absent or has no tools.
|
|
1475
|
+
# Used by 'aid projects list' to populate the "tools" column (task-004).
|
|
1476
|
+
_aid_project_tools() {
|
|
1477
|
+
local _path="$1"
|
|
1478
|
+
local _manifest="${_path}/.aid/.aid-manifest.json"
|
|
1479
|
+
[[ -f "$_manifest" ]] || { printf ''; return 0; }
|
|
1480
|
+
# Extract tool names as object keys inside the "tools": { ... } block.
|
|
1481
|
+
# Mirrors the canonical awk extractor in lib/aid-install-core.sh:~1019:
|
|
1482
|
+
# /"tools"/{found=1} found && /^ "[a-z]/{gsub(/[^a-zA-Z-]/,"",$1); print $1}
|
|
1483
|
+
local _tools
|
|
1484
|
+
_tools="$(awk '/"tools"/{found=1} found && /^ "[a-z]/{gsub(/[^a-zA-Z0-9_.-]/,"",$1); if ($1!="") print $1}' \
|
|
1485
|
+
"$_manifest" 2>/dev/null \
|
|
1486
|
+
| sort -u \
|
|
1487
|
+
| tr '\n' ',' \
|
|
1488
|
+
| sed -E 's/,+$//')"
|
|
1489
|
+
printf '%s' "$_tools"
|
|
1490
|
+
return 0
|
|
1491
|
+
}
|
|
1492
|
+
|
|
1493
|
+
# registry_register <canon-path> [<tier>]
|
|
1494
|
+
# Set-insert <canon-path> into the target tier registry (idempotent; atomic write).
|
|
1495
|
+
# <tier> is "user" (default) or "shared".
|
|
1496
|
+
# On a real change prints one concise line. On failure prints WARN and returns 0
|
|
1497
|
+
# so the host-tool op is never blocked (NFR10 / DD-3 / CLI-1).
|
|
1498
|
+
#
|
|
1499
|
+
# USER tier (default): primary target is $AID_STATE_HOME/registry.yml (which
|
|
1500
|
+
# honors the AID_HOME override via the startup scope derivation). If AID_STATE_HOME
|
|
1501
|
+
# is not user-writable AND is a different path from $HOME/.aid, degrades to
|
|
1502
|
+
# $HOME/.aid/registry.yml with a WARN (fire-and-continue; never blocks the host
|
|
1503
|
+
# command). Per-user collapse: when $AID_STATE_HOME == $HOME/.aid the two paths
|
|
1504
|
+
# are the same file -- single-tier, no fallback needed.
|
|
1505
|
+
#
|
|
1506
|
+
# SHARED tier: writes to $AID_STATE_HOME/registry.yml using a REAL probe of the
|
|
1507
|
+
# shared dir via _aid_priv_run. If elevation is declined or there is no TTY,
|
|
1508
|
+
# the function degrades: skip + WARN + return 0 (the host command is NOT blocked;
|
|
1509
|
+
# design SS3.3 decision #2 / SPEC AC6).
|
|
1510
|
+
# Per-user install ($AID_STATE_HOME == ~/.aid): shared-tier argument is treated
|
|
1511
|
+
# as user-tier (same file, no elevation needed).
|
|
1512
|
+
registry_register() {
|
|
1513
|
+
local repo="$1" tier="${2:-user}" reg tmp existing
|
|
1514
|
+
local _shared_reg_dir="${AID_STATE_HOME}"
|
|
1515
|
+
local _shared_reg="${AID_STATE_HOME}/registry.yml"
|
|
1516
|
+
# Per-user collapse: AID_STATE_HOME is the same path as $HOME/.aid.
|
|
1517
|
+
# In this case shared-tier is treated as user-tier (same file, no elevation).
|
|
1518
|
+
local _per_user=0
|
|
1519
|
+
[[ "$AID_STATE_HOME" == "${HOME}/.aid" ]] && _per_user=1
|
|
1520
|
+
if [[ "$tier" == "shared" && "$_per_user" -eq 0 ]]; then
|
|
1521
|
+
# SHARED-tier write: real probe of the shared dir; elevation allowed but
|
|
1522
|
+
# degrades on decline / no-TTY rather than blocking.
|
|
1523
|
+
# Ensure the shared dir exists (non-prompting best-effort).
|
|
1524
|
+
_aid_priv_run "" mkdir -p "$_shared_reg_dir" 2>/dev/null || true
|
|
1525
|
+
if [[ ! -w "$_shared_reg_dir" ]]; then
|
|
1526
|
+
# Shared dir not writable; real probe will elevate via sudo if available.
|
|
1527
|
+
# We attempt the write via _aid_priv_run with the REAL probe (not empty).
|
|
1528
|
+
# If elevation is declined or sudo is unavailable, _aid_priv_run returns
|
|
1529
|
+
# non-zero -- we catch that and degrade: skip + warn + return 0.
|
|
1530
|
+
existing="$(_registry_read_repos "$_shared_reg")"
|
|
1531
|
+
if printf '%s\n' "$existing" | grep -qxF "$repo"; then
|
|
1532
|
+
[[ "$_AID_VERBOSE" == "1" ]] && echo "Registry: ${repo} already registered in shared tier (no-op)."
|
|
1533
|
+
return 0
|
|
1534
|
+
fi
|
|
1535
|
+
tmp="$(mktemp "/tmp/.aid-reg-tmp.XXXXXX" 2>/dev/null)" || {
|
|
1536
|
+
echo "WARN: aid: could not update the shared project registry (${_shared_reg}): mktemp failed" >&2
|
|
1537
|
+
return 0
|
|
1538
|
+
}
|
|
1539
|
+
{
|
|
1540
|
+
printf '%s\n' "# AID machine project registry (managed by 'aid add' / 'aid remove' -- do not hand-edit)."
|
|
1541
|
+
printf '%s\n' "# Holds ONLY the base folders of projects this CLI install manages. Per-project name and"
|
|
1542
|
+
printf '%s\n' "# description come from .aid/settings.yml; version/tools from the manifest, at render time."
|
|
1543
|
+
printf '%s\n' "schema: 1"
|
|
1544
|
+
printf '%s\n' "projects:"
|
|
1545
|
+
{ printf '%s\n' "$existing"; printf '%s\n' "$repo"; } \
|
|
1546
|
+
| sed '/^$/d' | sort -u \
|
|
1547
|
+
| while IFS= read -r p; do printf ' - %s\n' "$p"; done
|
|
1548
|
+
} > "$tmp" || {
|
|
1549
|
+
rm -f "$tmp"
|
|
1550
|
+
echo "WARN: aid: could not update the shared project registry (${_shared_reg}): write failed" >&2
|
|
1551
|
+
return 0
|
|
1552
|
+
}
|
|
1553
|
+
# Real probe: elevates only when the shared dir is not user-writable.
|
|
1554
|
+
# If elevation is declined / no-TTY / sudo unavailable: skip + warn.
|
|
1555
|
+
_aid_priv_run "$_shared_reg_dir" mv -f "$tmp" "$_shared_reg" || {
|
|
1556
|
+
rm -f "$tmp" 2>/dev/null
|
|
1557
|
+
echo "WARN: aid: shared registry write declined or unavailable; project not registered in shared tier (${_shared_reg})" >&2
|
|
1558
|
+
return 0
|
|
1559
|
+
}
|
|
1560
|
+
else
|
|
1561
|
+
# Shared dir is user-writable (e.g. group-writable install or test sandbox).
|
|
1562
|
+
existing="$(_registry_read_repos "$_shared_reg")"
|
|
1563
|
+
if printf '%s\n' "$existing" | grep -qxF "$repo"; then
|
|
1564
|
+
[[ "$_AID_VERBOSE" == "1" ]] && echo "Registry: ${repo} already registered in shared tier (no-op)."
|
|
1565
|
+
return 0
|
|
1566
|
+
fi
|
|
1567
|
+
tmp="$(mktemp "${_shared_reg}.aid-tmp.XXXXXX" 2>/dev/null)" || {
|
|
1568
|
+
echo "WARN: aid: could not update the shared project registry (${_shared_reg}): mktemp failed" >&2
|
|
1569
|
+
return 0
|
|
1570
|
+
}
|
|
1571
|
+
{
|
|
1572
|
+
printf '%s\n' "# AID machine project registry (managed by 'aid add' / 'aid remove' -- do not hand-edit)."
|
|
1573
|
+
printf '%s\n' "# Holds ONLY the base folders of projects this CLI install manages. Per-project name and"
|
|
1574
|
+
printf '%s\n' "# description come from .aid/settings.yml; version/tools from the manifest, at render time."
|
|
1575
|
+
printf '%s\n' "schema: 1"
|
|
1576
|
+
printf '%s\n' "projects:"
|
|
1577
|
+
{ printf '%s\n' "$existing"; printf '%s\n' "$repo"; } \
|
|
1578
|
+
| sed '/^$/d' | sort -u \
|
|
1579
|
+
| while IFS= read -r p; do printf ' - %s\n' "$p"; done
|
|
1580
|
+
} > "$tmp" || {
|
|
1581
|
+
rm -f "$tmp"
|
|
1582
|
+
echo "WARN: aid: could not update the shared project registry (${_shared_reg}): write failed" >&2
|
|
1583
|
+
return 0
|
|
1584
|
+
}
|
|
1585
|
+
_aid_priv_run "" mv -f "$tmp" "$_shared_reg" || {
|
|
1586
|
+
rm -f "$tmp" 2>/dev/null
|
|
1587
|
+
echo "WARN: aid: could not update the shared project registry (${_shared_reg}): mv failed" >&2
|
|
1588
|
+
return 0
|
|
1589
|
+
}
|
|
1590
|
+
fi
|
|
1591
|
+
echo "Registered ${repo} with the AID CLI (shared registry)."
|
|
1592
|
+
return 0
|
|
1593
|
+
fi
|
|
1594
|
+
# USER tier (default) or per-user collapse.
|
|
1595
|
+
# Primary: $AID_STATE_HOME (honors AID_HOME override via startup scope derivation).
|
|
1596
|
+
# Fallback: $HOME/.aid (when AID_STATE_HOME is not writable and is a different path).
|
|
1597
|
+
# Never-elevate: empty probe + ensure-exists.
|
|
1598
|
+
_aid_priv_run "" mkdir -p "$AID_STATE_HOME" 2>/dev/null || true
|
|
1599
|
+
if [[ -w "$AID_STATE_HOME" ]]; then
|
|
1600
|
+
reg="${AID_STATE_HOME}/registry.yml"
|
|
1601
|
+
else
|
|
1602
|
+
# AID_STATE_HOME not writable; degrade to $HOME/.aid (user fallback).
|
|
1603
|
+
# This is the designed fallback for global installs -- silent by default,
|
|
1604
|
+
# visible under --verbose. Hard failures (mktemp/write/mv) stay unconditional.
|
|
1605
|
+
local _fb_dir="${HOME}/.aid"
|
|
1606
|
+
mkdir -p "$_fb_dir" 2>/dev/null || true
|
|
1607
|
+
[[ "${_AID_VERBOSE:-0}" == "1" ]] && \
|
|
1608
|
+
echo "WARN: aid: could not write to state home ${AID_STATE_HOME}; using ${_fb_dir}/registry.yml" >&2
|
|
1609
|
+
reg="${_fb_dir}/registry.yml"
|
|
1610
|
+
fi
|
|
1611
|
+
existing="$(_registry_read_repos "$reg")"
|
|
1612
|
+
# Idempotent: already registered -> silent no-op.
|
|
1613
|
+
if printf '%s\n' "$existing" | grep -qxF "$repo"; then
|
|
1614
|
+
[[ "$_AID_VERBOSE" == "1" ]] && echo "Registry: ${repo} already registered (no-op)."
|
|
1615
|
+
return 0
|
|
1616
|
+
fi
|
|
1617
|
+
# mktemp in the chosen (writable) target dir so mv is atomic same-filesystem.
|
|
1618
|
+
tmp="$(mktemp "${reg}.aid-tmp.XXXXXX" 2>/dev/null)" || {
|
|
1619
|
+
echo "WARN: aid: could not update the machine project registry (${reg}): mktemp failed" >&2
|
|
1620
|
+
return 0
|
|
1621
|
+
}
|
|
1622
|
+
{
|
|
1623
|
+
printf '%s\n' "# AID machine project registry (managed by 'aid add' / 'aid remove' -- do not hand-edit)."
|
|
1624
|
+
printf '%s\n' "# Holds ONLY the base folders of projects this CLI install manages. Per-project name and"
|
|
1625
|
+
printf '%s\n' "# description come from .aid/settings.yml; version/tools from the manifest, at render time."
|
|
1626
|
+
printf '%s\n' "schema: 1"
|
|
1627
|
+
printf '%s\n' "projects:"
|
|
1628
|
+
{ printf '%s\n' "$existing"; printf '%s\n' "$repo"; } \
|
|
1629
|
+
| sed '/^$/d' | sort -u \
|
|
1630
|
+
| while IFS= read -r p; do printf ' - %s\n' "$p"; done
|
|
1631
|
+
} > "$tmp" || {
|
|
1632
|
+
rm -f "$tmp"
|
|
1633
|
+
echo "WARN: aid: could not update the machine project registry (${reg}): write failed" >&2
|
|
1634
|
+
return 0
|
|
1635
|
+
}
|
|
1636
|
+
# Never-elevate atomic commit: empty probe forces direct (no-sudo) mv.
|
|
1637
|
+
_aid_priv_run "" mv -f "$tmp" "$reg" || {
|
|
1638
|
+
rm -f "$tmp" 2>/dev/null
|
|
1639
|
+
echo "WARN: aid: could not update the machine project registry (${reg}): mv failed" >&2
|
|
1640
|
+
return 0
|
|
1641
|
+
}
|
|
1642
|
+
echo "Registered ${repo} with the AID CLI."
|
|
1643
|
+
}
|
|
1644
|
+
|
|
1645
|
+
# registry_unregister <canon-path>
|
|
1646
|
+
# Set-remove <canon-path> from whichever tier(s) it appears in (idempotent; atomic write).
|
|
1647
|
+
# Called only when the repo manifest is now gone (last tool removed).
|
|
1648
|
+
# On a real change prints one concise line. On failure prints WARN and returns 0.
|
|
1649
|
+
#
|
|
1650
|
+
# Tier-aware: searches user tier and (when global scope) shared tier. Removes from
|
|
1651
|
+
# each tier where the entry is found, best-effort. User-tier write is never-elevate
|
|
1652
|
+
# (empty-probe _aid_priv_run "" mv -f). Shared-tier write uses real probe; if not
|
|
1653
|
+
# user-writable and elevation is unavailable, WARN + skip + return 0.
|
|
1654
|
+
# Per-user install ($AID_STATE_HOME == ~/.aid): both tiers are the same file; single
|
|
1655
|
+
# write, no elevation ever.
|
|
1656
|
+
registry_unregister() {
|
|
1657
|
+
local repo="$1" tmp existing
|
|
1658
|
+
# Determine the effective registry path, mirroring registry_register's
|
|
1659
|
+
# primary/fallback logic: $AID_STATE_HOME is primary (honors AID_HOME override);
|
|
1660
|
+
# $HOME/.aid is the fallback when AID_STATE_HOME is not writable.
|
|
1661
|
+
# Also search the other tier in case the entry was recorded there.
|
|
1662
|
+
local _shared_reg_dir="${AID_STATE_HOME}"
|
|
1663
|
+
local _shared_reg="${AID_STATE_HOME}/registry.yml"
|
|
1664
|
+
local _user_reg_dir="${HOME}/.aid"
|
|
1665
|
+
local _user_reg="${_user_reg_dir}/registry.yml"
|
|
1666
|
+
# Per-user collapse: AID_STATE_HOME is the same path as $HOME/.aid.
|
|
1667
|
+
local _per_user=0
|
|
1668
|
+
[[ "$AID_STATE_HOME" == "${HOME}/.aid" ]] && _per_user=1
|
|
1669
|
+
local _found_any=0
|
|
1670
|
+
# --- PRIMARY TIER ($AID_STATE_HOME) ---
|
|
1671
|
+
_aid_priv_run "" mkdir -p "$AID_STATE_HOME" 2>/dev/null || true
|
|
1672
|
+
if [[ -w "$AID_STATE_HOME" ]]; then
|
|
1673
|
+
existing="$(_registry_read_repos "$_shared_reg")"
|
|
1674
|
+
if printf '%s\n' "$existing" | grep -qxF "$repo"; then
|
|
1675
|
+
_found_any=1
|
|
1676
|
+
tmp="$(mktemp "${_shared_reg}.aid-tmp.XXXXXX" 2>/dev/null)" || {
|
|
1677
|
+
echo "WARN: aid: could not update the machine project registry (${_shared_reg}): mktemp failed" >&2
|
|
1678
|
+
return 0
|
|
1679
|
+
}
|
|
1680
|
+
{
|
|
1681
|
+
printf '%s\n' "# AID machine project registry (managed by 'aid add' / 'aid remove' -- do not hand-edit)."
|
|
1682
|
+
printf '%s\n' "# Holds ONLY the base folders of projects this CLI install manages. Per-project name and"
|
|
1683
|
+
printf '%s\n' "# description come from .aid/settings.yml; version/tools from the manifest, at render time."
|
|
1684
|
+
printf '%s\n' "schema: 1"
|
|
1685
|
+
printf '%s\n' "projects:"
|
|
1686
|
+
{ printf '%s\n' "$existing" | grep -vxF "$repo" || true; } | sed '/^$/d' | sort -u \
|
|
1687
|
+
| while IFS= read -r p; do printf ' - %s\n' "$p"; done
|
|
1688
|
+
} > "$tmp" || {
|
|
1689
|
+
rm -f "$tmp"
|
|
1690
|
+
echo "WARN: aid: could not update the machine project registry (${_shared_reg}): write failed" >&2
|
|
1691
|
+
return 0
|
|
1692
|
+
}
|
|
1693
|
+
_aid_priv_run "" mv -f "$tmp" "$_shared_reg" || {
|
|
1694
|
+
rm -f "$tmp" 2>/dev/null
|
|
1695
|
+
echo "WARN: aid: could not update the machine project registry (${_shared_reg}): mv failed" >&2
|
|
1696
|
+
return 0
|
|
1697
|
+
}
|
|
1698
|
+
fi
|
|
1699
|
+
else
|
|
1700
|
+
# AID_STATE_HOME not writable; check/operate in fallback $HOME/.aid tier.
|
|
1701
|
+
# Degrade WARN is silent by default (designed global-install behavior); visible under --verbose.
|
|
1702
|
+
mkdir -p "$_user_reg_dir" 2>/dev/null || true
|
|
1703
|
+
existing="$(_registry_read_repos "$_user_reg")"
|
|
1704
|
+
if printf '%s\n' "$existing" | grep -qxF "$repo"; then
|
|
1705
|
+
_found_any=1
|
|
1706
|
+
[[ "${_AID_VERBOSE:-0}" == "1" ]] && \
|
|
1707
|
+
echo "WARN: aid: could not write to state home ${AID_STATE_HOME}; using ${_user_reg}" >&2
|
|
1708
|
+
tmp="$(mktemp "${_user_reg}.aid-tmp.XXXXXX" 2>/dev/null)" || {
|
|
1709
|
+
echo "WARN: aid: could not update the machine project registry (${_user_reg}): mktemp failed" >&2
|
|
1710
|
+
return 0
|
|
1711
|
+
}
|
|
1712
|
+
{
|
|
1713
|
+
printf '%s\n' "# AID machine project registry (managed by 'aid add' / 'aid remove' -- do not hand-edit)."
|
|
1714
|
+
printf '%s\n' "# Holds ONLY the base folders of projects this CLI install manages. Per-project name and"
|
|
1715
|
+
printf '%s\n' "# description come from .aid/settings.yml; version/tools from the manifest, at render time."
|
|
1716
|
+
printf '%s\n' "schema: 1"
|
|
1717
|
+
printf '%s\n' "projects:"
|
|
1718
|
+
{ printf '%s\n' "$existing" | grep -vxF "$repo" || true; } | sed '/^$/d' | sort -u \
|
|
1719
|
+
| while IFS= read -r p; do printf ' - %s\n' "$p"; done
|
|
1720
|
+
} > "$tmp" || {
|
|
1721
|
+
rm -f "$tmp"
|
|
1722
|
+
echo "WARN: aid: could not update the machine project registry (${_user_reg}): write failed" >&2
|
|
1723
|
+
return 0
|
|
1724
|
+
}
|
|
1725
|
+
_aid_priv_run "" mv -f "$tmp" "$_user_reg" || {
|
|
1726
|
+
rm -f "$tmp" 2>/dev/null
|
|
1727
|
+
echo "WARN: aid: could not update the machine project registry (${_user_reg}): mv failed" >&2
|
|
1728
|
+
return 0
|
|
1729
|
+
}
|
|
1730
|
+
fi
|
|
1731
|
+
fi
|
|
1732
|
+
# --- FALLBACK / SECONDARY TIER ($HOME/.aid, global install only) ---
|
|
1733
|
+
# When AID_STATE_HOME is writable and != $HOME/.aid, also check if the entry
|
|
1734
|
+
# exists in $HOME/.aid (e.g. was registered when AID_STATE_HOME was non-writable).
|
|
1735
|
+
if [[ "$_per_user" -eq 0 && -w "$AID_STATE_HOME" ]]; then
|
|
1736
|
+
local _fb_existing
|
|
1737
|
+
_fb_existing="$(_registry_read_repos "$_user_reg")"
|
|
1738
|
+
if printf '%s\n' "$_fb_existing" | grep -qxF "$repo"; then
|
|
1739
|
+
_found_any=1
|
|
1740
|
+
mkdir -p "$_user_reg_dir" 2>/dev/null || true
|
|
1741
|
+
tmp="$(mktemp "${_user_reg}.aid-tmp.XXXXXX" 2>/dev/null)" || {
|
|
1742
|
+
echo "WARN: aid: could not update the machine project registry (${_user_reg}): mktemp failed" >&2
|
|
1743
|
+
}
|
|
1744
|
+
if [[ -n "$tmp" ]]; then
|
|
1745
|
+
{
|
|
1746
|
+
printf '%s\n' "# AID machine project registry (managed by 'aid add' / 'aid remove' -- do not hand-edit)."
|
|
1747
|
+
printf '%s\n' "# Holds ONLY the base folders of projects this CLI install manages. Per-project name and"
|
|
1748
|
+
printf '%s\n' "# description come from .aid/settings.yml; version/tools from the manifest, at render time."
|
|
1749
|
+
printf '%s\n' "schema: 1"
|
|
1750
|
+
printf '%s\n' "projects:"
|
|
1751
|
+
{ printf '%s\n' "$_fb_existing" | grep -vxF "$repo" || true; } | sed '/^$/d' | sort -u \
|
|
1752
|
+
| while IFS= read -r p; do printf ' - %s\n' "$p"; done
|
|
1753
|
+
} > "$tmp" || {
|
|
1754
|
+
rm -f "$tmp"
|
|
1755
|
+
echo "WARN: aid: could not update the machine project registry (${_user_reg}): write failed" >&2
|
|
1756
|
+
tmp=""
|
|
1757
|
+
}
|
|
1758
|
+
fi
|
|
1759
|
+
if [[ -n "$tmp" ]]; then
|
|
1760
|
+
_aid_priv_run "" mv -f "$tmp" "$_user_reg" || {
|
|
1761
|
+
rm -f "$tmp" 2>/dev/null
|
|
1762
|
+
echo "WARN: aid: could not update the machine project registry (${_user_reg}): mv failed" >&2
|
|
1763
|
+
}
|
|
1764
|
+
fi
|
|
1765
|
+
fi
|
|
1766
|
+
fi
|
|
1767
|
+
if [[ "$_found_any" -eq 0 ]]; then
|
|
1768
|
+
[[ "$_AID_VERBOSE" == "1" ]] && echo "Registry: ${repo} not in registry (no-op)."
|
|
1769
|
+
return 0
|
|
1770
|
+
fi
|
|
1771
|
+
echo "Unregistered ${repo} from the AID CLI."
|
|
1772
|
+
}
|
|
1773
|
+
|
|
1774
|
+
# ---------------------------------------------------------------------------
|
|
1775
|
+
# C4: _aid_repo_format <repo>
|
|
1776
|
+
# Read the format_version stamp from <repo>/.aid/settings.yml.
|
|
1777
|
+
# Greps the FIRST ^format_version: line, replicates the era-a closure strip
|
|
1778
|
+
# logic inline (prefix strip, trim, inline # comment strip, quote-unwrap),
|
|
1779
|
+
# validates as ^[0-9]+$; echoes the integer.
|
|
1780
|
+
# Collapses absent/empty/non-integer/malformed/negative to 0 (legacy default).
|
|
1781
|
+
# Never returns a value > sup from a garbled stamp (fail-safe).
|
|
1782
|
+
# ---------------------------------------------------------------------------
|
|
1783
|
+
_aid_repo_format() {
|
|
1784
|
+
local _repo="$1"
|
|
1785
|
+
local _settings="${_repo}/.aid/settings.yml"
|
|
1786
|
+
if [[ ! -f "${_settings}" ]]; then
|
|
1787
|
+
echo "0"
|
|
1788
|
+
return 0
|
|
1789
|
+
fi
|
|
1790
|
+
# First-match read (parity with duplicate-line policy).
|
|
1791
|
+
local _raw_line
|
|
1792
|
+
_raw_line="$(grep -m1 '^format_version:' "${_settings}" 2>/dev/null)" || true
|
|
1793
|
+
if [[ -z "${_raw_line}" ]]; then
|
|
1794
|
+
echo "0"
|
|
1795
|
+
return 0
|
|
1796
|
+
fi
|
|
1797
|
+
# Replicate the era-a closure strip logic inline (column-0 key variant).
|
|
1798
|
+
# Step 1: strip the "format_version:" prefix.
|
|
1799
|
+
local _val="${_raw_line#format_version:}"
|
|
1800
|
+
# Step 2: strip one optional leading space (the colon-space separator).
|
|
1801
|
+
_val="${_val# }"
|
|
1802
|
+
# Step 3: strip inline # comment (first " #" to end of line).
|
|
1803
|
+
_val="${_val%% #*}"
|
|
1804
|
+
# Step 4: quote-unwrap (double then single).
|
|
1805
|
+
_val="${_val%\"}"
|
|
1806
|
+
_val="${_val#\"}"
|
|
1807
|
+
_val="${_val%%\'}"
|
|
1808
|
+
_val="${_val##\'}"
|
|
1809
|
+
# Step 5: full trim (ltrim + rtrim remaining whitespace).
|
|
1810
|
+
local _lstrip="${_val%%[![:space:]]*}"
|
|
1811
|
+
_val="${_val#"${_lstrip}"}"
|
|
1812
|
+
local _rstrip="${_val##*[![:space:]]}"
|
|
1813
|
+
_val="${_val%"${_rstrip}"}"
|
|
1814
|
+
# Step 6: validate non-negative integer; collapse anything else to 0.
|
|
1815
|
+
if [[ "${_val}" =~ ^[0-9]+$ ]]; then
|
|
1816
|
+
echo "${_val}"
|
|
1817
|
+
else
|
|
1818
|
+
echo "0"
|
|
1819
|
+
fi
|
|
1820
|
+
return 0
|
|
1821
|
+
}
|
|
1822
|
+
|
|
1823
|
+
# ---------------------------------------------------------------------------
|
|
1824
|
+
# C5: _aid_format_gate <repo>
|
|
1825
|
+
# 3-way classify <repo>'s format stamp vs AID_SUPPORTED_FORMAT:
|
|
1826
|
+
# repo > sup -> refuse (stderr, return 1, no .aid/ write)
|
|
1827
|
+
# repo < sup -> warn + offer aid update (stdout, return 0, non-blocking)
|
|
1828
|
+
# repo == sup -> silent (return 0)
|
|
1829
|
+
# AID_NO_MIGRATE=1 suppresses the warn+offer notice only; never the refuse.
|
|
1830
|
+
# ---------------------------------------------------------------------------
|
|
1831
|
+
_aid_format_gate() {
|
|
1832
|
+
local _repo="$1"
|
|
1833
|
+
local _repo_fmt
|
|
1834
|
+
_repo_fmt="$(_aid_repo_format "${_repo}")"
|
|
1835
|
+
local _sup="${AID_SUPPORTED_FORMAT}"
|
|
1836
|
+
if [[ "${_repo_fmt}" -gt "${_sup}" ]]; then
|
|
1837
|
+
printf 'ERROR: aid: project format %s is newer than this CLI supports (%s). Upgrade the aid CLI to operate on this project.\n' \
|
|
1838
|
+
"${_repo_fmt}" "${_sup}" >&2
|
|
1839
|
+
return 1
|
|
1840
|
+
fi
|
|
1841
|
+
if [[ "${_repo_fmt}" -lt "${_sup}" ]]; then
|
|
1842
|
+
if [[ "${AID_NO_MIGRATE:-0}" != "1" ]] \
|
|
1843
|
+
&& [[ -f "${_repo}/.aid/.aid-manifest.json" ]]; then
|
|
1844
|
+
printf 'WARN: aid: this project uses an older format (v%s; current: v%s). Run: aid update\n' \
|
|
1845
|
+
"${_repo_fmt}" "${_sup}"
|
|
1846
|
+
fi
|
|
1847
|
+
return 0
|
|
1848
|
+
fi
|
|
1849
|
+
# repo == sup: silent.
|
|
1850
|
+
return 0
|
|
1851
|
+
}
|
|
1852
|
+
|
|
1853
|
+
# ---------------------------------------------------------------------------
|
|
1854
|
+
# _aid_migrate_repo <repo> (FF-1 / LC-MIG / task-077)
|
|
1855
|
+
# Per-repo migration core. Runs DETECT->SETTINGS->ADD->RELOCATE->REGISTER in
|
|
1856
|
+
# order. Each step is WARN-not-fail: a step failure logs WARN and the next
|
|
1857
|
+
# step runs; the function always returns 0 (SEC-4 / NFR12).
|
|
1858
|
+
# <repo> is a CAN-1 canonical repo base folder (resolved by the caller via
|
|
1859
|
+
# cd "$repo" && pwd -- identical to bin/aid:1366 / feature-010 SEC-5).
|
|
1860
|
+
# ---------------------------------------------------------------------------
|
|
1861
|
+
_aid_migrate_repo() {
|
|
1862
|
+
local repo="$1"
|
|
1863
|
+
|
|
1864
|
+
# ------------------------------------------------------------------
|
|
1865
|
+
# STEP 0 -- DETECT / QUALIFY (DD-6 / SEC-1) -- read-only, no write.
|
|
1866
|
+
# Qualify iff <repo>/.aid/ exists AND at least one era marker is
|
|
1867
|
+
# present. A bare .aid/ with no marker is NOT a candidate.
|
|
1868
|
+
# ------------------------------------------------------------------
|
|
1869
|
+
if [[ ! -d "${repo}/.aid" ]]; then
|
|
1870
|
+
return 0
|
|
1871
|
+
fi
|
|
1872
|
+
|
|
1873
|
+
local _era=""
|
|
1874
|
+
if [[ -f "${repo}/.aid/settings.yml" ]]; then
|
|
1875
|
+
_era="a"
|
|
1876
|
+
elif [[ -f "${repo}/.aid/knowledge/DISCOVERY_STATE.md" ]] \
|
|
1877
|
+
|| [[ -f "${repo}/.aid/knowledge/DISCOVERY-STATE.md" ]] \
|
|
1878
|
+
|| [[ -f "${repo}/.aid/knowledge/STATE.md" ]]; then
|
|
1879
|
+
_era="b"
|
|
1880
|
+
else
|
|
1881
|
+
# Bare .aid/ -- not a candidate, no mutation.
|
|
1882
|
+
return 0
|
|
1883
|
+
fi
|
|
1884
|
+
|
|
1885
|
+
# ------------------------------------------------------------------
|
|
1886
|
+
# STEP 1 -- SETTINGS (DM-1 / task-074 contract)
|
|
1887
|
+
# ------------------------------------------------------------------
|
|
1888
|
+
local _settings="${repo}/.aid/settings.yml"
|
|
1889
|
+
local _manifest="${repo}/.aid/.aid-manifest.json"
|
|
1890
|
+
local _repo_name; _repo_name="$(basename "${repo}")"
|
|
1891
|
+
|
|
1892
|
+
if [[ "$_era" == "a" ]]; then
|
|
1893
|
+
# Era-a: validate and targeted-repair REQUIRED keys only.
|
|
1894
|
+
# Preserves every present kb_baseline.* line and <skill>.minimum_grade
|
|
1895
|
+
# line byte-intact (IDIOM-A single-line replace / IDIOM-B append-block).
|
|
1896
|
+
_aid_migrate_repair_settings_era_a "${_settings}" "${_repo_name}" || \
|
|
1897
|
+
echo "WARN: aid migrate: settings repair failed for ${repo}/.aid/settings.yml (continuing)" >&2
|
|
1898
|
+
else
|
|
1899
|
+
# Era-b: synthesize a fresh settings.yml from the template defaults.
|
|
1900
|
+
_aid_migrate_synthesize_settings_era_b "${_settings}" "${_repo_name}" "${_manifest}" || \
|
|
1901
|
+
echo "WARN: aid migrate: settings synthesis failed for ${repo}/.aid/settings.yml (continuing)" >&2
|
|
1902
|
+
fi
|
|
1903
|
+
|
|
1904
|
+
# ------------------------------------------------------------------
|
|
1905
|
+
# STEP 2 -- ADD home.html (FR40 / RC-2) -- copy-when-absent only.
|
|
1906
|
+
# ------------------------------------------------------------------
|
|
1907
|
+
local _home_html_dest="${repo}/.aid/dashboard/home.html"
|
|
1908
|
+
if [[ ! -f "${_home_html_dest}" ]]; then
|
|
1909
|
+
local _home_html_src="${AID_CODE_HOME}/dashboard/home.html"
|
|
1910
|
+
if [[ -f "${_home_html_src}" ]]; then
|
|
1911
|
+
mkdir -p "${repo}/.aid/dashboard" 2>/dev/null || \
|
|
1912
|
+
echo "WARN: aid migrate: mkdir .aid/dashboard failed for ${repo} (continuing)" >&2
|
|
1913
|
+
if [[ -d "${repo}/.aid/dashboard" ]]; then
|
|
1914
|
+
cp "${_home_html_src}" "${_home_html_dest}" 2>/dev/null || \
|
|
1915
|
+
echo "WARN: aid migrate: copy home.html failed for ${repo} (continuing)" >&2
|
|
1916
|
+
fi
|
|
1917
|
+
else
|
|
1918
|
+
echo "WARN: aid migrate: home.html source not found at ${_home_html_src} (continuing)" >&2
|
|
1919
|
+
fi
|
|
1920
|
+
fi
|
|
1921
|
+
|
|
1922
|
+
# ------------------------------------------------------------------
|
|
1923
|
+
# STEP 3 -- RELOCATE legacy summary (DM-4 / FR31) -- no-clobber mv.
|
|
1924
|
+
# Exact idiom from canonical/scripts/summarize/summarize-preflight.sh:102-113
|
|
1925
|
+
# ------------------------------------------------------------------
|
|
1926
|
+
local _old_summary="${repo}/.aid/knowledge/knowledge-summary.html"
|
|
1927
|
+
local _new_summary="${repo}/.aid/dashboard/kb.html"
|
|
1928
|
+
if [[ -f "${_old_summary}" ]] && [[ ! -f "${_new_summary}" ]]; then
|
|
1929
|
+
mkdir -p "${repo}/.aid/dashboard" 2>/dev/null || \
|
|
1930
|
+
echo "WARN: aid migrate: mkdir .aid/dashboard failed for ${repo} (step 3, continuing)" >&2
|
|
1931
|
+
if [[ -d "${repo}/.aid/dashboard" ]]; then
|
|
1932
|
+
mv -n "${_old_summary}" "${_new_summary}" 2>/dev/null || \
|
|
1933
|
+
echo "WARN: aid migrate: relocate legacy summary failed for ${repo} (continuing)" >&2
|
|
1934
|
+
fi
|
|
1935
|
+
fi
|
|
1936
|
+
|
|
1937
|
+
# ------------------------------------------------------------------
|
|
1938
|
+
# STEP 4 -- REGISTER (DM-2 / FR28) -- existing idempotent writer.
|
|
1939
|
+
# Canonicalize path (same rule as bin/aid:1366).
|
|
1940
|
+
# FR7: deterministic tier via _aid_resolve_tier; never-elevate: if shared would
|
|
1941
|
+
# need elevation (shared dir not user-writable), degrade silently to user.
|
|
1942
|
+
# ------------------------------------------------------------------
|
|
1943
|
+
local _canon_repo
|
|
1944
|
+
_canon_repo="$(cd "${repo}" && pwd)" 2>/dev/null || _canon_repo="${repo}"
|
|
1945
|
+
local _migrate_tier
|
|
1946
|
+
_migrate_tier="$(_aid_resolve_tier "${_canon_repo}")"
|
|
1947
|
+
if [[ "$_migrate_tier" == "shared" && ! -w "${AID_STATE_HOME}" ]]; then
|
|
1948
|
+
_migrate_tier="user"
|
|
1949
|
+
fi
|
|
1950
|
+
registry_register "${_canon_repo}" "$_migrate_tier" || true
|
|
1951
|
+
|
|
1952
|
+
return 0
|
|
1953
|
+
}
|
|
1954
|
+
|
|
1955
|
+
# _aid_migrate_repair_settings_era_a <settings_file> <repo_name>
|
|
1956
|
+
# Era-a: validate/repair REQUIRED keys via targeted edits only.
|
|
1957
|
+
# A valid file -> no write (idempotent).
|
|
1958
|
+
# Batch all edits in memory, then write a single temp+mv -f (crash-safe).
|
|
1959
|
+
_aid_migrate_repair_settings_era_a() {
|
|
1960
|
+
local _sf="$1" _rname="$2"
|
|
1961
|
+
[[ -f "${_sf}" ]] || return 1
|
|
1962
|
+
|
|
1963
|
+
# Read file into an array (preserves byte content per line).
|
|
1964
|
+
local -a _lines=()
|
|
1965
|
+
while IFS= read -r _l || [[ -n "${_l}" ]]; do
|
|
1966
|
+
_lines+=("${_l}")
|
|
1967
|
+
done < "${_sf}"
|
|
1968
|
+
|
|
1969
|
+
local _changed=0
|
|
1970
|
+
|
|
1971
|
+
# ---- Helper: locate a section header line index (col-0 "^<sect>:$") ----
|
|
1972
|
+
_find_section() {
|
|
1973
|
+
local _sect="$1" _i
|
|
1974
|
+
for _i in "${!_lines[@]}"; do
|
|
1975
|
+
if [[ "${_lines[$_i]}" =~ ^${_sect}:[[:space:]]*$ ]]; then
|
|
1976
|
+
echo "$_i"; return 0
|
|
1977
|
+
fi
|
|
1978
|
+
done
|
|
1979
|
+
echo "-1"
|
|
1980
|
+
}
|
|
1981
|
+
|
|
1982
|
+
# ---- Helper: locate an indented key line index inside a section ----
|
|
1983
|
+
_find_key_in_section() {
|
|
1984
|
+
local _sect_idx="$1" _key="$2" _i
|
|
1985
|
+
local _n="${#_lines[@]}"
|
|
1986
|
+
for (( _i=_sect_idx+1; _i<_n; _i++ )); do
|
|
1987
|
+
local _ln="${_lines[$_i]}"
|
|
1988
|
+
# Stop at next col-0 non-comment non-blank line (next section).
|
|
1989
|
+
if [[ "${_ln}" =~ ^[a-zA-Z_] ]]; then
|
|
1990
|
+
echo "-1"; return 0
|
|
1991
|
+
fi
|
|
1992
|
+
if [[ "${_ln}" =~ ^[[:space:]]+${_key}:[[:space:]] ]] || \
|
|
1993
|
+
[[ "${_ln}" =~ ^[[:space:]]+${_key}:[[:space:]]*$ ]]; then
|
|
1994
|
+
echo "$_i"; return 0
|
|
1995
|
+
fi
|
|
1996
|
+
done
|
|
1997
|
+
echo "-1"
|
|
1998
|
+
}
|
|
1999
|
+
|
|
2000
|
+
# ---- Helper: get the scalar value of an indented " key: value" line ----
|
|
2001
|
+
_get_scalar_value() {
|
|
2002
|
+
local _ln="$1" _key="$2" _val
|
|
2003
|
+
# strip leading whitespace + key: (colon only; trailing space is optional so a bare
|
|
2004
|
+
# "name:" with no value is also reduced to empty; parity with the PS twin's \s*)
|
|
2005
|
+
_val="${_ln#*${_key}:}"
|
|
2006
|
+
# strip one optional leading space (was the colon-space separator)
|
|
2007
|
+
_val="${_val# }"
|
|
2008
|
+
# strip inline comment: first " #" to end of line (YAML inline-comment form).
|
|
2009
|
+
# This intentionally matches the first space-hash occurrence (parity with PS twin's
|
|
2010
|
+
# \s*#.*$ strip and the reader's _strip_yaml_inline_comment rule).
|
|
2011
|
+
_val="${_val%% #*}"
|
|
2012
|
+
_val="${_val%\"}"
|
|
2013
|
+
_val="${_val#\"}"
|
|
2014
|
+
_val="${_val%%\'}"
|
|
2015
|
+
_val="${_val##\'}"
|
|
2016
|
+
# Full rtrim: remove all trailing whitespace left after the comment strip.
|
|
2017
|
+
# The previous "%% " (single-space suffix) only removed ONE trailing space,
|
|
2018
|
+
# leaving the alignment padding that precedes " # comment" in lines like
|
|
2019
|
+
# " type: brownfield # brownfield | greenfield".
|
|
2020
|
+
local _rstrip="${_val##*[![:space:]]}"
|
|
2021
|
+
_val="${_val%"${_rstrip}"}"
|
|
2022
|
+
# Full ltrim: remove any leading whitespace (e.g. from multi-space colon-separator).
|
|
2023
|
+
local _lstrip="${_val%%[![:space:]]*}"
|
|
2024
|
+
_val="${_val#"${_lstrip}"}"
|
|
2025
|
+
echo "${_val}"
|
|
2026
|
+
}
|
|
2027
|
+
|
|
2028
|
+
# ---- Insert an indented line after a given index in _lines ----
|
|
2029
|
+
_insert_after() {
|
|
2030
|
+
local _idx="$1" _new_line="$2"
|
|
2031
|
+
local -a _new_lines=()
|
|
2032
|
+
local _i
|
|
2033
|
+
for (( _i=0; _i<${#_lines[@]}; _i++ )); do
|
|
2034
|
+
_new_lines+=("${_lines[$_i]}")
|
|
2035
|
+
if [[ "$_i" -eq "$_idx" ]]; then
|
|
2036
|
+
_new_lines+=("${_new_line}")
|
|
2037
|
+
fi
|
|
2038
|
+
done
|
|
2039
|
+
_lines=("${_new_lines[@]}")
|
|
2040
|
+
_changed=1
|
|
2041
|
+
}
|
|
2042
|
+
|
|
2043
|
+
# ---- Append a block at EOF (IDIOM-B for whole-section missing) ----
|
|
2044
|
+
# Always prepends a blank line so the new section is visually separated from
|
|
2045
|
+
# the preceding content (matching the template's blank-line-between-sections style).
|
|
2046
|
+
# Idempotency is preserved: on a 2nd run the section now exists, so _find_section
|
|
2047
|
+
# finds it and this path never executes again.
|
|
2048
|
+
_append_block() {
|
|
2049
|
+
local _block="$1"
|
|
2050
|
+
local -a _block_lines=()
|
|
2051
|
+
_block_lines+=("")
|
|
2052
|
+
while IFS= read -r _bl; do
|
|
2053
|
+
_block_lines+=("${_bl}")
|
|
2054
|
+
done <<< "${_block}"
|
|
2055
|
+
_lines+=("${_block_lines[@]}")
|
|
2056
|
+
_changed=1
|
|
2057
|
+
}
|
|
2058
|
+
|
|
2059
|
+
# ---- Replace a single line (IDIOM-A) ----
|
|
2060
|
+
_replace_line() {
|
|
2061
|
+
local _idx="$1" _new="$2"
|
|
2062
|
+
_lines[$_idx]="${_new}"
|
|
2063
|
+
_changed=1
|
|
2064
|
+
}
|
|
2065
|
+
|
|
2066
|
+
# ------------------------------------------------------------------
|
|
2067
|
+
# Validate / repair each REQUIRED key.
|
|
2068
|
+
# ------------------------------------------------------------------
|
|
2069
|
+
|
|
2070
|
+
# C3: format_version ensure-key step (top-of-file column-0 prepend).
|
|
2071
|
+
# If a ^format_version: line is present anywhere, replace it in-place
|
|
2072
|
+
# with the canonical stamp value (IDIOM-A single-line replace).
|
|
2073
|
+
# If absent, prepend format_version: <sup> at index 0 of _lines so it
|
|
2074
|
+
# sits above project: at column 0 (existing _append_block is EOF-only;
|
|
2075
|
+
# _insert_after places indented lines after a header -- neither works
|
|
2076
|
+
# here; we prepend directly).
|
|
2077
|
+
local _fv_idx=-1 _fi
|
|
2078
|
+
for _fi in "${!_lines[@]}"; do
|
|
2079
|
+
if [[ "${_lines[$_fi]}" =~ ^format_version: ]]; then
|
|
2080
|
+
_fv_idx="$_fi"
|
|
2081
|
+
break
|
|
2082
|
+
fi
|
|
2083
|
+
done
|
|
2084
|
+
if [[ "${_fv_idx}" -ge 0 ]]; then
|
|
2085
|
+
# Key present: replace with canonical value (IDIOM-A).
|
|
2086
|
+
_replace_line "${_fv_idx}" "format_version: ${AID_SUPPORTED_FORMAT}"
|
|
2087
|
+
else
|
|
2088
|
+
# Key absent: prepend at index 0 (new top-of-file col-0 insert).
|
|
2089
|
+
local -a _fv_new_lines=()
|
|
2090
|
+
_fv_new_lines+=("format_version: ${AID_SUPPORTED_FORMAT}")
|
|
2091
|
+
local _fv_i
|
|
2092
|
+
for _fv_i in "${!_lines[@]}"; do
|
|
2093
|
+
_fv_new_lines+=("${_lines[$_fv_i]}")
|
|
2094
|
+
done
|
|
2095
|
+
_lines=("${_fv_new_lines[@]}")
|
|
2096
|
+
_changed=1
|
|
2097
|
+
fi
|
|
2098
|
+
|
|
2099
|
+
# project section
|
|
2100
|
+
local _proj_idx; _proj_idx="$(_find_section "project")"
|
|
2101
|
+
if [[ "$_proj_idx" -eq -1 ]]; then
|
|
2102
|
+
# Whole project: section missing -> IDIOM-B append.
|
|
2103
|
+
_append_block "project:
|
|
2104
|
+
name: ${_rname}
|
|
2105
|
+
description: <project-description>
|
|
2106
|
+
type: brownfield"
|
|
2107
|
+
else
|
|
2108
|
+
# project.name
|
|
2109
|
+
local _name_idx; _name_idx="$(_find_key_in_section "$_proj_idx" "name")"
|
|
2110
|
+
if [[ "$_name_idx" -eq -1 ]]; then
|
|
2111
|
+
_insert_after "$_proj_idx" " name: ${_rname}"
|
|
2112
|
+
else
|
|
2113
|
+
local _name_val; _name_val="$(_get_scalar_value "${_lines[$_name_idx]}" "name")"
|
|
2114
|
+
if [[ -z "${_name_val}" ]]; then
|
|
2115
|
+
_replace_line "$_name_idx" " name: ${_rname}"
|
|
2116
|
+
fi
|
|
2117
|
+
fi
|
|
2118
|
+
|
|
2119
|
+
# project.description (key must exist; value may be placeholder)
|
|
2120
|
+
local _desc_idx; _desc_idx="$(_find_key_in_section "$_proj_idx" "description")"
|
|
2121
|
+
if [[ "$_desc_idx" -eq -1 ]]; then
|
|
2122
|
+
# Find name line to insert after it, else insert after section header.
|
|
2123
|
+
local _name_idx2; _name_idx2="$(_find_key_in_section "$_proj_idx" "name")"
|
|
2124
|
+
if [[ "$_name_idx2" -ne -1 ]]; then
|
|
2125
|
+
_insert_after "$_name_idx2" " description: <project-description>"
|
|
2126
|
+
else
|
|
2127
|
+
_insert_after "$_proj_idx" " description: <project-description>"
|
|
2128
|
+
fi
|
|
2129
|
+
fi
|
|
2130
|
+
|
|
2131
|
+
# project.type
|
|
2132
|
+
local _type_idx; _type_idx="$(_find_key_in_section "$_proj_idx" "type")"
|
|
2133
|
+
if [[ "$_type_idx" -eq -1 ]]; then
|
|
2134
|
+
# Insert after description (or name, or section header).
|
|
2135
|
+
local _desc_idx2; _desc_idx2="$(_find_key_in_section "$_proj_idx" "description")"
|
|
2136
|
+
local _ins_after_type
|
|
2137
|
+
if [[ "$_desc_idx2" -ne -1 ]]; then
|
|
2138
|
+
_ins_after_type="$_desc_idx2"
|
|
2139
|
+
else
|
|
2140
|
+
local _name_idx3; _name_idx3="$(_find_key_in_section "$_proj_idx" "name")"
|
|
2141
|
+
if [[ "$_name_idx3" -ne -1 ]]; then
|
|
2142
|
+
_ins_after_type="$_name_idx3"
|
|
2143
|
+
else
|
|
2144
|
+
_ins_after_type="$_proj_idx"
|
|
2145
|
+
fi
|
|
2146
|
+
fi
|
|
2147
|
+
_insert_after "$_ins_after_type" " type: brownfield"
|
|
2148
|
+
else
|
|
2149
|
+
local _type_val; _type_val="$(_get_scalar_value "${_lines[$_type_idx]}" "type")"
|
|
2150
|
+
if [[ "$_type_val" != "brownfield" && "$_type_val" != "greenfield" ]]; then
|
|
2151
|
+
_replace_line "$_type_idx" " type: brownfield"
|
|
2152
|
+
fi
|
|
2153
|
+
fi
|
|
2154
|
+
fi
|
|
2155
|
+
|
|
2156
|
+
# tools section
|
|
2157
|
+
local _tools_idx; _tools_idx="$(_find_section "tools")"
|
|
2158
|
+
if [[ "$_tools_idx" -eq -1 ]]; then
|
|
2159
|
+
_append_block "tools:
|
|
2160
|
+
installed: []"
|
|
2161
|
+
else
|
|
2162
|
+
local _inst_idx; _inst_idx="$(_find_key_in_section "$_tools_idx" "installed")"
|
|
2163
|
+
if [[ "$_inst_idx" -eq -1 ]]; then
|
|
2164
|
+
_insert_after "$_tools_idx" " installed: []"
|
|
2165
|
+
fi
|
|
2166
|
+
# If installed key exists (any value, even empty list) -> valid; no touch.
|
|
2167
|
+
fi
|
|
2168
|
+
|
|
2169
|
+
# review section
|
|
2170
|
+
local _rev_idx; _rev_idx="$(_find_section "review")"
|
|
2171
|
+
if [[ "$_rev_idx" -eq -1 ]]; then
|
|
2172
|
+
_append_block "review:
|
|
2173
|
+
minimum_grade: A"
|
|
2174
|
+
else
|
|
2175
|
+
local _mg_idx; _mg_idx="$(_find_key_in_section "$_rev_idx" "minimum_grade")"
|
|
2176
|
+
if [[ "$_mg_idx" -eq -1 ]]; then
|
|
2177
|
+
_insert_after "$_rev_idx" " minimum_grade: A"
|
|
2178
|
+
else
|
|
2179
|
+
local _mg_val; _mg_val="$(_get_scalar_value "${_lines[$_mg_idx]}" "minimum_grade")"
|
|
2180
|
+
if ! [[ "$_mg_val" =~ ^[A-F][+-]?$ ]]; then
|
|
2181
|
+
_replace_line "$_mg_idx" " minimum_grade: A"
|
|
2182
|
+
fi
|
|
2183
|
+
fi
|
|
2184
|
+
fi
|
|
2185
|
+
|
|
2186
|
+
# execution section
|
|
2187
|
+
local _exec_idx; _exec_idx="$(_find_section "execution")"
|
|
2188
|
+
if [[ "$_exec_idx" -eq -1 ]]; then
|
|
2189
|
+
_append_block "execution:
|
|
2190
|
+
max_parallel_tasks: 5"
|
|
2191
|
+
else
|
|
2192
|
+
local _mpt_idx; _mpt_idx="$(_find_key_in_section "$_exec_idx" "max_parallel_tasks")"
|
|
2193
|
+
if [[ "$_mpt_idx" -eq -1 ]]; then
|
|
2194
|
+
_insert_after "$_exec_idx" " max_parallel_tasks: 5"
|
|
2195
|
+
else
|
|
2196
|
+
local _mpt_val; _mpt_val="$(_get_scalar_value "${_lines[$_mpt_idx]}" "max_parallel_tasks")"
|
|
2197
|
+
if ! [[ "$_mpt_val" =~ ^[0-9]+$ ]] || [[ "$_mpt_val" -le 0 ]]; then
|
|
2198
|
+
_replace_line "$_mpt_idx" " max_parallel_tasks: 5"
|
|
2199
|
+
fi
|
|
2200
|
+
fi
|
|
2201
|
+
fi
|
|
2202
|
+
|
|
2203
|
+
# traceability section
|
|
2204
|
+
local _trace_idx; _trace_idx="$(_find_section "traceability")"
|
|
2205
|
+
if [[ "$_trace_idx" -eq -1 ]]; then
|
|
2206
|
+
_append_block "traceability:
|
|
2207
|
+
heartbeat_interval: 1"
|
|
2208
|
+
else
|
|
2209
|
+
local _hb_idx; _hb_idx="$(_find_key_in_section "$_trace_idx" "heartbeat_interval")"
|
|
2210
|
+
if [[ "$_hb_idx" -eq -1 ]]; then
|
|
2211
|
+
_insert_after "$_trace_idx" " heartbeat_interval: 1"
|
|
2212
|
+
else
|
|
2213
|
+
local _hb_val; _hb_val="$(_get_scalar_value "${_lines[$_hb_idx]}" "heartbeat_interval")"
|
|
2214
|
+
if ! [[ "$_hb_val" =~ ^[0-9]+$ ]]; then
|
|
2215
|
+
_replace_line "$_hb_idx" " heartbeat_interval: 1"
|
|
2216
|
+
fi
|
|
2217
|
+
fi
|
|
2218
|
+
fi
|
|
2219
|
+
|
|
2220
|
+
# ------------------------------------------------------------------
|
|
2221
|
+
# Write only if any edit was made (idempotent: no edit -> no write).
|
|
2222
|
+
# ------------------------------------------------------------------
|
|
2223
|
+
if [[ "$_changed" -eq 0 ]]; then
|
|
2224
|
+
return 0
|
|
2225
|
+
fi
|
|
2226
|
+
|
|
2227
|
+
local _tmp
|
|
2228
|
+
_tmp="$(mktemp "${_sf}.aid-tmp.XXXXXX")" || return 1
|
|
2229
|
+
{
|
|
2230
|
+
local _wi
|
|
2231
|
+
for _wi in "${!_lines[@]}"; do
|
|
2232
|
+
printf '%s\n' "${_lines[$_wi]}"
|
|
2233
|
+
done
|
|
2234
|
+
} > "${_tmp}" || { rm -f "${_tmp}"; return 1; }
|
|
2235
|
+
mv -f "${_tmp}" "${_sf}" || { rm -f "${_tmp}" 2>/dev/null; return 1; }
|
|
2236
|
+
return 0
|
|
2237
|
+
}
|
|
2238
|
+
|
|
2239
|
+
# _aid_migrate_synthesize_settings_era_b <settings_file> <repo_name> <manifest>
|
|
2240
|
+
# Era-b: write a fresh template-derived settings.yml when none exists.
|
|
2241
|
+
# tools.installed from manifest_list_tools (bash) or empty list if no manifest.
|
|
2242
|
+
# Crash-safe: same-directory temp + mv -f.
|
|
2243
|
+
_aid_migrate_synthesize_settings_era_b() {
|
|
2244
|
+
local _sf="$1" _rname="$2" _manifest="$3"
|
|
2245
|
+
|
|
2246
|
+
# Collect installed tools from manifest (exits 0, prints nothing when absent).
|
|
2247
|
+
local -a _tools=()
|
|
2248
|
+
while IFS= read -r _tid; do
|
|
2249
|
+
[[ -n "${_tid}" ]] && _tools+=("${_tid}")
|
|
2250
|
+
done < <(manifest_list_tools "${_manifest}" 2>/dev/null)
|
|
2251
|
+
|
|
2252
|
+
local _tmp
|
|
2253
|
+
_tmp="$(mktemp "${_sf}.aid-tmp.XXXXXX")" || return 1
|
|
2254
|
+
|
|
2255
|
+
{
|
|
2256
|
+
# C2: format_version stamp is the FIRST line (before project:).
|
|
2257
|
+
printf 'format_version: %s\n' "${AID_SUPPORTED_FORMAT}"
|
|
2258
|
+
printf 'project:\n'
|
|
2259
|
+
printf ' name: %s\n' "${_rname}"
|
|
2260
|
+
printf ' description: <project-description>\n'
|
|
2261
|
+
printf ' type: brownfield\n'
|
|
2262
|
+
printf '\n'
|
|
2263
|
+
printf 'tools:\n'
|
|
2264
|
+
if [[ "${#_tools[@]}" -eq 0 ]]; then
|
|
2265
|
+
printf ' installed: []\n'
|
|
2266
|
+
else
|
|
2267
|
+
printf ' installed:\n'
|
|
2268
|
+
local _t
|
|
2269
|
+
for _t in "${_tools[@]}"; do
|
|
2270
|
+
printf ' - %s\n' "${_t}"
|
|
2271
|
+
done
|
|
2272
|
+
fi
|
|
2273
|
+
printf '\n'
|
|
2274
|
+
printf 'review:\n'
|
|
2275
|
+
printf ' minimum_grade: A\n'
|
|
2276
|
+
printf '\n'
|
|
2277
|
+
printf 'execution:\n'
|
|
2278
|
+
printf ' max_parallel_tasks: 5\n'
|
|
2279
|
+
printf '\n'
|
|
2280
|
+
printf 'traceability:\n'
|
|
2281
|
+
printf ' heartbeat_interval: 1\n'
|
|
2282
|
+
} > "${_tmp}" || { rm -f "${_tmp}"; return 1; }
|
|
2283
|
+
|
|
2284
|
+
mv -f "${_tmp}" "${_sf}" || { rm -f "${_tmp}" 2>/dev/null; return 1; }
|
|
2285
|
+
return 0
|
|
2286
|
+
}
|
|
2287
|
+
|
|
2288
|
+
|
|
2289
|
+
# ---------------------------------------------------------------------------
|
|
2290
|
+
# _aid_cwd_classify <target-dir>
|
|
2291
|
+
# C-table: classify the cwd repo and perform register-on-encounter.
|
|
2292
|
+
# Called before repo commands (status, update [tool]) when .aid/ exists.
|
|
2293
|
+
# When called with a dir that has .aid/, this function:
|
|
2294
|
+
# 1. Checks if already registered (union read); if not, picks tier and registers.
|
|
2295
|
+
# 2. Does NOT check stale here -- callers use _aid_format_gate for that.
|
|
2296
|
+
# Returns 0 always (registration is best-effort; never blocks the host command).
|
|
2297
|
+
_aid_cwd_classify() {
|
|
2298
|
+
local _target="$1"
|
|
2299
|
+
local _canon_target
|
|
2300
|
+
_canon_target="$(cd "$_target" && pwd)" 2>/dev/null || _canon_target="$_target"
|
|
2301
|
+
|
|
2302
|
+
# Check if already registered in the union.
|
|
2303
|
+
local _is_registered=0
|
|
2304
|
+
while IFS= read -r _reg_p; do
|
|
2305
|
+
if [[ "$_reg_p" == "$_canon_target" ]]; then
|
|
2306
|
+
_is_registered=1
|
|
2307
|
+
break
|
|
2308
|
+
fi
|
|
2309
|
+
done < <(_registry_read_union)
|
|
2310
|
+
|
|
2311
|
+
if [[ "$_is_registered" -eq 0 ]]; then
|
|
2312
|
+
# Not registered -- pick tier and register (best-effort, never blocks).
|
|
2313
|
+
# FR7: deterministic, non-interactive tier selection via _aid_resolve_tier.
|
|
2314
|
+
# _AID_TIER_OVERRIDE is empty here (auto path; no CLI override in cwd-classify).
|
|
2315
|
+
local _reg_tier
|
|
2316
|
+
_reg_tier="$(_aid_resolve_tier "$_canon_target")"
|
|
2317
|
+
# register is best-effort: failure warns, returns 0, host command proceeds.
|
|
2318
|
+
registry_register "$_canon_target" "$_reg_tier" || true
|
|
2319
|
+
fi
|
|
2320
|
+
return 0
|
|
2321
|
+
}
|
|
2322
|
+
|
|
2323
|
+
# _aid_cwd_no_aid_offer <target-dir>
|
|
2324
|
+
# C-table last row: .aid/ absent -- print offer + optional non-git note, then exit 0.
|
|
2325
|
+
# This is the "no hard refuse" rule (decision #5): never errors on missing .aid/.
|
|
2326
|
+
_aid_cwd_no_aid_offer() {
|
|
2327
|
+
local _target="$1"
|
|
2328
|
+
local _canon
|
|
2329
|
+
_canon="$(cd "$_target" 2>/dev/null && pwd)" || _canon="$_target"
|
|
2330
|
+
printf 'no AID project here -- set it up? (aid add)\n'
|
|
2331
|
+
# Non-git note (decision #5): a non-git dir can use AID; .aid/ just won't be
|
|
2332
|
+
# version-controlled if git is absent.
|
|
2333
|
+
if ! git -C "$_canon" rev-parse --git-dir >/dev/null 2>&1; then
|
|
2334
|
+
printf 'Note: %s is not a git repository -- .aid/ will not be version-controlled.\n' "$_canon"
|
|
2335
|
+
fi
|
|
2336
|
+
exit 0
|
|
2337
|
+
}
|
|
2338
|
+
|
|
2339
|
+
# ---------------------------------------------------------------------------
|
|
2340
|
+
# _cmd_projects -- list/add/remove/help for the project registry.
|
|
2341
|
+
# ---------------------------------------------------------------------------
|
|
2342
|
+
_cmd_projects() {
|
|
2343
|
+
local _action="${1:-list}"
|
|
2344
|
+
local _path_arg=""
|
|
2345
|
+
local _verbose=0
|
|
2346
|
+
shift || true
|
|
2347
|
+
|
|
2348
|
+
# Parse remaining args: sub-action already consumed above.
|
|
2349
|
+
while [[ $# -gt 0 ]]; do
|
|
2350
|
+
case "$1" in
|
|
2351
|
+
-h|--help) _action="help"; shift ;;
|
|
2352
|
+
--local) _AID_TIER_OVERRIDE="--local"; shift ;;
|
|
2353
|
+
--shared) _AID_TIER_OVERRIDE="--shared"; shift ;;
|
|
2354
|
+
--verbose) _verbose=1; _AID_VERBOSE=1; shift ;;
|
|
2355
|
+
-*)
|
|
2356
|
+
echo "ERROR: aid projects: unknown flag: $1 (see 'aid projects -h')" >&2
|
|
2357
|
+
exit 2
|
|
2358
|
+
;;
|
|
2359
|
+
*)
|
|
2360
|
+
if [[ -z "$_path_arg" ]]; then
|
|
2361
|
+
_path_arg="$1"
|
|
2362
|
+
fi
|
|
2363
|
+
shift ;;
|
|
2364
|
+
esac
|
|
2365
|
+
done
|
|
2366
|
+
|
|
2367
|
+
case "$_action" in
|
|
2368
|
+
list) _cmd_projects_list "$_verbose" ;;
|
|
2369
|
+
add) _cmd_projects_add "$_path_arg" "$_verbose" ;;
|
|
2370
|
+
remove) _cmd_projects_remove "$_path_arg" "$_verbose" ;;
|
|
2371
|
+
help) _aid_usage projects; exit 0 ;;
|
|
2372
|
+
*)
|
|
2373
|
+
echo "ERROR: aid projects: unknown action: ${_action} (expected: list, add, remove, help)" >&2
|
|
2374
|
+
exit 2
|
|
2375
|
+
;;
|
|
2376
|
+
esac
|
|
2377
|
+
}
|
|
2378
|
+
|
|
2379
|
+
# _cmd_projects_list [verbose]
|
|
2380
|
+
# Render the raw union as an aligned table: marker, path, state, tools, tier.
|
|
2381
|
+
# Marks cwd with "*"; footnotes unregistered AID cwd.
|
|
2382
|
+
# --verbose: also print the registry file each entry was read from.
|
|
2383
|
+
_cmd_projects_list() {
|
|
2384
|
+
local _verbose="${1:-0}"
|
|
2385
|
+
|
|
2386
|
+
# Canonical cwd.
|
|
2387
|
+
local _cwd
|
|
2388
|
+
_cwd="$(cd . && pwd)"
|
|
2389
|
+
|
|
2390
|
+
# Collect raw union (includes no-aid / missing paths).
|
|
2391
|
+
local -a _paths=()
|
|
2392
|
+
while IFS= read -r _p; do
|
|
2393
|
+
[[ -n "$_p" ]] && _paths+=("$_p")
|
|
2394
|
+
done < <(_registry_read_raw_union)
|
|
2395
|
+
|
|
2396
|
+
# Column header.
|
|
2397
|
+
printf '%-2s %-45s %-10s %-20s %s\n' " " "PATH" "STATE" "TOOLS" "TIER"
|
|
2398
|
+
printf '%-2s %-45s %-10s %-20s %s\n' "--" "----" "-----" "-----" "----"
|
|
2399
|
+
|
|
2400
|
+
local _cwd_registered=0
|
|
2401
|
+
local _entry
|
|
2402
|
+
for _entry in "${_paths[@]+"${_paths[@]}"}"; do
|
|
2403
|
+
local _state _tools _tier _marker
|
|
2404
|
+
_state="$(_aid_project_state "$_entry")"
|
|
2405
|
+
_tools="$(_aid_project_tools "$_entry")"
|
|
2406
|
+
_tier="$(_which_tier_holds "$_entry")"
|
|
2407
|
+
_marker=" "
|
|
2408
|
+
if [[ "$_entry" == "$_cwd" ]]; then
|
|
2409
|
+
_marker="* "
|
|
2410
|
+
_cwd_registered=1
|
|
2411
|
+
fi
|
|
2412
|
+
# Truncate long tools string for display.
|
|
2413
|
+
local _tools_display="${_tools:--}"
|
|
2414
|
+
printf '%-2s %-45s %-10s %-20s %s\n' \
|
|
2415
|
+
"$_marker" "$_entry" "$_state" "$_tools_display" "$_tier"
|
|
2416
|
+
if [[ "$_verbose" -eq 1 ]]; then
|
|
2417
|
+
local _reg_src
|
|
2418
|
+
if [[ "$_tier" == "shared" && "$AID_STATE_HOME" != "${HOME}/.aid" ]]; then
|
|
2419
|
+
_reg_src="${AID_STATE_HOME}/registry.yml"
|
|
2420
|
+
else
|
|
2421
|
+
_reg_src="${HOME}/.aid/registry.yml"
|
|
2422
|
+
fi
|
|
2423
|
+
printf ' registry: %s\n' "$_reg_src"
|
|
2424
|
+
fi
|
|
2425
|
+
done
|
|
2426
|
+
|
|
2427
|
+
if [[ "${#_paths[@]}" -eq 0 ]]; then
|
|
2428
|
+
printf '(no projects registered)\n'
|
|
2429
|
+
fi
|
|
2430
|
+
|
|
2431
|
+
# Footnote: unregistered AID cwd (only when cwd is a real project, not the state home).
|
|
2432
|
+
if [[ "$_cwd_registered" -eq 0 ]] && _aid_is_project_dir "${_cwd}"; then
|
|
2433
|
+
printf '\n'
|
|
2434
|
+
printf "(here) -- not registered; run 'aid projects add'\n"
|
|
2435
|
+
fi
|
|
2436
|
+
|
|
2437
|
+
# Legend.
|
|
2438
|
+
if [[ "${#_paths[@]}" -gt 0 ]]; then
|
|
2439
|
+
printf '\n'
|
|
2440
|
+
printf '* = current directory\n'
|
|
2441
|
+
fi
|
|
2442
|
+
}
|
|
2443
|
+
|
|
2444
|
+
# _which_tier_holds <canon-path>
|
|
2445
|
+
# Returns "user" or "shared" based on which registry file contains the path.
|
|
2446
|
+
# Falls back to _aid_resolve_tier if the path is not found in either.
|
|
2447
|
+
_which_tier_holds() {
|
|
2448
|
+
local _p="$1"
|
|
2449
|
+
local _primary_reg="${AID_STATE_HOME}/registry.yml"
|
|
2450
|
+
local _user_reg="${HOME}/.aid/registry.yml"
|
|
2451
|
+
# Check shared/primary first.
|
|
2452
|
+
if [[ "$AID_STATE_HOME" != "${HOME}/.aid" ]]; then
|
|
2453
|
+
if _registry_read_repos "$_primary_reg" 2>/dev/null | grep -qxF "$_p"; then
|
|
2454
|
+
printf 'shared\n'
|
|
2455
|
+
return 0
|
|
2456
|
+
fi
|
|
2457
|
+
if _registry_read_repos "$_user_reg" 2>/dev/null | grep -qxF "$_p"; then
|
|
2458
|
+
printf 'user\n'
|
|
2459
|
+
return 0
|
|
2460
|
+
fi
|
|
2461
|
+
else
|
|
2462
|
+
# Per-user: single file.
|
|
2463
|
+
if _registry_read_repos "$_primary_reg" 2>/dev/null | grep -qxF "$_p"; then
|
|
2464
|
+
printf 'user\n'
|
|
2465
|
+
return 0
|
|
2466
|
+
fi
|
|
2467
|
+
fi
|
|
2468
|
+
# Fallback: derive from tier resolution.
|
|
2469
|
+
_aid_resolve_tier "$_p"
|
|
2470
|
+
}
|
|
2471
|
+
|
|
2472
|
+
# _cmd_projects_add [path] [verbose]
|
|
2473
|
+
# Register a project path (default: cwd) in the deterministic tier.
|
|
2474
|
+
_cmd_projects_add() {
|
|
2475
|
+
local _raw_path="${1:-.}"
|
|
2476
|
+
local _verbose="${2:-0}"
|
|
2477
|
+
|
|
2478
|
+
# Canonicalize.
|
|
2479
|
+
local _canon
|
|
2480
|
+
if ! _canon="$(cd "$_raw_path" 2>/dev/null && pwd)"; then
|
|
2481
|
+
echo "ERROR: aid projects add: path does not exist: ${_raw_path}" >&2
|
|
2482
|
+
exit 2
|
|
2483
|
+
fi
|
|
2484
|
+
|
|
2485
|
+
# Require a real AID project (.aid/ present AND not the CLI state home).
|
|
2486
|
+
if ! _aid_is_project_dir "${_canon}"; then
|
|
2487
|
+
echo "ERROR: aid projects add: '${_canon}' is not an AID project; run 'aid add <tool>' first." >&2
|
|
2488
|
+
exit 2
|
|
2489
|
+
fi
|
|
2490
|
+
|
|
2491
|
+
# Resolve tier.
|
|
2492
|
+
local _tier
|
|
2493
|
+
_tier="$(_aid_resolve_tier "$_canon")"
|
|
2494
|
+
|
|
2495
|
+
# Register (idempotent).
|
|
2496
|
+
# Suppress registry_register's own "Registered..." stdout line so we emit a
|
|
2497
|
+
# single consolidated message instead of two lines. stderr (WARN lines) is
|
|
2498
|
+
# left to flow through unchanged.
|
|
2499
|
+
registry_register "$_canon" "$_tier" >/dev/null
|
|
2500
|
+
local _rc=$?
|
|
2501
|
+
if [[ $_rc -eq 0 ]]; then
|
|
2502
|
+
printf "aid projects: '%s' registered in %s tier.\n" "$_canon" "$_tier"
|
|
2503
|
+
if [[ "$_verbose" -eq 1 ]]; then
|
|
2504
|
+
local _reg_file
|
|
2505
|
+
if [[ "$_tier" == "shared" && "$AID_STATE_HOME" != "${HOME}/.aid" ]]; then
|
|
2506
|
+
_reg_file="${AID_STATE_HOME}/registry.yml"
|
|
2507
|
+
else
|
|
2508
|
+
_reg_file="${HOME}/.aid/registry.yml"
|
|
2509
|
+
fi
|
|
2510
|
+
printf "aid projects: registry file: %s\n" "$_reg_file"
|
|
2511
|
+
fi
|
|
2512
|
+
fi
|
|
2513
|
+
return 0
|
|
2514
|
+
}
|
|
2515
|
+
|
|
2516
|
+
# _cmd_projects_remove [path] [verbose]
|
|
2517
|
+
# Unregister a project path (default: cwd) from the registry; no .aid/ required.
|
|
2518
|
+
_cmd_projects_remove() {
|
|
2519
|
+
local _raw_path="${1:-.}"
|
|
2520
|
+
local _verbose="${2:-0}"
|
|
2521
|
+
|
|
2522
|
+
# Canonicalize without requiring the directory to exist.
|
|
2523
|
+
local _canon
|
|
2524
|
+
if _canon="$(cd "$_raw_path" 2>/dev/null && pwd)"; then
|
|
2525
|
+
: # directory exists; use canonical path
|
|
2526
|
+
else
|
|
2527
|
+
# Directory absent (stale entry); use the raw path as-is after normalizing.
|
|
2528
|
+
_canon="$_raw_path"
|
|
2529
|
+
fi
|
|
2530
|
+
|
|
2531
|
+
# Unregister (idempotent; registry_unregister prints on real change).
|
|
2532
|
+
# Check first so we can emit the idempotent no-op message ourselves.
|
|
2533
|
+
local _primary_reg="${AID_STATE_HOME}/registry.yml"
|
|
2534
|
+
local _user_reg="${HOME}/.aid/registry.yml"
|
|
2535
|
+
local _found=0
|
|
2536
|
+
if _registry_read_repos "$_primary_reg" 2>/dev/null | grep -qxF "$_canon"; then
|
|
2537
|
+
_found=1
|
|
2538
|
+
elif [[ "$AID_STATE_HOME" != "${HOME}/.aid" ]]; then
|
|
2539
|
+
if _registry_read_repos "$_user_reg" 2>/dev/null | grep -qxF "$_canon"; then
|
|
2540
|
+
_found=1
|
|
2541
|
+
fi
|
|
2542
|
+
fi
|
|
2543
|
+
|
|
2544
|
+
registry_unregister "$_canon"
|
|
2545
|
+
if [[ "$_found" -eq 0 ]]; then
|
|
2546
|
+
printf "aid projects: '%s' was not registered (nothing to remove).\n" "$_canon"
|
|
2547
|
+
elif [[ "$_verbose" -eq 1 ]]; then
|
|
2548
|
+
printf "aid projects: removed '%s' from registry.\n" "$_canon"
|
|
2549
|
+
fi
|
|
2550
|
+
return 0
|
|
2551
|
+
}
|
|
2552
|
+
|
|
2553
|
+
# ---------------------------------------------------------------------------
|
|
2554
|
+
# Parse subcommand and dispatch.
|
|
2555
|
+
# ---------------------------------------------------------------------------
|
|
2556
|
+
|
|
2557
|
+
# Shared flag buckets (populated during subcommand-specific arg parsing).
|
|
2558
|
+
_AID_TOOL_ARG=""
|
|
2559
|
+
_AID_VERSION_ARG=""
|
|
2560
|
+
_AID_FROM_BUNDLE=""
|
|
2561
|
+
_AID_FORCE=0
|
|
2562
|
+
_AID_TARGET=""
|
|
2563
|
+
_AID_VERBOSE="${AID_VERBOSE:-0}"
|
|
2564
|
+
_AID_NO_PATH=0
|
|
2565
|
+
|
|
2566
|
+
# ---------------------------------------------------------------------------
|
|
2567
|
+
# Dashboard (bare 'aid' - no arguments).
|
|
2568
|
+
# ---------------------------------------------------------------------------
|
|
2569
|
+
_cmd_dashboard() {
|
|
2570
|
+
# Block 1 + 2: Header + description.
|
|
2571
|
+
local cli_version="unknown"
|
|
2572
|
+
local ver_file="${AID_CODE_HOME}/VERSION"
|
|
2573
|
+
if [[ -f "$ver_file" ]]; then
|
|
2574
|
+
cli_version="$(tr -d '[:space:]' < "$ver_file")"
|
|
2575
|
+
fi
|
|
2576
|
+
printf 'AID v%s - Agentic Iterative Development\n' "$cli_version"
|
|
2577
|
+
printf 'Install, update, and manage AID across your projects.\n'
|
|
2578
|
+
|
|
2579
|
+
# C6: format gate for cwd repo (.aid/ is guaranteed present here -- the
|
|
2580
|
+
# non-project case is intercepted at the dispatch level above via
|
|
2581
|
+
# _aid_cwd_no_aid_offer; register-on-encounter already ran via _aid_cwd_classify).
|
|
2582
|
+
# _aid_is_project_dir guards the state-home exclusion (double-check).
|
|
2583
|
+
if _aid_is_project_dir "."; then
|
|
2584
|
+
_aid_format_gate "." || return $?
|
|
2585
|
+
fi
|
|
2586
|
+
|
|
2587
|
+
# Block 3: Installed tools for cwd.
|
|
2588
|
+
printf '\n'
|
|
2589
|
+
aid_status_body "."
|
|
2590
|
+
|
|
2591
|
+
# Block 4: Usage/help.
|
|
2592
|
+
printf '\n'
|
|
2593
|
+
_aid_usage
|
|
2594
|
+
|
|
2595
|
+
# Block 5: Update check notice (final line, non-blocking).
|
|
2596
|
+
_aid_check_update
|
|
2597
|
+
}
|
|
2598
|
+
|
|
2599
|
+
# Early help check.
|
|
2600
|
+
if [[ $# -eq 0 ]]; then
|
|
2601
|
+
# C-table: if cwd is not an AID project -> offer (no hard refuse, decision #5); exit 0.
|
|
2602
|
+
# _aid_is_project_dir excludes the CLI state home from the "is project" classification.
|
|
2603
|
+
if ! _aid_is_project_dir "."; then
|
|
2604
|
+
_aid_cwd_no_aid_offer "."
|
|
2605
|
+
# _aid_cwd_no_aid_offer always exits 0.
|
|
2606
|
+
fi
|
|
2607
|
+
# C-table register-on-encounter (best-effort, never blocks bare aid).
|
|
2608
|
+
_aid_cwd_classify "."
|
|
2609
|
+
# Bare 'aid' -> dashboard landing screen.
|
|
2610
|
+
_cmd_dashboard
|
|
2611
|
+
exit $?
|
|
2612
|
+
fi
|
|
2613
|
+
|
|
2614
|
+
case "$1" in
|
|
2615
|
+
-h|--help)
|
|
2616
|
+
_aid_usage
|
|
2617
|
+
exit 0
|
|
2618
|
+
;;
|
|
2619
|
+
esac
|
|
2620
|
+
|
|
2621
|
+
SUBCMD="$1"
|
|
2622
|
+
shift
|
|
2623
|
+
|
|
2624
|
+
# ---- version ----------------------------------------------------------------
|
|
2625
|
+
if [[ "$SUBCMD" == "version" ]]; then
|
|
2626
|
+
local_version_file="${AID_CODE_HOME}/VERSION"
|
|
2627
|
+
if [[ -f "$local_version_file" ]]; then
|
|
2628
|
+
cat "$local_version_file"
|
|
2629
|
+
else
|
|
2630
|
+
echo "unknown (VERSION file not found at ${local_version_file})"
|
|
2631
|
+
fi
|
|
2632
|
+
exit 0
|
|
2633
|
+
fi
|
|
2634
|
+
|
|
2635
|
+
# ---- help -------------------------------------------------------------------
|
|
2636
|
+
if [[ "$SUBCMD" == "help" || "$SUBCMD" == "-h" || "$SUBCMD" == "--help" ]]; then
|
|
2637
|
+
_aid_usage
|
|
2638
|
+
exit 0
|
|
2639
|
+
fi
|
|
2640
|
+
|
|
2641
|
+
# ---- status -----------------------------------------------------------------
|
|
2642
|
+
if [[ "$SUBCMD" == "status" ]]; then
|
|
2643
|
+
# Parse flags for status.
|
|
2644
|
+
while [[ $# -gt 0 ]]; do
|
|
2645
|
+
case "$1" in
|
|
2646
|
+
--target)
|
|
2647
|
+
[[ $# -lt 2 ]] && _aid_die "--target requires a value" 2
|
|
2648
|
+
_AID_TARGET="$2"; shift 2 ;;
|
|
2649
|
+
--verbose) _AID_VERBOSE=1; shift ;;
|
|
2650
|
+
-h|--help) _aid_usage status; exit 0 ;;
|
|
2651
|
+
-*) _aid_die "unknown flag for status: $1" 2 ;;
|
|
2652
|
+
*) _aid_die "unexpected argument for status: $1" 2 ;;
|
|
2653
|
+
esac
|
|
2654
|
+
done
|
|
2655
|
+
# Apply env-var fallbacks.
|
|
2656
|
+
[[ -z "$_AID_TARGET" && -n "${AID_TARGET:-}" ]] && _AID_TARGET="$AID_TARGET"
|
|
2657
|
+
_AID_TARGET="${_AID_TARGET:-.}"
|
|
2658
|
+
export AID_VERBOSE="$_AID_VERBOSE"
|
|
2659
|
+
# C-table: if target is not an AID project -> offer (no hard refuse, decision #5); exit 0.
|
|
2660
|
+
# _aid_is_project_dir excludes the CLI state home from the "is project" classification.
|
|
2661
|
+
if ! _aid_is_project_dir "${_AID_TARGET}"; then
|
|
2662
|
+
_aid_cwd_no_aid_offer "${_AID_TARGET}"
|
|
2663
|
+
# _aid_cwd_no_aid_offer always exits 0.
|
|
2664
|
+
fi
|
|
2665
|
+
# C-table register-on-encounter (best-effort, never blocks status).
|
|
2666
|
+
_aid_cwd_classify "${_AID_TARGET}"
|
|
2667
|
+
# C6: format gate for status target (only when target is a real project).
|
|
2668
|
+
_aid_format_gate "${_AID_TARGET}" || exit $?
|
|
2669
|
+
aid_status "$_AID_TARGET"
|
|
2670
|
+
_status_rc=$?
|
|
2671
|
+
# Update check notice appended after status output (non-blocking).
|
|
2672
|
+
_aid_check_update
|
|
2673
|
+
exit $_status_rc
|
|
2674
|
+
fi
|
|
2675
|
+
|
|
2676
|
+
# ---- update -----------------------------------------------------------------
|
|
2677
|
+
if [[ "$SUBCMD" == "update" ]]; then
|
|
2678
|
+
# Check for 'update self' as first positional arg.
|
|
2679
|
+
if [[ $# -gt 0 && "$1" == "self" ]]; then
|
|
2680
|
+
shift
|
|
2681
|
+
# Consume any flags after 'self'.
|
|
2682
|
+
_SELF_FROM_BUNDLE=""
|
|
2683
|
+
_SELF_DRYRUN=0
|
|
2684
|
+
while [[ $# -gt 0 ]]; do
|
|
2685
|
+
case "$1" in
|
|
2686
|
+
--force|-y) shift ;; # no-op for update self
|
|
2687
|
+
--from-bundle)
|
|
2688
|
+
[[ $# -lt 2 ]] && _aid_die "--from-bundle requires a value" 2
|
|
2689
|
+
_SELF_FROM_BUNDLE="$2"; shift 2 ;;
|
|
2690
|
+
--dry-run) _SELF_DRYRUN=1; shift ;;
|
|
2691
|
+
-h|--help) _aid_usage update; exit 0 ;;
|
|
2692
|
+
*) _aid_die "unknown flag for 'update self': $1" 2 ;;
|
|
2693
|
+
esac
|
|
2694
|
+
done
|
|
2695
|
+
export _SELF_FROM_BUNDLE _SELF_DRYRUN
|
|
2696
|
+
_cmd_update_self; _us_rc=$?
|
|
2697
|
+
if [[ "${_us_rc}" -ne 0 ]]; then exit "${_us_rc}"; fi
|
|
2698
|
+
# Post-update: registry-driven migration (feature-004).
|
|
2699
|
+
# Iterate _registry_read_union -- NO scan -- with All/Yes/No/Cancel per-repo
|
|
2700
|
+
# consent walk. Unregistered repos are caught lazily by the per-repo stamp.
|
|
2701
|
+
# No .migrated marker is written (removed; stamp in settings.yml is the record).
|
|
2702
|
+
# dry-run: the install step already printed its command; skip migration silently.
|
|
2703
|
+
if [[ "${_SELF_DRYRUN:-0}" != "1" ]]; then
|
|
2704
|
+
_us_migrate_all=0
|
|
2705
|
+
_us_migrate_cancel=0
|
|
2706
|
+
# Read the union of registered repos (quiet-prunes stale entries).
|
|
2707
|
+
_us_repos=()
|
|
2708
|
+
while IFS= read -r _us_r; do
|
|
2709
|
+
[[ -n "$_us_r" ]] && _us_repos+=("$_us_r")
|
|
2710
|
+
done < <(_registry_read_union)
|
|
2711
|
+
if [[ "${#_us_repos[@]}" -eq 0 ]]; then
|
|
2712
|
+
echo "No registered projects to migrate."
|
|
2713
|
+
else
|
|
2714
|
+
# Determine interactive mode: AID_MIGRATE_YES=1 is the explicit opt-in for
|
|
2715
|
+
# auto-yes. Non-interactive without opt-in -> no migration (per SPEC: without
|
|
2716
|
+
# opt-in, no migration is forced). Test /dev/tty by attempting to open it.
|
|
2717
|
+
_us_auto_yes=0
|
|
2718
|
+
_us_have_tty=0
|
|
2719
|
+
[[ "${AID_MIGRATE_YES:-0}" == "1" ]] && _us_auto_yes=1
|
|
2720
|
+
{ exec 3</dev/tty; } 2>/dev/null && { _us_have_tty=1; exec 3>&-; } || true
|
|
2721
|
+
if [[ "$_us_auto_yes" -eq 0 && "$_us_have_tty" -eq 0 ]]; then
|
|
2722
|
+
# Non-interactive, no opt-in: skip all (non-interactive default, SPEC edge-cases).
|
|
2723
|
+
echo "Skipping project migration (non-interactive; set AID_MIGRATE_YES=1 to opt in)."
|
|
2724
|
+
else
|
|
2725
|
+
for _us_repo in "${_us_repos[@]}"; do
|
|
2726
|
+
if [[ "$_us_migrate_cancel" -eq 1 ]]; then
|
|
2727
|
+
break
|
|
2728
|
+
fi
|
|
2729
|
+
if [[ "$_us_migrate_all" -eq 1 || "$_us_auto_yes" -eq 1 ]]; then
|
|
2730
|
+
_us_answer="y"
|
|
2731
|
+
else
|
|
2732
|
+
printf 'Migrate project %s? [All/Yes/No/Cancel] ' "$_us_repo"
|
|
2733
|
+
_us_answer=""
|
|
2734
|
+
read -r _us_answer < /dev/tty
|
|
2735
|
+
fi
|
|
2736
|
+
case "$_us_answer" in
|
|
2737
|
+
[Aa]|all|All|ALL)
|
|
2738
|
+
_us_migrate_all=1
|
|
2739
|
+
_aid_migrate_repo "$_us_repo"
|
|
2740
|
+
;;
|
|
2741
|
+
[Yy]|yes|Yes|YES)
|
|
2742
|
+
_aid_migrate_repo "$_us_repo"
|
|
2743
|
+
;;
|
|
2744
|
+
[Cc]|cancel|Cancel|CANCEL)
|
|
2745
|
+
_us_migrate_cancel=1
|
|
2746
|
+
echo "Migration cancelled."
|
|
2747
|
+
;;
|
|
2748
|
+
*)
|
|
2749
|
+
echo "Skipped: ${_us_repo}"
|
|
2750
|
+
;;
|
|
2751
|
+
esac
|
|
2752
|
+
done
|
|
2753
|
+
fi
|
|
2754
|
+
fi
|
|
2755
|
+
fi
|
|
2756
|
+
exit 0
|
|
2757
|
+
fi
|
|
2758
|
+
# Fall through to the shared add/update handler below.
|
|
2759
|
+
fi
|
|
2760
|
+
|
|
2761
|
+
# ---- remove -----------------------------------------------------------------
|
|
2762
|
+
if [[ "$SUBCMD" == "remove" ]]; then
|
|
2763
|
+
# Check for 'remove self' as first positional arg.
|
|
2764
|
+
if [[ $# -gt 0 && "$1" == "self" ]]; then
|
|
2765
|
+
shift
|
|
2766
|
+
_cmd_remove_self "$@"
|
|
2767
|
+
# _cmd_remove_self always exits.
|
|
2768
|
+
fi
|
|
2769
|
+
|
|
2770
|
+
# Check for 'remove' with no tool args (remove ALL from project).
|
|
2771
|
+
# We do this check after parsing flags below, so continue to parse first.
|
|
2772
|
+
fi
|
|
2773
|
+
|
|
2774
|
+
# ---- dashboard --------------------------------------------------------------
|
|
2775
|
+
if [[ "$SUBCMD" == "dashboard" ]]; then
|
|
2776
|
+
_cmd_dashboard_ctl "$@"
|
|
2777
|
+
exit $?
|
|
2778
|
+
fi
|
|
2779
|
+
|
|
2780
|
+
# ---- projects ---------------------------------------------------------------
|
|
2781
|
+
if [[ "$SUBCMD" == "projects" ]]; then
|
|
2782
|
+
# Check for -h/--help as first arg before dispatching.
|
|
2783
|
+
if [[ "${1:-}" == "-h" || "${1:-}" == "--help" ]]; then
|
|
2784
|
+
_aid_usage projects
|
|
2785
|
+
exit 0
|
|
2786
|
+
fi
|
|
2787
|
+
# Determine sub-action (first positional or default "list").
|
|
2788
|
+
# Scan through leading flags to find the action word; unknown positionals are
|
|
2789
|
+
# rejected here so errors surface before entering _cmd_projects.
|
|
2790
|
+
_PROJ_ACTION="list"
|
|
2791
|
+
_PROJ_ARGS=()
|
|
2792
|
+
while [[ $# -gt 0 ]]; do
|
|
2793
|
+
case "$1" in
|
|
2794
|
+
list|add|remove|help)
|
|
2795
|
+
_PROJ_ACTION="$1"; shift
|
|
2796
|
+
_PROJ_ARGS+=("$@")
|
|
2797
|
+
set --
|
|
2798
|
+
break
|
|
2799
|
+
;;
|
|
2800
|
+
-h|--help) _aid_usage projects; exit 0 ;;
|
|
2801
|
+
--local|--shared|--verbose)
|
|
2802
|
+
_PROJ_ARGS+=("$1"); shift ;;
|
|
2803
|
+
-*)
|
|
2804
|
+
# Unknown flag: pass through to _cmd_projects for rejection.
|
|
2805
|
+
_PROJ_ARGS+=("$1"); shift ;;
|
|
2806
|
+
*)
|
|
2807
|
+
echo "ERROR: aid projects: unknown action: ${1} (expected: list, add, remove, help)" >&2
|
|
2808
|
+
exit 2
|
|
2809
|
+
;;
|
|
2810
|
+
esac
|
|
2811
|
+
done
|
|
2812
|
+
_AID_TIER_OVERRIDE="${_AID_TIER_OVERRIDE:-}"
|
|
2813
|
+
_cmd_projects "$_PROJ_ACTION" "${_PROJ_ARGS[@]+"${_PROJ_ARGS[@]}"}"
|
|
2814
|
+
exit $?
|
|
2815
|
+
fi
|
|
2816
|
+
|
|
2817
|
+
# ---- __migrate-repo (hidden, callable-core only -- task-077/081) ------------
|
|
2818
|
+
if [[ "$SUBCMD" == "__migrate-repo" ]]; then
|
|
2819
|
+
if [[ $# -lt 1 ]]; then
|
|
2820
|
+
echo "ERROR: aid __migrate-repo requires a <repo> path argument" >&2
|
|
2821
|
+
exit 2
|
|
2822
|
+
fi
|
|
2823
|
+
_MIG_TARGET="$1"
|
|
2824
|
+
if [[ ! -d "${_MIG_TARGET}" ]]; then
|
|
2825
|
+
echo "ERROR: aid __migrate-repo: not a directory: ${_MIG_TARGET}" >&2
|
|
2826
|
+
exit 2
|
|
2827
|
+
fi
|
|
2828
|
+
_MIG_TARGET="$(cd "${_MIG_TARGET}" && pwd)"
|
|
2829
|
+
_aid_migrate_repo "${_MIG_TARGET}"
|
|
2830
|
+
exit 0
|
|
2831
|
+
fi
|
|
2832
|
+
|
|
2833
|
+
# ---- add / remove / update --------------------------------------------------
|
|
2834
|
+
# These subcommands all share flag parsing; we then call the engine functions
|
|
2835
|
+
# (install_tool / uninstall_tool) directly through a per-tool loop, exactly as
|
|
2836
|
+
# install.sh does. We build a temporary staging area for install/update, and
|
|
2837
|
+
# reuse the same prepare_tool_staging + install_tool / uninstall_tool pattern.
|
|
2838
|
+
|
|
2839
|
+
# First, validate the subcommand.
|
|
2840
|
+
case "$SUBCMD" in
|
|
2841
|
+
add|remove|update) ;;
|
|
2842
|
+
*)
|
|
2843
|
+
echo "ERROR: aid: unknown command: ${SUBCMD} (see 'aid -h')" >&2
|
|
2844
|
+
exit 2
|
|
2845
|
+
;;
|
|
2846
|
+
esac
|
|
2847
|
+
|
|
2848
|
+
|
|
2849
|
+
# Collect positional tool args (comma-separated or space-separated before flags).
|
|
663
2850
|
_AID_POSITIONAL_TOOLS=""
|
|
664
2851
|
_AID_REMOVE_FORCE=0
|
|
665
2852
|
|
|
@@ -709,6 +2896,24 @@ if [[ ! -d "$_AID_TARGET" ]]; then
|
|
|
709
2896
|
fi
|
|
710
2897
|
_AID_TARGET="$(cd "$_AID_TARGET" && pwd)"
|
|
711
2898
|
|
|
2899
|
+
# ---- C-table pre-check for 'update [tool]': missing .aid/ -> offer + exit 0 ----
|
|
2900
|
+
# Must run BEFORE _resolve_tools_for_aid so we never reach exit-6 when no .aid/ exists.
|
|
2901
|
+
# 'add' uses the B-table (checked inside the dispatch case); 'remove' is not in C-table.
|
|
2902
|
+
# _aid_is_project_dir excludes the CLI state home from the "is project" classification.
|
|
2903
|
+
if [[ "$SUBCMD" == "update" ]]; then
|
|
2904
|
+
if ! _aid_is_project_dir "${_AID_TARGET}"; then
|
|
2905
|
+
_aid_cwd_no_aid_offer "${_AID_TARGET}"
|
|
2906
|
+
# _aid_cwd_no_aid_offer always exits 0.
|
|
2907
|
+
fi
|
|
2908
|
+
fi
|
|
2909
|
+
|
|
2910
|
+
# ---- Self-update-if-needed preamble (FF-3 / CLI-2 / task-079) --------------
|
|
2911
|
+
# For 'update [<tool>]' only (not 'add', not 'update self'). Ensures the CLI
|
|
2912
|
+
# is current before the per-repo migration runs (FR38 / OQ-6). WARN-not-fail.
|
|
2913
|
+
if [[ "$SUBCMD" == "update" ]]; then
|
|
2914
|
+
_aid_update_self_if_stale
|
|
2915
|
+
fi
|
|
2916
|
+
|
|
712
2917
|
# Strip leading 'v' from version.
|
|
713
2918
|
_AID_VERSION_ARG="${_AID_VERSION_ARG#v}"
|
|
714
2919
|
|
|
@@ -858,8 +3063,22 @@ _prepare_tool_staging_aid() {
|
|
|
858
3063
|
verify_bundle_checksum "$tarball" || exit $?
|
|
859
3064
|
local tbase
|
|
860
3065
|
tbase="$(basename "$tarball")"
|
|
861
|
-
|
|
862
|
-
|
|
3066
|
+
# Derive the version STRICTLY from the canonical tool-bundle name
|
|
3067
|
+
# (aid-<tool>-v<semver>.tar.gz). Never stamp a raw filename: a name that
|
|
3068
|
+
# does not match this shape is the wrong artifact (e.g. the CLI installer
|
|
3069
|
+
# package aid-installer-<ver>.tgz), not a tool bundle. Fall back to an
|
|
3070
|
+
# explicit --version, else fail loudly rather than recording garbage.
|
|
3071
|
+
if [[ "$tbase" =~ ^aid-${tool}-v([0-9]+\.[0-9]+\.[0-9]+([.+-][0-9A-Za-z.+-]+)?)\.tar\.gz$ ]]; then
|
|
3072
|
+
_AID_RESOLVED_VERSION="${BASH_REMATCH[1]}"
|
|
3073
|
+
elif [[ -n "${version:-}" ]]; then
|
|
3074
|
+
_AID_RESOLVED_VERSION="$version"
|
|
3075
|
+
else
|
|
3076
|
+
echo "ERROR: aid: '${tbase}' is not a valid AID tool bundle for '${tool}'." >&2
|
|
3077
|
+
echo " Expected a tarball named 'aid-${tool}-v<version>.tar.gz'." >&2
|
|
3078
|
+
echo " (Did you pass the CLI installer package instead of a tool bundle?" >&2
|
|
3079
|
+
echo " Point --from-bundle at a release staging dir or an aid-${tool}-v*.tar.gz file.)" >&2
|
|
3080
|
+
exit 2
|
|
3081
|
+
fi
|
|
863
3082
|
extract_tarball "$tarball" "$tool_staging" || exit $?
|
|
864
3083
|
else
|
|
865
3084
|
if [[ -z "$version" ]]; then
|
|
@@ -880,31 +3099,59 @@ _prepare_tool_staging_aid() {
|
|
|
880
3099
|
# ---------------------------------------------------------------------------
|
|
881
3100
|
# Dispatch to engine.
|
|
882
3101
|
# ---------------------------------------------------------------------------
|
|
883
|
-
_AID_OVERALL_BLOCKED=0
|
|
884
|
-
|
|
885
3102
|
case "$SUBCMD" in
|
|
886
3103
|
add|update)
|
|
3104
|
+
# B-table (for 'add'): writability pre-check BEFORE any .aid/ is created.
|
|
3105
|
+
# Decision #3: never elevate .aid/ creation -- error if folder is not writable.
|
|
3106
|
+
if [[ "$SUBCMD" == "add" ]]; then
|
|
3107
|
+
if [[ ! -w "$_AID_TARGET" ]]; then
|
|
3108
|
+
echo "ERROR: aid: add: target directory is not writable: ${_AID_TARGET}" >&2
|
|
3109
|
+
echo "ERROR: aid: add: AID will not create a root-owned .aid/ -- fix folder permissions and retry." >&2
|
|
3110
|
+
rm -rf "$_AID_STAGING_BASE"
|
|
3111
|
+
exit 1
|
|
3112
|
+
fi
|
|
3113
|
+
fi
|
|
3114
|
+
|
|
3115
|
+
# C-table (for 'update [tool]'): register-on-encounter + format gate.
|
|
3116
|
+
# The missing-.aid/ case was already intercepted above (pre-resolve-tools).
|
|
3117
|
+
if [[ "$SUBCMD" == "update" ]]; then
|
|
3118
|
+
# C-table register-on-encounter (best-effort).
|
|
3119
|
+
_aid_cwd_classify "${_AID_TARGET}"
|
|
3120
|
+
# C6: format gate for the update repo path.
|
|
3121
|
+
_aid_format_gate "${_AID_TARGET}" || exit $?
|
|
3122
|
+
fi
|
|
3123
|
+
|
|
887
3124
|
for _tool in "${_AID_TOOLS[@]}"; do
|
|
888
3125
|
echo ""
|
|
889
3126
|
_prepare_tool_staging_aid "$_tool" "$_AID_VERSION_ARG" "$_AID_FROM_BUNDLE"
|
|
890
3127
|
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
|
-
}
|
|
3128
|
+
install_tool "$_AID_STAGING_DIR" "$_tool" "$_AID_TARGET" "$_AID_RESOLVED_VERSION" "$_AID_FORCE" || exit $?
|
|
899
3129
|
done
|
|
900
3130
|
|
|
901
3131
|
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
3132
|
echo "Done. AID ${_AID_RESOLVED_VERSION:-} installed into: ${_AID_TARGET}"
|
|
3133
|
+
|
|
3134
|
+
# B-table (for 'add'): tier-aware registration after successful install.
|
|
3135
|
+
# Decision #3 (unwritable) already handled above with error+abort.
|
|
3136
|
+
if [[ "$SUBCMD" == "add" ]]; then
|
|
3137
|
+
# FR7: deterministic, non-interactive tier selection via _aid_resolve_tier.
|
|
3138
|
+
# Honors _AID_TIER_OVERRIDE (--local/--shared) if already set by caller.
|
|
3139
|
+
_btab_tier="$(_aid_resolve_tier "$_AID_TARGET")"
|
|
3140
|
+
registry_register "$_AID_TARGET" "$_btab_tier"
|
|
3141
|
+
else
|
|
3142
|
+
# 'update [tool]': C-table register-on-encounter already ran above.
|
|
3143
|
+
# The post-install register is idempotent; route via user tier.
|
|
3144
|
+
registry_register "$_AID_TARGET" "user"
|
|
3145
|
+
fi
|
|
3146
|
+
|
|
3147
|
+
# FF-3 / CLI-2 / task-079: per-repo migration on the 'update' reach only.
|
|
3148
|
+
# Runs on the already-CAN-1-canonicalized $_AID_TARGET (cd && pwd above).
|
|
3149
|
+
# The registry_register above already ran, so migration step 4 is an
|
|
3150
|
+
# idempotent no-op; steps 1-3 run per FF-1. WARN-not-fail (NFR12):
|
|
3151
|
+
# migration never changes the tool-update exit code.
|
|
3152
|
+
if [[ "$SUBCMD" == "update" ]]; then
|
|
3153
|
+
_aid_migrate_repo "$_AID_TARGET"
|
|
3154
|
+
fi
|
|
908
3155
|
exit 0
|
|
909
3156
|
;;
|
|
910
3157
|
|
|
@@ -926,6 +3173,10 @@ case "$SUBCMD" in
|
|
|
926
3173
|
|
|
927
3174
|
echo ""
|
|
928
3175
|
echo "Uninstall complete."
|
|
3176
|
+
# DR-1 registry side-effect: unregister repo only when the manifest is now gone (last tool removed).
|
|
3177
|
+
if [[ ! -f "$_AID_MANIFEST" ]]; then
|
|
3178
|
+
registry_unregister "$_AID_TARGET"
|
|
3179
|
+
fi
|
|
929
3180
|
exit 0
|
|
930
3181
|
;;
|
|
931
3182
|
esac
|