aid-installer 0.7.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +17 -0
- package/VERSION +1 -0
- package/bin/aid +931 -0
- package/bin/aid.cmd +24 -0
- package/bin/aid.js +70 -0
- package/bin/aid.ps1 +875 -0
- package/lib/AidInstallCore.psm1 +1411 -0
- package/lib/aid-install-core.sh +1646 -0
- package/package.json +36 -0
|
@@ -0,0 +1,1646 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# aid-install-core.sh - Shared Bash install-core library for the AID installer.
|
|
3
|
+
#
|
|
4
|
+
# Purpose:
|
|
5
|
+
# Sourceable library of pure functions used by install.sh (Bash bootstrap) and
|
|
6
|
+
# any future in-language caller. No top-level side effects when sourced - every
|
|
7
|
+
# function is defined here; nothing executes at source time.
|
|
8
|
+
#
|
|
9
|
+
# Provides:
|
|
10
|
+
# sha256_file <path> -> hex sha256 of file content (stdout)
|
|
11
|
+
# normalize_tool <id> -> canonical lower-case-hyphen tool id (stdout)
|
|
12
|
+
# detect_tool <target> -> detected tool id (stdout); exit 2 on 0 or >1 matches
|
|
13
|
+
# resolve_version -> latest GitHub release version (stdout); exit 3 on fail
|
|
14
|
+
# fetch_tarball <tool> <ver> <dest_dir>
|
|
15
|
+
# -> downloads aid-<tool>-v<ver>.tar.gz + SHA256SUMS into
|
|
16
|
+
# dest_dir; verifies sha256; exit 3 on fetch, 4 on mismatch
|
|
17
|
+
# extract_tarball <tarball> <dest_dir>
|
|
18
|
+
# -> extracts tarball (flat root) into dest_dir; exit 1 on fail
|
|
19
|
+
# verify_bundle_checksum <tarball>
|
|
20
|
+
# -> verifies sibling SHA256SUMS when present; exit 4 on mismatch
|
|
21
|
+
# copy_file <src> <dst> [force]
|
|
22
|
+
# -> copy semantics (skip-identical/skip-on-diff/force)
|
|
23
|
+
# copy_dir <src_dir> <dst_dir> [force]
|
|
24
|
+
# -> recursive copy via copy_file
|
|
25
|
+
# install_tool <staging> <tool> <target> <version> [force]
|
|
26
|
+
# -> run the full install for one tool (copy + manifest + protect-on-diff)
|
|
27
|
+
# manifest_read_tool_paths <manifest> <tool>
|
|
28
|
+
# -> newline-delimited paths from tools.<tool>.paths (stdout)
|
|
29
|
+
# manifest_read_tool_version <manifest> <tool>
|
|
30
|
+
# -> version string from tools.<tool>.version (stdout)
|
|
31
|
+
# manifest_read_root_agent <manifest> <tool> <path>
|
|
32
|
+
# -> sha256 from root_agent_files entry (stdout); empty if absent
|
|
33
|
+
# manifest_read_root_agent_status <manifest> <tool> <path>
|
|
34
|
+
# -> status field from root_agent_files entry (stdout)
|
|
35
|
+
# manifest_write <manifest> <tool> <version> <paths_arr_name> <root_entries_arr_name>
|
|
36
|
+
# -> atomically writes/merges the manifest JSON
|
|
37
|
+
# manifest_remove_tool <manifest> <tool>
|
|
38
|
+
# -> removes a tool section from the manifest
|
|
39
|
+
# manifest_exists <manifest> -> exit 0 when manifest exists and is parseable, else exit 6
|
|
40
|
+
# uninstall_tool <manifest> <tool> <target>
|
|
41
|
+
# -> manifest-driven removal of one tool's files
|
|
42
|
+
# write_version_marker <target> <version>
|
|
43
|
+
# -> writes <target>/.aid/.aid-version
|
|
44
|
+
#
|
|
45
|
+
# Verbose mode:
|
|
46
|
+
# Set AID_VERBOSE=1 (or pass --verbose to install.sh) to print per-file
|
|
47
|
+
# Copied:/Up to date:/Updated:/Skipped:/Removed: lines. Default (0) prints
|
|
48
|
+
# only the per-tool summary line. Protect-on-diff WARNs and errors always show.
|
|
49
|
+
#
|
|
50
|
+
# Exit codes (from install.sh):
|
|
51
|
+
# 0 success
|
|
52
|
+
# 1 generic runtime failure
|
|
53
|
+
# 2 usage error
|
|
54
|
+
# 3 network / fetch failure
|
|
55
|
+
# 4 checksum mismatch
|
|
56
|
+
# 5 protect-on-diff blocked (without --force)
|
|
57
|
+
# 6 uninstall with no manifest
|
|
58
|
+
|
|
59
|
+
# Guard against being sourced more than once.
|
|
60
|
+
[[ -n "${_AID_INSTALL_CORE_LOADED:-}" ]] && return 0
|
|
61
|
+
_AID_INSTALL_CORE_LOADED=1
|
|
62
|
+
|
|
63
|
+
# ---------------------------------------------------------------------------
|
|
64
|
+
# Constants
|
|
65
|
+
# ---------------------------------------------------------------------------
|
|
66
|
+
|
|
67
|
+
AID_REPO_SLUG="AndreVianna/aid-methodology"
|
|
68
|
+
AID_API_BASE="https://api.github.com/repos/${AID_REPO_SLUG}"
|
|
69
|
+
AID_DOWNLOAD_BASE="https://github.com/${AID_REPO_SLUG}/releases/download"
|
|
70
|
+
|
|
71
|
+
# Canonical tool ids.
|
|
72
|
+
AID_TOOLS=(claude-code codex cursor copilot-cli antigravity)
|
|
73
|
+
|
|
74
|
+
# Root agent file per tool.
|
|
75
|
+
_root_agent_file() {
|
|
76
|
+
case "$1" in
|
|
77
|
+
claude-code) echo "CLAUDE.md" ;;
|
|
78
|
+
codex|cursor|copilot-cli|antigravity) echo "AGENTS.md" ;;
|
|
79
|
+
esac
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
# ---------------------------------------------------------------------------
|
|
83
|
+
# Utility
|
|
84
|
+
# ---------------------------------------------------------------------------
|
|
85
|
+
|
|
86
|
+
# sha256_file <path> - print lower-case hex sha256 of file.
|
|
87
|
+
sha256_file() {
|
|
88
|
+
local f="$1"
|
|
89
|
+
if command -v sha256sum >/dev/null 2>&1; then
|
|
90
|
+
sha256sum "$f" | awk '{print $1}'
|
|
91
|
+
elif command -v shasum >/dev/null 2>&1; then
|
|
92
|
+
shasum -a 256 "$f" | awk '{print $1}'
|
|
93
|
+
else
|
|
94
|
+
echo "ERROR: aid-install-core: no sha256 utility (need sha256sum or shasum)" >&2
|
|
95
|
+
return 1
|
|
96
|
+
fi
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
# ---------------------------------------------------------------------------
|
|
100
|
+
# Tool-id normalization + detection
|
|
101
|
+
# ---------------------------------------------------------------------------
|
|
102
|
+
|
|
103
|
+
# normalize_tool <input> - print canonical id or exit 2 on unknown.
|
|
104
|
+
normalize_tool() {
|
|
105
|
+
local raw
|
|
106
|
+
raw="$(echo "$1" | tr '[:upper:]' '[:lower:]')"
|
|
107
|
+
case "$raw" in
|
|
108
|
+
claude-code|claudecode) echo "claude-code" ;;
|
|
109
|
+
codex) echo "codex" ;;
|
|
110
|
+
cursor) echo "cursor" ;;
|
|
111
|
+
copilot-cli|copilotcli) echo "copilot-cli" ;;
|
|
112
|
+
antigravity) echo "antigravity" ;;
|
|
113
|
+
*)
|
|
114
|
+
echo "ERROR: aid-install-core: unknown tool id: $1 (valid: claude-code, codex, cursor, copilot-cli, antigravity)" >&2
|
|
115
|
+
return 2
|
|
116
|
+
;;
|
|
117
|
+
esac
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
# detect_tool <target> - auto-detect the installed host tool from tree markers.
|
|
121
|
+
# Prints the canonical id. Exits 2 on ambiguous (>1) or undetectable (0).
|
|
122
|
+
detect_tool() {
|
|
123
|
+
local target="$1"
|
|
124
|
+
local found=()
|
|
125
|
+
|
|
126
|
+
[[ -d "${target}/.claude" ]] && found+=("claude-code")
|
|
127
|
+
# codex: .codex or .agents dir
|
|
128
|
+
if [[ -d "${target}/.codex" || -d "${target}/.agents" ]]; then
|
|
129
|
+
found+=("codex")
|
|
130
|
+
fi
|
|
131
|
+
[[ -d "${target}/.cursor" ]] && found+=("cursor")
|
|
132
|
+
# copilot-cli: .github with AID copilot subtree (.github/agents/ or .github/skills/)
|
|
133
|
+
if [[ -d "${target}/.github" ]] && \
|
|
134
|
+
( [[ -d "${target}/.github/agents" ]] || [[ -d "${target}/.github/skills" ]] ); then
|
|
135
|
+
found+=("copilot-cli")
|
|
136
|
+
fi
|
|
137
|
+
[[ -d "${target}/.agent" ]] && found+=("antigravity")
|
|
138
|
+
|
|
139
|
+
if [[ "${#found[@]}" -eq 1 ]]; then
|
|
140
|
+
echo "${found[0]}"
|
|
141
|
+
return 0
|
|
142
|
+
elif [[ "${#found[@]}" -eq 0 ]]; then
|
|
143
|
+
echo "ERROR: cannot auto-detect host tool; pass --tool <name>" >&2
|
|
144
|
+
return 2
|
|
145
|
+
else
|
|
146
|
+
local list
|
|
147
|
+
list="$(printf '%s, ' "${found[@]}")"
|
|
148
|
+
list="${list%, }"
|
|
149
|
+
echo "ERROR: ambiguous host tool (found: ${list}); pass --tool <name>" >&2
|
|
150
|
+
return 2
|
|
151
|
+
fi
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
# ---------------------------------------------------------------------------
|
|
155
|
+
# Version resolution (online)
|
|
156
|
+
# ---------------------------------------------------------------------------
|
|
157
|
+
|
|
158
|
+
# resolve_version - fetch the latest release tag from GitHub API.
|
|
159
|
+
# Prints the version without leading 'v'. Exits 3 on failure.
|
|
160
|
+
resolve_version() {
|
|
161
|
+
local url="${AID_API_BASE}/releases/latest"
|
|
162
|
+
local curl_args=(-fsSL)
|
|
163
|
+
# Optional bearer token for rate-limit relief.
|
|
164
|
+
local token="${GITHUB_TOKEN:-${GH_TOKEN:-}}"
|
|
165
|
+
if [[ -n "$token" ]]; then
|
|
166
|
+
curl_args+=(-H "Authorization: Bearer ${token}")
|
|
167
|
+
fi
|
|
168
|
+
|
|
169
|
+
local response
|
|
170
|
+
response="$(curl "${curl_args[@]}" "$url" 2>/dev/null)" || {
|
|
171
|
+
echo "ERROR: aid-install-core: failed to fetch ${url}" >&2
|
|
172
|
+
return 3
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
# Extract tag_name via grep+sed (no jq required).
|
|
176
|
+
local tag
|
|
177
|
+
tag="$(echo "$response" | grep '"tag_name"' | head -1 | sed 's/.*"tag_name"[[:space:]]*:[[:space:]]*"\([^"]*\)".*/\1/')"
|
|
178
|
+
if [[ -z "$tag" ]]; then
|
|
179
|
+
echo "ERROR: aid-install-core: could not parse tag_name from GitHub API response" >&2
|
|
180
|
+
return 3
|
|
181
|
+
fi
|
|
182
|
+
# Strip leading 'v'
|
|
183
|
+
echo "${tag#v}"
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
# ---------------------------------------------------------------------------
|
|
187
|
+
# Fetch + extract (online mode)
|
|
188
|
+
# ---------------------------------------------------------------------------
|
|
189
|
+
|
|
190
|
+
# fetch_tarball <tool> <version> <dest_dir>
|
|
191
|
+
# Downloads the tarball + SHA256SUMS into dest_dir and verifies.
|
|
192
|
+
# Exits 3 on fetch failure, 4 on checksum mismatch.
|
|
193
|
+
fetch_tarball() {
|
|
194
|
+
local tool="$1" version="$2" dest_dir="$3"
|
|
195
|
+
local filename="aid-${tool}-v${version}.tar.gz"
|
|
196
|
+
local url="${AID_DOWNLOAD_BASE}/v${version}/${filename}"
|
|
197
|
+
local sums_url="${AID_DOWNLOAD_BASE}/v${version}/SHA256SUMS"
|
|
198
|
+
local tarball="${dest_dir}/${filename}"
|
|
199
|
+
local sums_file="${dest_dir}/SHA256SUMS"
|
|
200
|
+
|
|
201
|
+
local curl_args=(-fsSL)
|
|
202
|
+
local token="${GITHUB_TOKEN:-${GH_TOKEN:-}}"
|
|
203
|
+
if [[ -n "$token" ]]; then
|
|
204
|
+
curl_args+=(-H "Authorization: Bearer ${token}")
|
|
205
|
+
fi
|
|
206
|
+
|
|
207
|
+
echo "Fetching ${filename} ..." >&2
|
|
208
|
+
curl "${curl_args[@]}" -o "$tarball" "$url" || {
|
|
209
|
+
echo "ERROR: aid-install-core: failed to download ${url}" >&2
|
|
210
|
+
return 3
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
# Fetch SHA256SUMS (best-effort: warn if absent on older releases).
|
|
214
|
+
if curl "${curl_args[@]}" -o "$sums_file" "$sums_url" 2>/dev/null; then
|
|
215
|
+
_verify_checksum "$tarball" "$sums_file" || return 4
|
|
216
|
+
else
|
|
217
|
+
echo "WARN: aid-install-core: SHA256SUMS not available for v${version}; skipping checksum verification" >&2
|
|
218
|
+
fi
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
# _verify_checksum <tarball> <sums_file>
|
|
222
|
+
# Fails with exit 4 if the tarball's sha256 is not in the sums file.
|
|
223
|
+
_verify_checksum() {
|
|
224
|
+
local tarball="$1" sums_file="$2"
|
|
225
|
+
local filename
|
|
226
|
+
filename="$(basename "$tarball")"
|
|
227
|
+
|
|
228
|
+
local expected
|
|
229
|
+
expected="$(grep "[[:space:]]${filename}$" "$sums_file" | awk '{print $1}')"
|
|
230
|
+
if [[ -z "$expected" ]]; then
|
|
231
|
+
echo "ERROR: aid-install-core: ${filename} not found in SHA256SUMS" >&2
|
|
232
|
+
return 4
|
|
233
|
+
fi
|
|
234
|
+
|
|
235
|
+
local actual
|
|
236
|
+
actual="$(sha256_file "$tarball")"
|
|
237
|
+
if [[ "$actual" != "$expected" ]]; then
|
|
238
|
+
echo "ERROR: aid-install-core: checksum mismatch for ${filename}: expected ${expected}, got ${actual}" >&2
|
|
239
|
+
return 4
|
|
240
|
+
fi
|
|
241
|
+
echo "Checksum OK: ${filename}" >&2
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
# verify_bundle_checksum <tarball>
|
|
245
|
+
# Checks for a sibling SHA256SUMS file next to the tarball.
|
|
246
|
+
# No-op if absent; exits 4 on mismatch.
|
|
247
|
+
verify_bundle_checksum() {
|
|
248
|
+
local tarball="$1"
|
|
249
|
+
local dir
|
|
250
|
+
dir="$(dirname "$tarball")"
|
|
251
|
+
local sums_file="${dir}/SHA256SUMS"
|
|
252
|
+
[[ -f "$sums_file" ]] || return 0
|
|
253
|
+
_verify_checksum "$tarball" "$sums_file" || return 4
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
# extract_tarball <tarball> <dest_dir>
|
|
257
|
+
# Extracts into dest_dir. feature-002 S2.3 guarantees a flat-root tarball (no
|
|
258
|
+
# wrapping top-level directory). Asserts this contract and fails loudly when
|
|
259
|
+
# violated rather than silently stripping components.
|
|
260
|
+
extract_tarball() {
|
|
261
|
+
local tarball="$1" dest_dir="$2"
|
|
262
|
+
mkdir -p "$dest_dir"
|
|
263
|
+
|
|
264
|
+
# Verify flat-root contract: the first entry must NOT be a bare directory
|
|
265
|
+
# (i.e. must not be "somedir/" with no slash before the trailing slash at the
|
|
266
|
+
# very start of the path component, e.g. "topdir/").
|
|
267
|
+
# Use a temp file to avoid pipefail from SIGPIPE when piping tar -t to head.
|
|
268
|
+
local _first_member_file
|
|
269
|
+
_first_member_file="$(mktemp)"
|
|
270
|
+
tar -tzf "$tarball" > "$_first_member_file" 2>/dev/null
|
|
271
|
+
local _tar_list_rc=$?
|
|
272
|
+
local first_member
|
|
273
|
+
first_member="$(head -1 "$_first_member_file")"
|
|
274
|
+
rm -f "$_first_member_file"
|
|
275
|
+
|
|
276
|
+
if [[ "$_tar_list_rc" -ne 0 && -z "$first_member" ]]; then
|
|
277
|
+
echo "ERROR: aid-install-core: failed to list tarball contents: ${tarball}" >&2
|
|
278
|
+
return 1
|
|
279
|
+
fi
|
|
280
|
+
|
|
281
|
+
# A wrapping dir would look like "topdir/" (a single path component ending with /).
|
|
282
|
+
# Pattern: starts with optional "./" then one path component (no inner slashes) then "/"
|
|
283
|
+
# at the end of the string (meaning the entire entry IS just "topdir/").
|
|
284
|
+
# "./topdir/" counts as a wrapping dir; "./.claude/file.md" does not.
|
|
285
|
+
local _stripped="${first_member#./}"
|
|
286
|
+
if [[ "$_stripped" =~ ^[^/]+/$ ]]; then
|
|
287
|
+
echo "ERROR: aid-install-core: tarball has a wrapping top-level directory ('${first_member}') - expected flat-root per feature-002 S2.3 contract: ${tarball}" >&2
|
|
288
|
+
return 1
|
|
289
|
+
fi
|
|
290
|
+
|
|
291
|
+
# Flat tarball (expected feature-002 layout).
|
|
292
|
+
tar -xzf "$tarball" -C "$dest_dir" || {
|
|
293
|
+
echo "ERROR: aid-install-core: failed to extract ${tarball}" >&2
|
|
294
|
+
return 1
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
# ---------------------------------------------------------------------------
|
|
299
|
+
# Copy semantics
|
|
300
|
+
# ---------------------------------------------------------------------------
|
|
301
|
+
|
|
302
|
+
# copy_file <src> <dst> [force]
|
|
303
|
+
# Returns 0 always (errors are logged; protect-on-diff is handled in install_tool).
|
|
304
|
+
# Prints per-file lines only when AID_VERBOSE=1.
|
|
305
|
+
# Increments counters: _COPY_COUNT_COPIED, _COPY_COUNT_UPTODATE, _COPY_COUNT_UPDATED,
|
|
306
|
+
# _COPY_COUNT_SKIPPED (caller initialises before iterating; install_tool reads them).
|
|
307
|
+
# This function handles NON-root-agent files only.
|
|
308
|
+
# Root agent files go through _copy_root_agent_file in install_tool.
|
|
309
|
+
copy_file() {
|
|
310
|
+
local src="$1" dst="$2" force="${3:-0}"
|
|
311
|
+
local dst_dir
|
|
312
|
+
dst_dir="$(dirname "$dst")"
|
|
313
|
+
mkdir -p "$dst_dir"
|
|
314
|
+
|
|
315
|
+
if [[ ! -e "$dst" ]]; then
|
|
316
|
+
cp "$src" "$dst"
|
|
317
|
+
_COPY_COUNT_COPIED=$((_COPY_COUNT_COPIED + 1))
|
|
318
|
+
[[ "${AID_VERBOSE:-0}" -eq 1 ]] && echo "Copied: ${dst}"
|
|
319
|
+
return 0
|
|
320
|
+
fi
|
|
321
|
+
|
|
322
|
+
if cmp -s "$src" "$dst"; then
|
|
323
|
+
_COPY_COUNT_UPTODATE=$((_COPY_COUNT_UPTODATE + 1))
|
|
324
|
+
[[ "${AID_VERBOSE:-0}" -eq 1 ]] && echo "Up to date: ${dst}"
|
|
325
|
+
return 0
|
|
326
|
+
fi
|
|
327
|
+
|
|
328
|
+
# File exists and differs.
|
|
329
|
+
if [[ "$force" -eq 1 ]]; then
|
|
330
|
+
cp "$src" "$dst"
|
|
331
|
+
_COPY_COUNT_UPDATED=$((_COPY_COUNT_UPDATED + 1))
|
|
332
|
+
[[ "${AID_VERBOSE:-0}" -eq 1 ]] && echo "Updated: ${dst}"
|
|
333
|
+
else
|
|
334
|
+
_COPY_COUNT_SKIPPED=$((_COPY_COUNT_SKIPPED + 1))
|
|
335
|
+
[[ "${AID_VERBOSE:-0}" -eq 1 ]] && echo "Skipped (differs; use --force): ${dst}"
|
|
336
|
+
fi
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
# copy_dir <src_dir> <dst_dir> [force]
|
|
340
|
+
# Recursively copies a directory tree, file by file (preserving empty dirs).
|
|
341
|
+
# Root agent files in the source dir are skipped - caller handles them.
|
|
342
|
+
copy_dir() {
|
|
343
|
+
local src="$1" dst="$2" force="${3:-0}"
|
|
344
|
+
|
|
345
|
+
# Create directory structure first.
|
|
346
|
+
while IFS= read -r -d '' dir; do
|
|
347
|
+
local rel="${dir#${src}/}"
|
|
348
|
+
mkdir -p "${dst}/${rel}"
|
|
349
|
+
done < <(find "$src" -mindepth 1 -type d -print0 2>/dev/null)
|
|
350
|
+
|
|
351
|
+
# Copy files.
|
|
352
|
+
while IFS= read -r -d '' file; do
|
|
353
|
+
local rel="${file#${src}/}"
|
|
354
|
+
copy_file "$file" "${dst}/${rel}" "$force"
|
|
355
|
+
done < <(find "$src" -type f -print0 2>/dev/null | sort -z)
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
# ---------------------------------------------------------------------------
|
|
359
|
+
# Protect-on-diff (FR11) for root agent files
|
|
360
|
+
# ---------------------------------------------------------------------------
|
|
361
|
+
|
|
362
|
+
# _copy_root_agent_file <src> <dst> <tool> <force> <manifest>
|
|
363
|
+
# Implements the FR11 algorithm.
|
|
364
|
+
# Returns:
|
|
365
|
+
# 0 - success (copied/up-to-date/updated/forced)
|
|
366
|
+
# 5 - protect-on-diff blocked (written .aid-new instead)
|
|
367
|
+
#
|
|
368
|
+
# Callers accumulate path/sha256 data into arrays and pass them to manifest_write.
|
|
369
|
+
# This function prints FR11 messages to stdout (progress) and stderr (warnings).
|
|
370
|
+
# It writes the status into _CORE_ROOT_AGENT_STATUS (caller reads this var).
|
|
371
|
+
_copy_root_agent_file() {
|
|
372
|
+
local src="$1" dst="$2" tool="$3" force="${4:-0}" manifest="${5:-}"
|
|
373
|
+
|
|
374
|
+
_CORE_ROOT_AGENT_STATUS="owned"
|
|
375
|
+
local inc_sha
|
|
376
|
+
inc_sha="$(sha256_file "$src")"
|
|
377
|
+
|
|
378
|
+
if [[ ! -e "$dst" ]]; then
|
|
379
|
+
# Step 2: Destination absent -> copy.
|
|
380
|
+
mkdir -p "$(dirname "$dst")"
|
|
381
|
+
cp "$src" "$dst"
|
|
382
|
+
_COPY_COUNT_COPIED=$((_COPY_COUNT_COPIED + 1))
|
|
383
|
+
[[ "${AID_VERBOSE:-0}" -eq 1 ]] && echo "Copied: ${dst}"
|
|
384
|
+
_CORE_ROOT_AGENT_STATUS="owned"
|
|
385
|
+
return 0
|
|
386
|
+
fi
|
|
387
|
+
|
|
388
|
+
local disk_sha
|
|
389
|
+
disk_sha="$(sha256_file "$dst")"
|
|
390
|
+
|
|
391
|
+
if [[ "$disk_sha" == "$inc_sha" ]]; then
|
|
392
|
+
# Step 3: Identical -> up to date.
|
|
393
|
+
_COPY_COUNT_UPTODATE=$((_COPY_COUNT_UPTODATE + 1))
|
|
394
|
+
[[ "${AID_VERBOSE:-0}" -eq 1 ]] && echo "Up to date: ${dst}"
|
|
395
|
+
_CORE_ROOT_AGENT_STATUS="owned"
|
|
396
|
+
return 0
|
|
397
|
+
fi
|
|
398
|
+
|
|
399
|
+
# Check manifest for AID-owned sha.
|
|
400
|
+
local recorded_sha=""
|
|
401
|
+
if [[ -n "$manifest" && -f "$manifest" ]]; then
|
|
402
|
+
recorded_sha="$(manifest_read_root_agent "$manifest" "$tool" "$(basename "$dst")")"
|
|
403
|
+
fi
|
|
404
|
+
|
|
405
|
+
if [[ -n "$recorded_sha" && "$disk_sha" == "$recorded_sha" ]]; then
|
|
406
|
+
# Step 4: AID owns it -> overwrite.
|
|
407
|
+
cp "$src" "$dst"
|
|
408
|
+
_COPY_COUNT_UPDATED=$((_COPY_COUNT_UPDATED + 1))
|
|
409
|
+
[[ "${AID_VERBOSE:-0}" -eq 1 ]] && echo "Updated: ${dst}"
|
|
410
|
+
_CORE_ROOT_AGENT_STATUS="owned"
|
|
411
|
+
return 0
|
|
412
|
+
fi
|
|
413
|
+
|
|
414
|
+
# Step 5: Someone else owns it.
|
|
415
|
+
if [[ "$force" -eq 1 ]]; then
|
|
416
|
+
cp "$src" "$dst"
|
|
417
|
+
_COPY_COUNT_UPDATED=$((_COPY_COUNT_UPDATED + 1))
|
|
418
|
+
[[ "${AID_VERBOSE:-0}" -eq 1 ]] && echo "Updated: ${dst} (forced over existing)"
|
|
419
|
+
_CORE_ROOT_AGENT_STATUS="owned"
|
|
420
|
+
return 0
|
|
421
|
+
fi
|
|
422
|
+
|
|
423
|
+
# Without --force: write .aid-new.
|
|
424
|
+
cp "$src" "${dst}.aid-new"
|
|
425
|
+
# WARN always shows regardless of AID_VERBOSE.
|
|
426
|
+
echo "WARN: ${dst} exists and was not written by AID; wrote incoming version to ${dst}.aid-new - review and merge, or re-run with --force to overwrite" >&2
|
|
427
|
+
_CORE_ROOT_AGENT_STATUS="pending-merge"
|
|
428
|
+
return 5
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
# ---------------------------------------------------------------------------
|
|
432
|
+
# Manifest - pure-Bash reader (no jq/python required)
|
|
433
|
+
# ---------------------------------------------------------------------------
|
|
434
|
+
#
|
|
435
|
+
# The manifest has this shape (2-space indent, \n newlines):
|
|
436
|
+
# {
|
|
437
|
+
# "manifest_version": 1,
|
|
438
|
+
# "aid_version": "0.7.0",
|
|
439
|
+
# "installed_at": "...",
|
|
440
|
+
# "tools": {
|
|
441
|
+
# "claude-code": {
|
|
442
|
+
# "version": "0.7.0",
|
|
443
|
+
# "installed_at": "...",
|
|
444
|
+
# "paths": ["..."],
|
|
445
|
+
# "root_agent_files": [{"path": "...", "sha256": "...", "status": "owned"}]
|
|
446
|
+
# }
|
|
447
|
+
# }
|
|
448
|
+
# }
|
|
449
|
+
#
|
|
450
|
+
# Readers use grep/sed/awk to extract what they need - sufficient because the
|
|
451
|
+
# schema is flat enough.
|
|
452
|
+
|
|
453
|
+
# manifest_read_tool_paths <manifest> <tool>
|
|
454
|
+
# Prints one path per line (from the "paths" array of the named tool section).
|
|
455
|
+
manifest_read_tool_paths() {
|
|
456
|
+
local manifest="$1" tool="$2"
|
|
457
|
+
[[ -f "$manifest" ]] || return 0
|
|
458
|
+
# Fast path: python3
|
|
459
|
+
if command -v python3 >/dev/null 2>&1; then
|
|
460
|
+
python3 - "$manifest" "$tool" <<'PY'
|
|
461
|
+
import json, sys
|
|
462
|
+
try:
|
|
463
|
+
data = json.load(open(sys.argv[1]))
|
|
464
|
+
for p in data.get("tools", {}).get(sys.argv[2], {}).get("paths", []):
|
|
465
|
+
print(p)
|
|
466
|
+
except Exception:
|
|
467
|
+
pass
|
|
468
|
+
PY
|
|
469
|
+
return
|
|
470
|
+
fi
|
|
471
|
+
# Pure-Bash fallback: extract section between "tool": { ... } and next top-level "tool":
|
|
472
|
+
# Use awk to capture paths array lines for the target tool.
|
|
473
|
+
awk -v tool="$tool" '
|
|
474
|
+
BEGIN { in_tool=0; in_paths=0 }
|
|
475
|
+
/"'"${tool}"'"[[:space:]]*:/ { in_tool=1; next }
|
|
476
|
+
in_tool && /"paths"[[:space:]]*:/ { in_paths=1; next }
|
|
477
|
+
in_tool && in_paths && /\]/ { exit }
|
|
478
|
+
in_tool && in_paths && /"[^"]*"/ {
|
|
479
|
+
# Extract the quoted string.
|
|
480
|
+
s=$0; gsub(/^[^"]*"/, "", s); gsub(/".*/, "", s)
|
|
481
|
+
if (s != "") print s
|
|
482
|
+
}
|
|
483
|
+
' "$manifest"
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
# manifest_read_tool_version <manifest> <tool>
|
|
487
|
+
# Prints the version string for the named tool.
|
|
488
|
+
manifest_read_tool_version() {
|
|
489
|
+
local manifest="$1" tool="$2"
|
|
490
|
+
[[ -f "$manifest" ]] || return 0
|
|
491
|
+
if command -v python3 >/dev/null 2>&1; then
|
|
492
|
+
python3 - "$manifest" "$tool" <<'PY'
|
|
493
|
+
import json, sys
|
|
494
|
+
try:
|
|
495
|
+
data = json.load(open(sys.argv[1]))
|
|
496
|
+
v = data.get("tools", {}).get(sys.argv[2], {}).get("version", "")
|
|
497
|
+
if v: print(v)
|
|
498
|
+
except Exception:
|
|
499
|
+
pass
|
|
500
|
+
PY
|
|
501
|
+
return
|
|
502
|
+
fi
|
|
503
|
+
# Pure-Bash: find version line inside the tool block.
|
|
504
|
+
awk -v tool="$tool" '
|
|
505
|
+
BEGIN { in_tool=0; depth=0 }
|
|
506
|
+
/"'"${tool}"'"[[:space:]]*:/ { in_tool=1; depth=0; next }
|
|
507
|
+
in_tool && /\{/ { depth++ }
|
|
508
|
+
in_tool && /\}/ { depth--; if (depth<0) exit }
|
|
509
|
+
in_tool && depth==1 && /"version"/ {
|
|
510
|
+
s=$0; gsub(/.*"version"[^:]*:[^"]*"/, "", s); gsub(/".*/, "", s)
|
|
511
|
+
print s; exit
|
|
512
|
+
}
|
|
513
|
+
' "$manifest"
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
# manifest_read_root_agent <manifest> <tool> <filename>
|
|
517
|
+
# Prints the sha256 for the root agent file entry (empty if not present).
|
|
518
|
+
manifest_read_root_agent() {
|
|
519
|
+
local manifest="$1" tool="$2" fname="$3"
|
|
520
|
+
[[ -f "$manifest" ]] || return 0
|
|
521
|
+
if command -v python3 >/dev/null 2>&1; then
|
|
522
|
+
python3 - "$manifest" "$tool" "$fname" <<'PY'
|
|
523
|
+
import json, sys
|
|
524
|
+
try:
|
|
525
|
+
data = json.load(open(sys.argv[1]))
|
|
526
|
+
for e in data.get("tools", {}).get(sys.argv[2], {}).get("root_agent_files", []):
|
|
527
|
+
if e.get("path") == sys.argv[3]:
|
|
528
|
+
print(e.get("sha256", ""))
|
|
529
|
+
break
|
|
530
|
+
except Exception:
|
|
531
|
+
pass
|
|
532
|
+
PY
|
|
533
|
+
return
|
|
534
|
+
fi
|
|
535
|
+
# Pure-Bash: find root_agent_files section for tool, look for fname.
|
|
536
|
+
awk -v tool="$tool" -v fname="$fname" '
|
|
537
|
+
BEGIN { in_tool=0; in_raf=0; in_entry=0; found_path=0 }
|
|
538
|
+
/"'"${tool}"'"[[:space:]]*:/ { in_tool=1 }
|
|
539
|
+
in_tool && /"root_agent_files"/ { in_raf=1 }
|
|
540
|
+
in_raf && /\{/ { in_entry=1; found_path=0 }
|
|
541
|
+
in_raf && in_entry && /"path"/ {
|
|
542
|
+
s=$0; gsub(/.*"path"[^:]*:[^"]*"/, "", s); gsub(/".*/, "", s)
|
|
543
|
+
if (s == fname) found_path=1
|
|
544
|
+
}
|
|
545
|
+
in_raf && in_entry && found_path && /"sha256"/ {
|
|
546
|
+
s=$0; gsub(/.*"sha256"[^:]*:[^"]*"/, "", s); gsub(/".*/, "", s)
|
|
547
|
+
print s; exit
|
|
548
|
+
}
|
|
549
|
+
in_raf && /\]/ { exit }
|
|
550
|
+
' "$manifest"
|
|
551
|
+
}
|
|
552
|
+
|
|
553
|
+
# manifest_read_root_agent_status <manifest> <tool> <filename>
|
|
554
|
+
# Prints the status field ("owned" or "pending-merge") for the root agent entry.
|
|
555
|
+
manifest_read_root_agent_status() {
|
|
556
|
+
local manifest="$1" tool="$2" fname="$3"
|
|
557
|
+
[[ -f "$manifest" ]] || return 0
|
|
558
|
+
if command -v python3 >/dev/null 2>&1; then
|
|
559
|
+
python3 - "$manifest" "$tool" "$fname" <<'PY'
|
|
560
|
+
import json, sys
|
|
561
|
+
try:
|
|
562
|
+
data = json.load(open(sys.argv[1]))
|
|
563
|
+
for e in data.get("tools", {}).get(sys.argv[2], {}).get("root_agent_files", []):
|
|
564
|
+
if e.get("path") == sys.argv[3]:
|
|
565
|
+
print(e.get("status", "owned"))
|
|
566
|
+
break
|
|
567
|
+
except Exception:
|
|
568
|
+
pass
|
|
569
|
+
PY
|
|
570
|
+
return
|
|
571
|
+
fi
|
|
572
|
+
awk -v tool="$tool" -v fname="$fname" '
|
|
573
|
+
BEGIN { in_tool=0; in_raf=0; in_entry=0; found_path=0 }
|
|
574
|
+
/"'"${tool}"'"[[:space:]]*:/ { in_tool=1 }
|
|
575
|
+
in_tool && /"root_agent_files"/ { in_raf=1 }
|
|
576
|
+
in_raf && /\{/ { in_entry=1; found_path=0 }
|
|
577
|
+
in_raf && in_entry && /"path"/ {
|
|
578
|
+
s=$0; gsub(/.*"path"[^:]*:[^"]*"/, "", s); gsub(/".*/, "", s)
|
|
579
|
+
if (s == fname) found_path=1
|
|
580
|
+
}
|
|
581
|
+
in_raf && in_entry && found_path && /"status"/ {
|
|
582
|
+
s=$0; gsub(/.*"status"[^:]*:[^"]*"/, "", s); gsub(/".*/, "", s)
|
|
583
|
+
print s; exit
|
|
584
|
+
}
|
|
585
|
+
in_raf && /\]/ { exit }
|
|
586
|
+
' "$manifest"
|
|
587
|
+
}
|
|
588
|
+
|
|
589
|
+
# ---------------------------------------------------------------------------
|
|
590
|
+
# Manifest writer (pure Bash + python3 fast-path)
|
|
591
|
+
# ---------------------------------------------------------------------------
|
|
592
|
+
|
|
593
|
+
# manifest_write <manifest_path> <tool> <version> <paths_varname> <root_entries_varname>
|
|
594
|
+
#
|
|
595
|
+
# <paths_varname> - name of a Bash array variable holding relative POSIX paths.
|
|
596
|
+
# <root_entries_varname> - name of a Bash array variable holding entries, each formatted as
|
|
597
|
+
# "path|sha256|status" (pipe-delimited).
|
|
598
|
+
#
|
|
599
|
+
# Reads the existing manifest (if any), merges the tool's entry, writes back atomically
|
|
600
|
+
# via a temp file. Creates <target>/.aid/ as needed.
|
|
601
|
+
manifest_write() {
|
|
602
|
+
local manifest="$1" tool="$2" version="$3"
|
|
603
|
+
local paths_var="$4" # indirect reference to array
|
|
604
|
+
local root_var="$5" # indirect reference to array
|
|
605
|
+
|
|
606
|
+
# Dereference array variables (Bash 4.3+ nameref or indirect via eval).
|
|
607
|
+
local -a paths_arr=()
|
|
608
|
+
local -a root_arr=()
|
|
609
|
+
eval "paths_arr=(\"\${${paths_var}[@]}\")"
|
|
610
|
+
eval "root_arr=(\"\${${root_var}[@]}\")"
|
|
611
|
+
|
|
612
|
+
local manifest_dir
|
|
613
|
+
manifest_dir="$(dirname "$manifest")"
|
|
614
|
+
mkdir -p "$manifest_dir"
|
|
615
|
+
|
|
616
|
+
local now
|
|
617
|
+
now="$(date -u +%Y-%m-%dT%H:%M:%SZ)"
|
|
618
|
+
|
|
619
|
+
if command -v python3 >/dev/null 2>&1; then
|
|
620
|
+
_manifest_write_python "$manifest" "$tool" "$version" "$now" "${paths_arr[@]+"${paths_arr[@]}"}" -- "${root_arr[@]+"${root_arr[@]}"}"
|
|
621
|
+
else
|
|
622
|
+
_manifest_write_bash "$manifest" "$tool" "$version" "$now" "${paths_arr[@]+"${paths_arr[@]}"}" -- "${root_arr[@]+"${root_arr[@]}"}"
|
|
623
|
+
fi
|
|
624
|
+
}
|
|
625
|
+
|
|
626
|
+
# _manifest_write_python - write manifest via python3 (fast path).
|
|
627
|
+
# Signature: <manifest> <tool> <version> <now> [paths...] -- [root_entries...]
|
|
628
|
+
_manifest_write_python() {
|
|
629
|
+
local manifest="$1" tool="$2" version="$3" now="$4"
|
|
630
|
+
shift 4
|
|
631
|
+
|
|
632
|
+
local -a paths_arr=()
|
|
633
|
+
local -a root_arr=()
|
|
634
|
+
local past_sep=0
|
|
635
|
+
for arg in "$@"; do
|
|
636
|
+
if [[ "$arg" == "--" ]]; then
|
|
637
|
+
past_sep=1
|
|
638
|
+
continue
|
|
639
|
+
fi
|
|
640
|
+
if [[ "$past_sep" -eq 0 ]]; then
|
|
641
|
+
paths_arr+=("$arg")
|
|
642
|
+
else
|
|
643
|
+
root_arr+=("$arg")
|
|
644
|
+
fi
|
|
645
|
+
done
|
|
646
|
+
|
|
647
|
+
python3 - "$manifest" "$tool" "$version" "$now" \
|
|
648
|
+
"$(printf '%s\n' "${paths_arr[@]+"${paths_arr[@]}"}")" \
|
|
649
|
+
"$(printf '%s\n' "${root_arr[@]+"${root_arr[@]}"}")" <<'PY'
|
|
650
|
+
import json, sys, os, tempfile
|
|
651
|
+
|
|
652
|
+
manifest_path = sys.argv[1]
|
|
653
|
+
tool = sys.argv[2]
|
|
654
|
+
version = sys.argv[3]
|
|
655
|
+
now = sys.argv[4]
|
|
656
|
+
paths_raw = sys.argv[5]
|
|
657
|
+
roots_raw = sys.argv[6]
|
|
658
|
+
|
|
659
|
+
paths = [p for p in paths_raw.splitlines() if p]
|
|
660
|
+
roots = []
|
|
661
|
+
for line in roots_raw.splitlines():
|
|
662
|
+
if not line:
|
|
663
|
+
continue
|
|
664
|
+
parts = line.split("|", 2)
|
|
665
|
+
entry = {"path": parts[0], "sha256": parts[1] if len(parts) > 1 else "",
|
|
666
|
+
"status": parts[2] if len(parts) > 2 else "owned"}
|
|
667
|
+
roots.append(entry)
|
|
668
|
+
|
|
669
|
+
# Load existing manifest.
|
|
670
|
+
data = {}
|
|
671
|
+
if os.path.isfile(manifest_path):
|
|
672
|
+
try:
|
|
673
|
+
with open(manifest_path) as f:
|
|
674
|
+
data = json.load(f)
|
|
675
|
+
except Exception:
|
|
676
|
+
data = {}
|
|
677
|
+
|
|
678
|
+
if not isinstance(data.get("tools"), dict):
|
|
679
|
+
data["tools"] = {}
|
|
680
|
+
|
|
681
|
+
# Merge: preserve existing installed_at for the tool if present.
|
|
682
|
+
existing_tool = data["tools"].get(tool, {})
|
|
683
|
+
tool_installed_at = existing_tool.get("installed_at", now)
|
|
684
|
+
|
|
685
|
+
# De-duplicate paths (union).
|
|
686
|
+
existing_paths = existing_tool.get("paths", [])
|
|
687
|
+
merged_paths = list(dict.fromkeys(existing_paths + paths))
|
|
688
|
+
|
|
689
|
+
# Merge root_agent_files: update or add per path.
|
|
690
|
+
existing_raf = {e["path"]: e for e in existing_tool.get("root_agent_files", [])}
|
|
691
|
+
for e in roots:
|
|
692
|
+
existing_raf[e["path"]] = e
|
|
693
|
+
merged_raf = list(existing_raf.values())
|
|
694
|
+
|
|
695
|
+
data["tools"][tool] = {
|
|
696
|
+
"version": version,
|
|
697
|
+
"installed_at": tool_installed_at,
|
|
698
|
+
"paths": merged_paths,
|
|
699
|
+
"root_agent_files": merged_raf,
|
|
700
|
+
}
|
|
701
|
+
|
|
702
|
+
# Build output with canonical key order.
|
|
703
|
+
top_installed_at = data.get("installed_at", now)
|
|
704
|
+
output = {
|
|
705
|
+
"manifest_version": 1,
|
|
706
|
+
"aid_version": version,
|
|
707
|
+
"installed_at": top_installed_at,
|
|
708
|
+
"tools": data["tools"],
|
|
709
|
+
}
|
|
710
|
+
data = output
|
|
711
|
+
|
|
712
|
+
# Write atomically.
|
|
713
|
+
manifest_dir = os.path.dirname(manifest_path)
|
|
714
|
+
fd, tmp = tempfile.mkstemp(dir=manifest_dir, suffix=".tmp")
|
|
715
|
+
try:
|
|
716
|
+
with os.fdopen(fd, "w", newline="\n") as f:
|
|
717
|
+
json.dump(data, f, indent=2)
|
|
718
|
+
f.write("\n")
|
|
719
|
+
os.replace(tmp, manifest_path)
|
|
720
|
+
except Exception as e:
|
|
721
|
+
os.unlink(tmp)
|
|
722
|
+
print(f"ERROR: aid-install-core: manifest write failed: {e}", file=sys.stderr)
|
|
723
|
+
sys.exit(1)
|
|
724
|
+
PY
|
|
725
|
+
}
|
|
726
|
+
|
|
727
|
+
# _manifest_write_bash - pure-Bash fallback manifest writer.
|
|
728
|
+
# Signature same as _manifest_write_python.
|
|
729
|
+
_manifest_write_bash() {
|
|
730
|
+
local manifest="$1" tool="$2" version="$3" now="$4"
|
|
731
|
+
shift 4
|
|
732
|
+
|
|
733
|
+
local -a paths_arr=()
|
|
734
|
+
local -a root_arr=()
|
|
735
|
+
local past_sep=0
|
|
736
|
+
for arg in "$@"; do
|
|
737
|
+
if [[ "$arg" == "--" ]]; then
|
|
738
|
+
past_sep=1
|
|
739
|
+
continue
|
|
740
|
+
fi
|
|
741
|
+
if [[ "$past_sep" -eq 0 ]]; then
|
|
742
|
+
paths_arr+=("$arg")
|
|
743
|
+
else
|
|
744
|
+
root_arr+=("$arg")
|
|
745
|
+
fi
|
|
746
|
+
done
|
|
747
|
+
|
|
748
|
+
local manifest_dir
|
|
749
|
+
manifest_dir="$(dirname "$manifest")"
|
|
750
|
+
|
|
751
|
+
# Load existing manifest fields we need to preserve.
|
|
752
|
+
local top_installed_at="$now"
|
|
753
|
+
local tool_installed_at="$now"
|
|
754
|
+
local -a existing_paths=()
|
|
755
|
+
local existing_aid_version=""
|
|
756
|
+
|
|
757
|
+
if [[ -f "$manifest" ]]; then
|
|
758
|
+
top_installed_at="$(grep '"installed_at"' "$manifest" | head -1 | sed 's/.*"installed_at"[^:]*:[^"]*"\([^"]*\)".*/\1/')"
|
|
759
|
+
[[ -z "$top_installed_at" ]] && top_installed_at="$now"
|
|
760
|
+
|
|
761
|
+
# Collect existing paths for this tool.
|
|
762
|
+
while IFS= read -r p; do
|
|
763
|
+
[[ -n "$p" ]] && existing_paths+=("$p")
|
|
764
|
+
done < <(manifest_read_tool_paths "$manifest" "$tool")
|
|
765
|
+
|
|
766
|
+
local t_inst
|
|
767
|
+
t_inst="$(manifest_read_tool_version "$manifest" "$tool")"
|
|
768
|
+
# Read tool-specific installed_at (reuse existing or now).
|
|
769
|
+
tool_installed_at="$(awk -v tool="$tool" '
|
|
770
|
+
BEGIN{in_tool=0}
|
|
771
|
+
/"'"${tool}"'"/{in_tool=1}
|
|
772
|
+
in_tool && /"installed_at"/{
|
|
773
|
+
s=$0; gsub(/.*"installed_at"[^:]*:[^"]*"/,"",s); gsub(/".*$/,"",s); print s; exit
|
|
774
|
+
}' "$manifest")"
|
|
775
|
+
[[ -z "$tool_installed_at" ]] && tool_installed_at="$now"
|
|
776
|
+
fi
|
|
777
|
+
|
|
778
|
+
# Merge paths (de-duplicate).
|
|
779
|
+
local -a merged_paths=()
|
|
780
|
+
declare -A _seen_paths=()
|
|
781
|
+
for p in "${existing_paths[@]+"${existing_paths[@]}"}" "${paths_arr[@]+"${paths_arr[@]}"}"; do
|
|
782
|
+
if [[ -z "${_seen_paths[$p]+x}" ]]; then
|
|
783
|
+
_seen_paths[$p]=1
|
|
784
|
+
merged_paths+=("$p")
|
|
785
|
+
fi
|
|
786
|
+
done
|
|
787
|
+
|
|
788
|
+
# Assemble paths JSON array.
|
|
789
|
+
local paths_json="["
|
|
790
|
+
local first=1
|
|
791
|
+
for p in "${merged_paths[@]+"${merged_paths[@]}"}"; do
|
|
792
|
+
[[ "$first" -eq 0 ]] && paths_json+=","
|
|
793
|
+
paths_json+=$'\n "'
|
|
794
|
+
paths_json+="$p"
|
|
795
|
+
paths_json+='"'
|
|
796
|
+
first=0
|
|
797
|
+
done
|
|
798
|
+
if [[ "${#merged_paths[@]}" -gt 0 ]]; then
|
|
799
|
+
paths_json+=$'\n '
|
|
800
|
+
fi
|
|
801
|
+
paths_json+="]"
|
|
802
|
+
|
|
803
|
+
# Assemble root_agent_files JSON array.
|
|
804
|
+
# Merge with existing entries.
|
|
805
|
+
local -a all_root=()
|
|
806
|
+
if [[ -f "$manifest" ]]; then
|
|
807
|
+
# Read existing root_agent_files for this tool from manifest.
|
|
808
|
+
local _existing_raf_paths=()
|
|
809
|
+
while IFS= read -r _raf_line; do
|
|
810
|
+
[[ -n "$_raf_line" ]] && _existing_raf_paths+=("$_raf_line")
|
|
811
|
+
done < <(awk -v tool="$tool" '
|
|
812
|
+
BEGIN{in_tool=0;in_raf=0;in_entry=0}
|
|
813
|
+
/"'"${tool}"'"[[:space:]]*:/{in_tool=1}
|
|
814
|
+
in_tool && /"root_agent_files"/{in_raf=1}
|
|
815
|
+
in_raf && /\{/{in_entry=1; cur_path=""; cur_sha=""; cur_status="owned"}
|
|
816
|
+
in_raf && in_entry && /"path"/{
|
|
817
|
+
s=$0; gsub(/.*"path"[^:]*:[^"]*"/,"",s); gsub(/".*$/,"",s); cur_path=s
|
|
818
|
+
}
|
|
819
|
+
in_raf && in_entry && /"sha256"/{
|
|
820
|
+
s=$0; gsub(/.*"sha256"[^:]*:[^"]*"/,"",s); gsub(/".*$/,"",s); cur_sha=s
|
|
821
|
+
}
|
|
822
|
+
in_raf && in_entry && /"status"/{
|
|
823
|
+
s=$0; gsub(/.*"status"[^:]*:[^"]*"/,"",s); gsub(/".*$/,"",s); cur_status=s
|
|
824
|
+
}
|
|
825
|
+
in_raf && in_entry && /\}/ {
|
|
826
|
+
if (cur_path!="") print cur_path "|" cur_sha "|" cur_status
|
|
827
|
+
in_entry=0
|
|
828
|
+
}
|
|
829
|
+
in_raf && /\]/{exit}
|
|
830
|
+
' "$manifest")
|
|
831
|
+
|
|
832
|
+
# Build map of existing entries.
|
|
833
|
+
declare -A _raf_map=()
|
|
834
|
+
for _entry in "${_existing_raf_paths[@]+"${_existing_raf_paths[@]}"}"; do
|
|
835
|
+
local _k="${_entry%%|*}"
|
|
836
|
+
_raf_map["$_k"]="$_entry"
|
|
837
|
+
done
|
|
838
|
+
# Override with incoming entries.
|
|
839
|
+
for _entry in "${root_arr[@]+"${root_arr[@]}"}"; do
|
|
840
|
+
local _k="${_entry%%|*}"
|
|
841
|
+
_raf_map["$_k"]="$_entry"
|
|
842
|
+
done
|
|
843
|
+
for _k in "${!_raf_map[@]}"; do
|
|
844
|
+
all_root+=("${_raf_map[$_k]}")
|
|
845
|
+
done
|
|
846
|
+
else
|
|
847
|
+
all_root=("${root_arr[@]+"${root_arr[@]}"}")
|
|
848
|
+
fi
|
|
849
|
+
|
|
850
|
+
local raf_json="["
|
|
851
|
+
local first=1
|
|
852
|
+
for entry in "${all_root[@]+"${all_root[@]}"}"; do
|
|
853
|
+
local rpath rsha rstatus
|
|
854
|
+
IFS='|' read -r rpath rsha rstatus <<< "$entry"
|
|
855
|
+
[[ "$first" -eq 0 ]] && raf_json+=","
|
|
856
|
+
raf_json+=$'\n '
|
|
857
|
+
raf_json+='{ "path": "'"${rpath}"'", "sha256": "'"${rsha}"'", "status": "'"${rstatus}"'" }'
|
|
858
|
+
first=0
|
|
859
|
+
done
|
|
860
|
+
if [[ "${#all_root[@]}" -gt 0 ]]; then
|
|
861
|
+
raf_json+=$'\n '
|
|
862
|
+
fi
|
|
863
|
+
raf_json+="]"
|
|
864
|
+
|
|
865
|
+
# Read all existing tools to preserve them.
|
|
866
|
+
local -a all_tool_ids=()
|
|
867
|
+
if [[ -f "$manifest" ]]; then
|
|
868
|
+
while IFS= read -r tid; do
|
|
869
|
+
[[ -n "$tid" && "$tid" != "$tool" ]] && all_tool_ids+=("$tid")
|
|
870
|
+
done < <(grep -o '"[a-z][a-zA-Z-]*"[[:space:]]*:' "$manifest" | \
|
|
871
|
+
grep -v 'manifest_version\|aid_version\|installed_at\|version\|paths\|root_agent_files\|sha256\|status\|path\|tools' | \
|
|
872
|
+
sed 's/"//g' | sed 's/[[:space:]]*://g')
|
|
873
|
+
fi
|
|
874
|
+
|
|
875
|
+
# Assemble complete manifest JSON.
|
|
876
|
+
local tmp_file
|
|
877
|
+
tmp_file="$(mktemp "${manifest_dir}/.manifest.tmp.XXXXXX")"
|
|
878
|
+
|
|
879
|
+
{
|
|
880
|
+
printf '{\n'
|
|
881
|
+
printf ' "manifest_version": 1,\n'
|
|
882
|
+
printf ' "aid_version": "%s",\n' "$version"
|
|
883
|
+
printf ' "installed_at": "%s",\n' "$top_installed_at"
|
|
884
|
+
printf ' "tools": {\n'
|
|
885
|
+
|
|
886
|
+
# Write other tools first (preserve existing).
|
|
887
|
+
local need_comma=0
|
|
888
|
+
for tid in "${all_tool_ids[@]+"${all_tool_ids[@]}"}"; do
|
|
889
|
+
# Re-serialize existing tool block.
|
|
890
|
+
if [[ "$need_comma" -eq 1 ]]; then printf ',\n'; fi
|
|
891
|
+
local t_ver t_iat
|
|
892
|
+
t_ver="$(manifest_read_tool_version "$manifest" "$tid")"
|
|
893
|
+
t_iat="$(awk -v t="$tid" 'BEGIN{in_t=0} /"'"${tid}"'"/{in_t=1} in_t && /"installed_at"/{s=$0; gsub(/.*"installed_at"[^:]*:[^"]*"/,"",s); gsub(/".*$/,"",s); print s; exit}' "$manifest")"
|
|
894
|
+
[[ -z "$t_iat" ]] && t_iat="$now"
|
|
895
|
+
printf ' "%s": {\n' "$tid"
|
|
896
|
+
printf ' "version": "%s",\n' "$t_ver"
|
|
897
|
+
printf ' "installed_at": "%s",\n' "$t_iat"
|
|
898
|
+
# Re-read and emit paths for existing tool.
|
|
899
|
+
local t_paths_json="["
|
|
900
|
+
local tp_first=1
|
|
901
|
+
while IFS= read -r tp; do
|
|
902
|
+
[[ -z "$tp" ]] && continue
|
|
903
|
+
[[ "$tp_first" -eq 0 ]] && t_paths_json+=","
|
|
904
|
+
t_paths_json+=$'\n "'
|
|
905
|
+
t_paths_json+="$tp"
|
|
906
|
+
t_paths_json+='"'
|
|
907
|
+
tp_first=0
|
|
908
|
+
done < <(manifest_read_tool_paths "$manifest" "$tid")
|
|
909
|
+
[[ "$tp_first" -eq 0 ]] && t_paths_json+=$'\n '
|
|
910
|
+
t_paths_json+="]"
|
|
911
|
+
printf ' "paths": %s,\n' "$t_paths_json"
|
|
912
|
+
# Re-read root_agent_files for existing tool using the awk RAF parser.
|
|
913
|
+
local t_raf_json="["
|
|
914
|
+
local tr_first=1
|
|
915
|
+
local -a _t_raf_lines=()
|
|
916
|
+
while IFS= read -r _t_raf_line; do
|
|
917
|
+
[[ -n "$_t_raf_line" ]] && _t_raf_lines+=("$_t_raf_line")
|
|
918
|
+
done < <(awk -v tool="$tid" '
|
|
919
|
+
BEGIN{in_tool=0;in_raf=0;in_entry=0}
|
|
920
|
+
/"'"${tid}"'"[[:space:]]*:/{in_tool=1}
|
|
921
|
+
in_tool && /"root_agent_files"/{in_raf=1}
|
|
922
|
+
in_raf && /\{/{in_entry=1; cur_path=""; cur_sha=""; cur_status="owned"}
|
|
923
|
+
in_raf && in_entry && /"path"/{
|
|
924
|
+
s=$0; gsub(/.*"path"[^:]*:[^"]*"/,"",s); gsub(/".*$/,"",s); cur_path=s
|
|
925
|
+
}
|
|
926
|
+
in_raf && in_entry && /"sha256"/{
|
|
927
|
+
s=$0; gsub(/.*"sha256"[^:]*:[^"]*"/,"",s); gsub(/".*$/,"",s); cur_sha=s
|
|
928
|
+
}
|
|
929
|
+
in_raf && in_entry && /"status"/{
|
|
930
|
+
s=$0; gsub(/.*"status"[^:]*:[^"]*"/,"",s); gsub(/".*$/,"",s); cur_status=s
|
|
931
|
+
}
|
|
932
|
+
in_raf && in_entry && /\}/ {
|
|
933
|
+
if (cur_path!="") print cur_path "|" cur_sha "|" cur_status
|
|
934
|
+
in_entry=0
|
|
935
|
+
}
|
|
936
|
+
in_raf && /\]/{exit}
|
|
937
|
+
' "$manifest")
|
|
938
|
+
for _t_entry in "${_t_raf_lines[@]+"${_t_raf_lines[@]}"}"; do
|
|
939
|
+
local _t_rpath _t_rsha _t_rstatus
|
|
940
|
+
IFS='|' read -r _t_rpath _t_rsha _t_rstatus <<< "$_t_entry"
|
|
941
|
+
[[ "$tr_first" -eq 0 ]] && t_raf_json+=","
|
|
942
|
+
t_raf_json+=$'\n '
|
|
943
|
+
t_raf_json+='{ "path": "'"${_t_rpath}"'", "sha256": "'"${_t_rsha}"'", "status": "'"${_t_rstatus}"'" }'
|
|
944
|
+
tr_first=0
|
|
945
|
+
done
|
|
946
|
+
if [[ "${#_t_raf_lines[@]}" -gt 0 ]]; then
|
|
947
|
+
t_raf_json+=$'\n '
|
|
948
|
+
fi
|
|
949
|
+
t_raf_json+="]"
|
|
950
|
+
printf ' "root_agent_files": %s\n' "$t_raf_json"
|
|
951
|
+
printf ' }'
|
|
952
|
+
need_comma=1
|
|
953
|
+
done
|
|
954
|
+
|
|
955
|
+
# Write the current tool.
|
|
956
|
+
if [[ "$need_comma" -eq 1 ]]; then printf ',\n'; fi
|
|
957
|
+
printf ' "%s": {\n' "$tool"
|
|
958
|
+
printf ' "version": "%s",\n' "$version"
|
|
959
|
+
printf ' "installed_at": "%s",\n' "$tool_installed_at"
|
|
960
|
+
printf ' "paths": %s,\n' "$paths_json"
|
|
961
|
+
printf ' "root_agent_files": %s\n' "$raf_json"
|
|
962
|
+
printf ' }\n'
|
|
963
|
+
|
|
964
|
+
printf ' }\n'
|
|
965
|
+
printf '}\n'
|
|
966
|
+
} > "$tmp_file"
|
|
967
|
+
|
|
968
|
+
mv "$tmp_file" "$manifest"
|
|
969
|
+
}
|
|
970
|
+
|
|
971
|
+
# manifest_remove_tool <manifest> <tool>
|
|
972
|
+
# Removes a tool's section from the manifest. If no tools remain, removes the manifest.
|
|
973
|
+
manifest_remove_tool() {
|
|
974
|
+
local manifest="$1" tool="$2"
|
|
975
|
+
[[ -f "$manifest" ]] || return 0
|
|
976
|
+
|
|
977
|
+
if command -v python3 >/dev/null 2>&1; then
|
|
978
|
+
python3 - "$manifest" "$tool" <<'PY'
|
|
979
|
+
import json, sys, os, tempfile
|
|
980
|
+
|
|
981
|
+
manifest_path = sys.argv[1]
|
|
982
|
+
tool = sys.argv[2]
|
|
983
|
+
|
|
984
|
+
try:
|
|
985
|
+
with open(manifest_path) as f:
|
|
986
|
+
data = json.load(f)
|
|
987
|
+
except Exception:
|
|
988
|
+
sys.exit(0)
|
|
989
|
+
|
|
990
|
+
tools = data.get("tools", {})
|
|
991
|
+
tools.pop(tool, None)
|
|
992
|
+
data["tools"] = tools
|
|
993
|
+
|
|
994
|
+
if not tools:
|
|
995
|
+
os.remove(manifest_path)
|
|
996
|
+
sys.exit(0)
|
|
997
|
+
|
|
998
|
+
manifest_dir = os.path.dirname(manifest_path)
|
|
999
|
+
fd, tmp = tempfile.mkstemp(dir=manifest_dir, suffix=".tmp")
|
|
1000
|
+
try:
|
|
1001
|
+
with os.fdopen(fd, "w", newline="\n") as f:
|
|
1002
|
+
json.dump(data, f, indent=2)
|
|
1003
|
+
f.write("\n")
|
|
1004
|
+
os.replace(tmp, manifest_path)
|
|
1005
|
+
except Exception as e:
|
|
1006
|
+
os.unlink(tmp)
|
|
1007
|
+
print(f"ERROR: manifest remove failed: {e}", file=sys.stderr)
|
|
1008
|
+
sys.exit(1)
|
|
1009
|
+
PY
|
|
1010
|
+
else
|
|
1011
|
+
# Pure-Bash: re-read all other tools and write a new manifest.
|
|
1012
|
+
# Collect remaining tool ids.
|
|
1013
|
+
local -a remaining=()
|
|
1014
|
+
while IFS= read -r tid; do
|
|
1015
|
+
[[ -n "$tid" && "$tid" != "$tool" ]] && remaining+=("$tid")
|
|
1016
|
+
done < <(awk '/"tools"/{found=1} found && /^ "[a-z]/{gsub(/[^a-zA-Z-]/,"",$1); print $1}' "$manifest" | sort -u)
|
|
1017
|
+
|
|
1018
|
+
if [[ "${#remaining[@]}" -eq 0 ]]; then
|
|
1019
|
+
rm -f "$manifest"
|
|
1020
|
+
return
|
|
1021
|
+
fi
|
|
1022
|
+
|
|
1023
|
+
# Rebuild the manifest with only remaining tools.
|
|
1024
|
+
local now
|
|
1025
|
+
now="$(date -u +%Y-%m-%dT%H:%M:%SZ)"
|
|
1026
|
+
local top_iat
|
|
1027
|
+
top_iat="$(grep '"installed_at"' "$manifest" | head -1 | sed 's/.*"installed_at"[^:]*:[^"]*"\([^"]*\)".*/\1/')"
|
|
1028
|
+
[[ -z "$top_iat" ]] && top_iat="$now"
|
|
1029
|
+
local last_ver
|
|
1030
|
+
last_ver="$(grep '"aid_version"' "$manifest" | head -1 | sed 's/.*"aid_version"[^:]*:[^"]*"\([^"]*\)".*/\1/')"
|
|
1031
|
+
[[ -z "$last_ver" ]] && last_ver="0.0.0"
|
|
1032
|
+
|
|
1033
|
+
local tmp_file
|
|
1034
|
+
tmp_file="$(mktemp "$(dirname "$manifest")/.manifest.tmp.XXXXXX")"
|
|
1035
|
+
{
|
|
1036
|
+
printf '{\n'
|
|
1037
|
+
printf ' "manifest_version": 1,\n'
|
|
1038
|
+
printf ' "aid_version": "%s",\n' "$last_ver"
|
|
1039
|
+
printf ' "installed_at": "%s",\n' "$top_iat"
|
|
1040
|
+
printf ' "tools": {\n'
|
|
1041
|
+
local need_comma=0
|
|
1042
|
+
for tid in "${remaining[@]}"; do
|
|
1043
|
+
[[ "$need_comma" -eq 1 ]] && printf ',\n'
|
|
1044
|
+
local t_ver t_iat
|
|
1045
|
+
t_ver="$(manifest_read_tool_version "$manifest" "$tid")"
|
|
1046
|
+
t_iat="$(awk -v t="$tid" 'BEGIN{in_t=0} /"'"${tid}"'"/{in_t=1} in_t && /"installed_at"/{s=$0; gsub(/.*"installed_at"[^:]*:[^"]*"/,"",s); gsub(/".*$/,"",s); print s; exit}' "$manifest")"
|
|
1047
|
+
[[ -z "$t_iat" ]] && t_iat="$now"
|
|
1048
|
+
printf ' "%s": {\n' "$tid"
|
|
1049
|
+
printf ' "version": "%s",\n' "$t_ver"
|
|
1050
|
+
printf ' "installed_at": "%s",\n' "$t_iat"
|
|
1051
|
+
# Re-read paths for this tool.
|
|
1052
|
+
local rm_paths_json="["
|
|
1053
|
+
local rm_tp_first=1
|
|
1054
|
+
while IFS= read -r rm_tp; do
|
|
1055
|
+
[[ -z "$rm_tp" ]] && continue
|
|
1056
|
+
[[ "$rm_tp_first" -eq 0 ]] && rm_paths_json+=","
|
|
1057
|
+
rm_paths_json+=$'\n "'
|
|
1058
|
+
rm_paths_json+="$rm_tp"
|
|
1059
|
+
rm_paths_json+='"'
|
|
1060
|
+
rm_tp_first=0
|
|
1061
|
+
done < <(manifest_read_tool_paths "$manifest" "$tid")
|
|
1062
|
+
[[ "$rm_tp_first" -eq 0 ]] && rm_paths_json+=$'\n '
|
|
1063
|
+
rm_paths_json+="]"
|
|
1064
|
+
printf ' "paths": %s,\n' "$rm_paths_json"
|
|
1065
|
+
# Re-read root_agent_files for this tool using the awk RAF parser.
|
|
1066
|
+
local rm_raf_json="["
|
|
1067
|
+
local rm_tr_first=1
|
|
1068
|
+
local -a _rm_raf_lines=()
|
|
1069
|
+
while IFS= read -r _rm_raf_line; do
|
|
1070
|
+
[[ -n "$_rm_raf_line" ]] && _rm_raf_lines+=("$_rm_raf_line")
|
|
1071
|
+
done < <(awk -v tool="$tid" '
|
|
1072
|
+
BEGIN{in_tool=0;in_raf=0;in_entry=0}
|
|
1073
|
+
/"'"${tid}"'"[[:space:]]*:/{in_tool=1}
|
|
1074
|
+
in_tool && /"root_agent_files"/{in_raf=1}
|
|
1075
|
+
in_raf && /\{/{in_entry=1; cur_path=""; cur_sha=""; cur_status="owned"}
|
|
1076
|
+
in_raf && in_entry && /"path"/{
|
|
1077
|
+
s=$0; gsub(/.*"path"[^:]*:[^"]*"/,"",s); gsub(/".*$/,"",s); cur_path=s
|
|
1078
|
+
}
|
|
1079
|
+
in_raf && in_entry && /"sha256"/{
|
|
1080
|
+
s=$0; gsub(/.*"sha256"[^:]*:[^"]*"/,"",s); gsub(/".*$/,"",s); cur_sha=s
|
|
1081
|
+
}
|
|
1082
|
+
in_raf && in_entry && /"status"/{
|
|
1083
|
+
s=$0; gsub(/.*"status"[^:]*:[^"]*"/,"",s); gsub(/".*$/,"",s); cur_status=s
|
|
1084
|
+
}
|
|
1085
|
+
in_raf && in_entry && /\}/ {
|
|
1086
|
+
if (cur_path!="") print cur_path "|" cur_sha "|" cur_status
|
|
1087
|
+
in_entry=0
|
|
1088
|
+
}
|
|
1089
|
+
in_raf && /\]/{exit}
|
|
1090
|
+
' "$manifest")
|
|
1091
|
+
for _rm_entry in "${_rm_raf_lines[@]+"${_rm_raf_lines[@]}"}"; do
|
|
1092
|
+
local _rm_rpath _rm_rsha _rm_rstatus
|
|
1093
|
+
IFS='|' read -r _rm_rpath _rm_rsha _rm_rstatus <<< "$_rm_entry"
|
|
1094
|
+
[[ "$rm_tr_first" -eq 0 ]] && rm_raf_json+=","
|
|
1095
|
+
rm_raf_json+=$'\n '
|
|
1096
|
+
rm_raf_json+='{ "path": "'"${_rm_rpath}"'", "sha256": "'"${_rm_rsha}"'", "status": "'"${_rm_rstatus}"'" }'
|
|
1097
|
+
rm_tr_first=0
|
|
1098
|
+
done
|
|
1099
|
+
if [[ "${#_rm_raf_lines[@]}" -gt 0 ]]; then
|
|
1100
|
+
rm_raf_json+=$'\n '
|
|
1101
|
+
fi
|
|
1102
|
+
rm_raf_json+="]"
|
|
1103
|
+
printf ' "root_agent_files": %s\n' "$rm_raf_json"
|
|
1104
|
+
printf ' }'
|
|
1105
|
+
need_comma=1
|
|
1106
|
+
done
|
|
1107
|
+
printf '\n }\n'
|
|
1108
|
+
printf '}\n'
|
|
1109
|
+
} > "$tmp_file"
|
|
1110
|
+
mv "$tmp_file" "$manifest"
|
|
1111
|
+
fi
|
|
1112
|
+
}
|
|
1113
|
+
|
|
1114
|
+
# manifest_list_tools <manifest>
|
|
1115
|
+
# Prints each installed tool id (one per line) from tools.<id> keys.
|
|
1116
|
+
# Exits 0 even when the manifest is absent (prints nothing).
|
|
1117
|
+
manifest_list_tools() {
|
|
1118
|
+
local manifest="$1"
|
|
1119
|
+
[[ -f "$manifest" ]] || return 0
|
|
1120
|
+
if command -v python3 >/dev/null 2>&1; then
|
|
1121
|
+
python3 - "$manifest" <<'PY'
|
|
1122
|
+
import json, sys
|
|
1123
|
+
try:
|
|
1124
|
+
data = json.load(open(sys.argv[1]))
|
|
1125
|
+
for t in data.get("tools", {}).keys():
|
|
1126
|
+
print(t)
|
|
1127
|
+
except Exception:
|
|
1128
|
+
pass
|
|
1129
|
+
PY
|
|
1130
|
+
return
|
|
1131
|
+
fi
|
|
1132
|
+
# Pure-Bash fallback: extract tool keys from the "tools" section.
|
|
1133
|
+
# Each tool entry looks like: "tool-name": { at 4-space indent.
|
|
1134
|
+
awk '
|
|
1135
|
+
BEGIN { in_tools=0; depth=0 }
|
|
1136
|
+
/"tools"[[:space:]]*:/ { in_tools=1; depth=0; next }
|
|
1137
|
+
in_tools && /\{/ { depth++ }
|
|
1138
|
+
in_tools && /\}/ { depth--; if (depth<=0) exit }
|
|
1139
|
+
in_tools && depth==1 && /^ "[a-z][a-zA-Z-]*"[[:space:]]*:/ {
|
|
1140
|
+
s=$0; gsub(/^[[:space:]]*"/, "", s); gsub(/".*/, "", s)
|
|
1141
|
+
if (s != "") print s
|
|
1142
|
+
}
|
|
1143
|
+
' "$manifest"
|
|
1144
|
+
}
|
|
1145
|
+
|
|
1146
|
+
# ---------------------------------------------------------------------------
|
|
1147
|
+
# Semver comparison helpers
|
|
1148
|
+
# ---------------------------------------------------------------------------
|
|
1149
|
+
|
|
1150
|
+
# _semver_lt <a> <b>
|
|
1151
|
+
# Returns 0 (true) when version a < version b using simple numeric major.minor.patch
|
|
1152
|
+
# comparison. Non-numeric segments are treated as 0. Returns 1 when a >= b.
|
|
1153
|
+
_semver_lt() {
|
|
1154
|
+
local a="$1" b="$2"
|
|
1155
|
+
local -a pa=() pb=()
|
|
1156
|
+
IFS='.' read -ra pa <<< "$a"
|
|
1157
|
+
IFS='.' read -ra pb <<< "$b"
|
|
1158
|
+
local i
|
|
1159
|
+
for i in 0 1 2; do
|
|
1160
|
+
local va="${pa[$i]:-0}" vb="${pb[$i]:-0}"
|
|
1161
|
+
# Strip non-numeric suffixes (e.g. "1-rc1" -> "1").
|
|
1162
|
+
va="${va%%[^0-9]*}"
|
|
1163
|
+
vb="${vb%%[^0-9]*}"
|
|
1164
|
+
[[ -z "$va" ]] && va=0
|
|
1165
|
+
[[ -z "$vb" ]] && vb=0
|
|
1166
|
+
if (( va < vb )); then return 0; fi
|
|
1167
|
+
if (( va > vb )); then return 1; fi
|
|
1168
|
+
done
|
|
1169
|
+
return 1 # equal -> not less than
|
|
1170
|
+
}
|
|
1171
|
+
|
|
1172
|
+
# ---------------------------------------------------------------------------
|
|
1173
|
+
# Shared tool-list renderer (used by both aid_status_body and aid_status)
|
|
1174
|
+
# ---------------------------------------------------------------------------
|
|
1175
|
+
|
|
1176
|
+
# _render_tools_block <manifest> <ref_version> <header_prefix>
|
|
1177
|
+
#
|
|
1178
|
+
# <manifest> - path to the .aid-manifest.json
|
|
1179
|
+
# <ref_version> - the CLI's own version (from $AID_HOME/VERSION)
|
|
1180
|
+
# <header_prefix> - text before the "- all at vX:" collapse suffix (e.g.
|
|
1181
|
+
# "Installed tools (in /path)" or "Installed tools")
|
|
1182
|
+
#
|
|
1183
|
+
# Outputs the complete tools block:
|
|
1184
|
+
# - uniform case: "<header_prefix> - all at v<V>[update hint]:\n <tool>\n..."
|
|
1185
|
+
# - divergent case: "<header_prefix>:\n <tool> v<ver>[update hint]\n..."
|
|
1186
|
+
# Root-agent annotation only when status != "owned".
|
|
1187
|
+
_render_tools_block() {
|
|
1188
|
+
local manifest="$1"
|
|
1189
|
+
local ref_version="$2"
|
|
1190
|
+
local header_prefix="$3"
|
|
1191
|
+
|
|
1192
|
+
# Enumerate tools (sorted by insertion order from manifest).
|
|
1193
|
+
local -a tools=()
|
|
1194
|
+
while IFS= read -r t; do
|
|
1195
|
+
[[ -n "$t" ]] && tools+=("$t")
|
|
1196
|
+
done < <(manifest_list_tools "$manifest")
|
|
1197
|
+
|
|
1198
|
+
if [[ "${#tools[@]}" -eq 0 ]]; then
|
|
1199
|
+
# Nothing to show - shouldn't happen if manifest exists, but safe guard.
|
|
1200
|
+
printf '%s:\n' "$header_prefix"
|
|
1201
|
+
return 0
|
|
1202
|
+
fi
|
|
1203
|
+
|
|
1204
|
+
# Collect per-tool version + root-agent status.
|
|
1205
|
+
local -a tool_vers=()
|
|
1206
|
+
local -a tool_rstatus=()
|
|
1207
|
+
for tool_id in "${tools[@]}"; do
|
|
1208
|
+
local ver
|
|
1209
|
+
ver="$(manifest_read_tool_version "$manifest" "$tool_id")"
|
|
1210
|
+
tool_vers+=("${ver:-}")
|
|
1211
|
+
local root_agent
|
|
1212
|
+
root_agent="$(_root_agent_file "$tool_id")"
|
|
1213
|
+
local root_status=""
|
|
1214
|
+
if [[ -n "$root_agent" ]]; then
|
|
1215
|
+
root_status="$(manifest_read_root_agent_status "$manifest" "$tool_id" "$root_agent")"
|
|
1216
|
+
fi
|
|
1217
|
+
tool_rstatus+=("${root_status:-owned}")
|
|
1218
|
+
done
|
|
1219
|
+
|
|
1220
|
+
# Determine uniform vs divergent.
|
|
1221
|
+
local first_ver="${tool_vers[0]:-}"
|
|
1222
|
+
local uniform=1
|
|
1223
|
+
local ver
|
|
1224
|
+
for ver in "${tool_vers[@]}"; do
|
|
1225
|
+
if [[ "$ver" != "$first_ver" ]]; then
|
|
1226
|
+
uniform=0
|
|
1227
|
+
break
|
|
1228
|
+
fi
|
|
1229
|
+
done
|
|
1230
|
+
|
|
1231
|
+
if [[ "$uniform" -eq 1 ]]; then
|
|
1232
|
+
# Uniform case.
|
|
1233
|
+
local hint=""
|
|
1234
|
+
if [[ -n "$ref_version" && -n "$first_ver" ]] && _semver_lt "$first_ver" "$ref_version"; then
|
|
1235
|
+
hint=" (update -> v${ref_version})"
|
|
1236
|
+
fi
|
|
1237
|
+
printf '%s - all at v%s%s:\n' "$header_prefix" "$first_ver" "$hint"
|
|
1238
|
+
local idx=0
|
|
1239
|
+
for tool_id in "${tools[@]}"; do
|
|
1240
|
+
local rs="${tool_rstatus[$idx]}"
|
|
1241
|
+
local extra=""
|
|
1242
|
+
if [[ "$rs" != "owned" && -n "$rs" ]]; then
|
|
1243
|
+
extra=" (root pending merge)"
|
|
1244
|
+
fi
|
|
1245
|
+
printf ' %s%s\n' "$tool_id" "$extra"
|
|
1246
|
+
# --verbose: also show file count
|
|
1247
|
+
if [[ "${AID_VERBOSE:-0}" == "1" ]]; then
|
|
1248
|
+
local count=0
|
|
1249
|
+
while IFS= read -r _p; do
|
|
1250
|
+
[[ -n "$_p" ]] && count=$((count + 1))
|
|
1251
|
+
done < <(manifest_read_tool_paths "$manifest" "$tool_id")
|
|
1252
|
+
printf ' (%d files installed)\n' "$count"
|
|
1253
|
+
fi
|
|
1254
|
+
idx=$((idx + 1))
|
|
1255
|
+
done
|
|
1256
|
+
else
|
|
1257
|
+
# Divergent case.
|
|
1258
|
+
printf '%s:\n' "$header_prefix"
|
|
1259
|
+
local idx=0
|
|
1260
|
+
for tool_id in "${tools[@]}"; do
|
|
1261
|
+
local ver="${tool_vers[$idx]}"
|
|
1262
|
+
local rs="${tool_rstatus[$idx]}"
|
|
1263
|
+
local hint=""
|
|
1264
|
+
if [[ -n "$ref_version" && -n "$ver" ]] && _semver_lt "$ver" "$ref_version"; then
|
|
1265
|
+
hint=" (update -> v${ref_version})"
|
|
1266
|
+
fi
|
|
1267
|
+
local root_extra=""
|
|
1268
|
+
if [[ "$rs" != "owned" && -n "$rs" ]]; then
|
|
1269
|
+
root_extra=" (root pending merge)"
|
|
1270
|
+
fi
|
|
1271
|
+
# Pad tool id to 14 chars for alignment.
|
|
1272
|
+
local line
|
|
1273
|
+
printf -v line ' %-14s v%s%s%s' "$tool_id" "$ver" "$hint" "$root_extra"
|
|
1274
|
+
printf '%s\n' "$line"
|
|
1275
|
+
# --verbose: also show file count
|
|
1276
|
+
if [[ "${AID_VERBOSE:-0}" == "1" ]]; then
|
|
1277
|
+
local count=0
|
|
1278
|
+
while IFS= read -r _p; do
|
|
1279
|
+
[[ -n "$_p" ]] && count=$((count + 1))
|
|
1280
|
+
done < <(manifest_read_tool_paths "$manifest" "$tool_id")
|
|
1281
|
+
printf ' (%d files installed)\n' "$count"
|
|
1282
|
+
fi
|
|
1283
|
+
idx=$((idx + 1))
|
|
1284
|
+
done
|
|
1285
|
+
fi
|
|
1286
|
+
return 0
|
|
1287
|
+
}
|
|
1288
|
+
|
|
1289
|
+
# aid_status_body <target>
|
|
1290
|
+
# Renders only the installed-tools block for an AID project rooted at <target>.
|
|
1291
|
+
# Caller is responsible for checking whether a manifest exists first.
|
|
1292
|
+
# Prints:
|
|
1293
|
+
# Installed tools (in <cwd>) - all at v<V>[hint]:
|
|
1294
|
+
# <per-tool lines (name-only when uniform)>
|
|
1295
|
+
# OR (divergent):
|
|
1296
|
+
# Installed tools (in <cwd>):
|
|
1297
|
+
# <per-tool lines with version + hint>
|
|
1298
|
+
# OR (when no manifest):
|
|
1299
|
+
# No AID tools installed in <cwd> yet - run 'aid add <tool>'.
|
|
1300
|
+
# Returns:
|
|
1301
|
+
# 0 - always (no exit-7; caller decides what to do on missing manifest)
|
|
1302
|
+
aid_status_body() {
|
|
1303
|
+
local target="${1:-.}"
|
|
1304
|
+
local manifest="${target}/.aid/.aid-manifest.json"
|
|
1305
|
+
local cwd_display
|
|
1306
|
+
cwd_display="$(cd "$target" && pwd)"
|
|
1307
|
+
|
|
1308
|
+
if [[ ! -f "$manifest" ]] || ! grep -q '"manifest_version"' "$manifest" 2>/dev/null; then
|
|
1309
|
+
printf "No AID tools installed in %s yet - run 'aid add <tool>'.\n" "$cwd_display"
|
|
1310
|
+
return 0
|
|
1311
|
+
fi
|
|
1312
|
+
|
|
1313
|
+
# Read CLI ref version from $AID_HOME/VERSION.
|
|
1314
|
+
local ref_version=""
|
|
1315
|
+
if [[ -n "${AID_HOME:-}" && -f "${AID_HOME}/VERSION" ]]; then
|
|
1316
|
+
ref_version="$(tr -d '[:space:]' < "${AID_HOME}/VERSION")"
|
|
1317
|
+
fi
|
|
1318
|
+
|
|
1319
|
+
_render_tools_block "$manifest" "$ref_version" "Installed tools (in ${cwd_display})"
|
|
1320
|
+
return 0
|
|
1321
|
+
}
|
|
1322
|
+
|
|
1323
|
+
# aid_status <target>
|
|
1324
|
+
# Renders the "aid status" output for the AID project rooted at <target>.
|
|
1325
|
+
# Reads <target>/.aid/.aid-manifest.json (and .aid/.aid-version).
|
|
1326
|
+
# Returns:
|
|
1327
|
+
# 0 - manifest found; status printed to stdout.
|
|
1328
|
+
# 7 - no manifest in <target>; "not an AID project here" message printed to stdout.
|
|
1329
|
+
aid_status() {
|
|
1330
|
+
local target="${1:-.}"
|
|
1331
|
+
local manifest="${target}/.aid/.aid-manifest.json"
|
|
1332
|
+
local cwd_display
|
|
1333
|
+
cwd_display="$(cd "$target" && pwd)"
|
|
1334
|
+
|
|
1335
|
+
if [[ ! -f "$manifest" ]] || ! grep -q '"manifest_version"' "$manifest" 2>/dev/null; then
|
|
1336
|
+
printf "No AID install found in %s. Run 'aid add <tool>' to install.\n" "$cwd_display"
|
|
1337
|
+
return 7
|
|
1338
|
+
fi
|
|
1339
|
+
|
|
1340
|
+
# Read aid_version from manifest.
|
|
1341
|
+
local aid_version=""
|
|
1342
|
+
if command -v python3 >/dev/null 2>&1; then
|
|
1343
|
+
aid_version="$(python3 - "$manifest" <<'PY'
|
|
1344
|
+
import json, sys
|
|
1345
|
+
try:
|
|
1346
|
+
data = json.load(open(sys.argv[1]))
|
|
1347
|
+
print(data.get("aid_version", ""))
|
|
1348
|
+
except Exception:
|
|
1349
|
+
pass
|
|
1350
|
+
PY
|
|
1351
|
+
)"
|
|
1352
|
+
else
|
|
1353
|
+
aid_version="$(grep '"aid_version"' "$manifest" | head -1 | \
|
|
1354
|
+
sed 's/.*"aid_version"[^:]*:[^"]*"\([^"]*\)".*/\1/')"
|
|
1355
|
+
fi
|
|
1356
|
+
|
|
1357
|
+
# Read CLI ref version from $AID_HOME/VERSION.
|
|
1358
|
+
local ref_version=""
|
|
1359
|
+
if [[ -n "${AID_HOME:-}" && -f "${AID_HOME}/VERSION" ]]; then
|
|
1360
|
+
ref_version="$(tr -d '[:space:]' < "${AID_HOME}/VERSION")"
|
|
1361
|
+
fi
|
|
1362
|
+
|
|
1363
|
+
printf 'AID %s (project: %s)\n' "$aid_version" "$cwd_display"
|
|
1364
|
+
|
|
1365
|
+
_render_tools_block "$manifest" "$ref_version" "Installed tools"
|
|
1366
|
+
|
|
1367
|
+
return 0
|
|
1368
|
+
}
|
|
1369
|
+
|
|
1370
|
+
# manifest_exists <manifest> - exits 0 when manifest exists and is parseable; 6 otherwise.
|
|
1371
|
+
manifest_exists() {
|
|
1372
|
+
local manifest="$1"
|
|
1373
|
+
if [[ ! -f "$manifest" ]]; then
|
|
1374
|
+
return 6
|
|
1375
|
+
fi
|
|
1376
|
+
# Must have at least one key.
|
|
1377
|
+
if grep -q '"manifest_version"' "$manifest" 2>/dev/null; then
|
|
1378
|
+
return 0
|
|
1379
|
+
fi
|
|
1380
|
+
return 6
|
|
1381
|
+
}
|
|
1382
|
+
|
|
1383
|
+
# ---------------------------------------------------------------------------
|
|
1384
|
+
# Version marker
|
|
1385
|
+
# ---------------------------------------------------------------------------
|
|
1386
|
+
|
|
1387
|
+
# write_version_marker <target> <version>
|
|
1388
|
+
write_version_marker() {
|
|
1389
|
+
local target="$1" version="$2"
|
|
1390
|
+
mkdir -p "${target}/.aid"
|
|
1391
|
+
printf '%s\n' "$version" > "${target}/.aid/.aid-version"
|
|
1392
|
+
}
|
|
1393
|
+
|
|
1394
|
+
# ---------------------------------------------------------------------------
|
|
1395
|
+
# High-level install_tool
|
|
1396
|
+
# ---------------------------------------------------------------------------
|
|
1397
|
+
|
|
1398
|
+
# install_tool <staging_dir> <tool> <target> <version> <force>
|
|
1399
|
+
# <staging_dir> - directory produced by extract_tarball (content of profiles/<tool>/)
|
|
1400
|
+
# Returns:
|
|
1401
|
+
# 0 - success (all files installed or up-to-date)
|
|
1402
|
+
# 5 - at least one root agent file was protect-on-diff blocked
|
|
1403
|
+
#
|
|
1404
|
+
# Side effects: writes <target>/.aid/.aid-manifest.json and .aid/.aid-version.
|
|
1405
|
+
install_tool() {
|
|
1406
|
+
local staging="$1" tool="$2" target="$3" version="$4" force="${5:-0}"
|
|
1407
|
+
local manifest="${target}/.aid/.aid-manifest.json"
|
|
1408
|
+
|
|
1409
|
+
local -a install_paths=()
|
|
1410
|
+
local -a root_entries=()
|
|
1411
|
+
local blocked=0
|
|
1412
|
+
|
|
1413
|
+
# Per-tool file counters (incremented by copy_file and _copy_root_agent_file).
|
|
1414
|
+
_COPY_COUNT_COPIED=0
|
|
1415
|
+
_COPY_COUNT_UPTODATE=0
|
|
1416
|
+
_COPY_COUNT_UPDATED=0
|
|
1417
|
+
_COPY_COUNT_SKIPPED=0
|
|
1418
|
+
|
|
1419
|
+
local root_agent
|
|
1420
|
+
root_agent="$(_root_agent_file "$tool")"
|
|
1421
|
+
|
|
1422
|
+
# Determine which dirs/files this tool installs (mirrors install.sh per-tool dispatch).
|
|
1423
|
+
case "$tool" in
|
|
1424
|
+
claude-code)
|
|
1425
|
+
# .claude/ tree + CLAUDE.md
|
|
1426
|
+
if [[ -d "${staging}/.claude" ]]; then
|
|
1427
|
+
copy_dir "${staging}/.claude" "${target}/.claude" "$force"
|
|
1428
|
+
# Collect paths from .claude/
|
|
1429
|
+
while IFS= read -r -d '' f; do
|
|
1430
|
+
local rel="${f#${staging}/}"
|
|
1431
|
+
install_paths+=("$rel")
|
|
1432
|
+
done < <(find "${staging}/.claude" -type f -print0 2>/dev/null | sort -z)
|
|
1433
|
+
fi
|
|
1434
|
+
;;
|
|
1435
|
+
codex)
|
|
1436
|
+
# .codex/ + .agents/ + AGENTS.md
|
|
1437
|
+
if [[ -d "${staging}/.codex" ]]; then
|
|
1438
|
+
copy_dir "${staging}/.codex" "${target}/.codex" "$force"
|
|
1439
|
+
while IFS= read -r -d '' f; do
|
|
1440
|
+
local rel="${f#${staging}/}"
|
|
1441
|
+
install_paths+=("$rel")
|
|
1442
|
+
done < <(find "${staging}/.codex" -type f -print0 2>/dev/null | sort -z)
|
|
1443
|
+
fi
|
|
1444
|
+
if [[ -d "${staging}/.agents" ]]; then
|
|
1445
|
+
copy_dir "${staging}/.agents" "${target}/.agents" "$force"
|
|
1446
|
+
while IFS= read -r -d '' f; do
|
|
1447
|
+
local rel="${f#${staging}/}"
|
|
1448
|
+
install_paths+=("$rel")
|
|
1449
|
+
done < <(find "${staging}/.agents" -type f -print0 2>/dev/null | sort -z)
|
|
1450
|
+
fi
|
|
1451
|
+
;;
|
|
1452
|
+
cursor)
|
|
1453
|
+
# .cursor/ + AGENTS.md
|
|
1454
|
+
if [[ -d "${staging}/.cursor" ]]; then
|
|
1455
|
+
copy_dir "${staging}/.cursor" "${target}/.cursor" "$force"
|
|
1456
|
+
while IFS= read -r -d '' f; do
|
|
1457
|
+
local rel="${f#${staging}/}"
|
|
1458
|
+
install_paths+=("$rel")
|
|
1459
|
+
done < <(find "${staging}/.cursor" -type f -print0 2>/dev/null | sort -z)
|
|
1460
|
+
fi
|
|
1461
|
+
;;
|
|
1462
|
+
copilot-cli)
|
|
1463
|
+
# .github/ + AGENTS.md
|
|
1464
|
+
if [[ -d "${staging}/.github" ]]; then
|
|
1465
|
+
copy_dir "${staging}/.github" "${target}/.github" "$force"
|
|
1466
|
+
while IFS= read -r -d '' f; do
|
|
1467
|
+
local rel="${f#${staging}/}"
|
|
1468
|
+
install_paths+=("$rel")
|
|
1469
|
+
done < <(find "${staging}/.github" -type f -print0 2>/dev/null | sort -z)
|
|
1470
|
+
fi
|
|
1471
|
+
;;
|
|
1472
|
+
antigravity)
|
|
1473
|
+
# .agent/ + AGENTS.md
|
|
1474
|
+
if [[ -d "${staging}/.agent" ]]; then
|
|
1475
|
+
copy_dir "${staging}/.agent" "${target}/.agent" "$force"
|
|
1476
|
+
while IFS= read -r -d '' f; do
|
|
1477
|
+
local rel="${f#${staging}/}"
|
|
1478
|
+
install_paths+=("$rel")
|
|
1479
|
+
done < <(find "${staging}/.agent" -type f -print0 2>/dev/null | sort -z)
|
|
1480
|
+
fi
|
|
1481
|
+
;;
|
|
1482
|
+
esac
|
|
1483
|
+
|
|
1484
|
+
# Handle root agent file via FR11.
|
|
1485
|
+
local root_src="${staging}/${root_agent}"
|
|
1486
|
+
local root_dst="${target}/${root_agent}"
|
|
1487
|
+
|
|
1488
|
+
if [[ -f "$root_src" ]]; then
|
|
1489
|
+
_CORE_ROOT_AGENT_STATUS="owned"
|
|
1490
|
+
_copy_root_agent_file "$root_src" "$root_dst" "$tool" "$force" "$manifest" || {
|
|
1491
|
+
local rc=$?
|
|
1492
|
+
[[ "$rc" -eq 5 ]] && blocked=1
|
|
1493
|
+
}
|
|
1494
|
+
|
|
1495
|
+
local inc_sha
|
|
1496
|
+
inc_sha="$(sha256_file "$root_src")"
|
|
1497
|
+
root_entries+=("${root_agent}|${inc_sha}|${_CORE_ROOT_AGENT_STATUS}")
|
|
1498
|
+
|
|
1499
|
+
# Include root agent path in paths list only when owned (not pending-merge).
|
|
1500
|
+
if [[ "${_CORE_ROOT_AGENT_STATUS}" == "owned" ]]; then
|
|
1501
|
+
install_paths+=("$root_agent")
|
|
1502
|
+
fi
|
|
1503
|
+
fi
|
|
1504
|
+
|
|
1505
|
+
# Write manifest (merge).
|
|
1506
|
+
manifest_write "$manifest" "$tool" "$version" "install_paths" "root_entries"
|
|
1507
|
+
|
|
1508
|
+
# Write version marker.
|
|
1509
|
+
write_version_marker "$target" "$version"
|
|
1510
|
+
|
|
1511
|
+
# Print concise summary (always shown; per-file lines only when AID_VERBOSE=1).
|
|
1512
|
+
local _total_files=$((_COPY_COUNT_COPIED + _COPY_COUNT_UPTODATE + _COPY_COUNT_UPDATED + _COPY_COUNT_SKIPPED))
|
|
1513
|
+
if [[ "$_total_files" -gt 0 ]]; then
|
|
1514
|
+
if [[ "$_COPY_COUNT_COPIED" -gt 0 && "$_COPY_COUNT_UPTODATE" -eq 0 && "$_COPY_COUNT_UPDATED" -eq 0 ]]; then
|
|
1515
|
+
echo " ${_COPY_COUNT_COPIED} files installed"
|
|
1516
|
+
elif [[ "$_COPY_COUNT_UPTODATE" -gt 0 && "$_COPY_COUNT_COPIED" -eq 0 && "$_COPY_COUNT_UPDATED" -eq 0 ]]; then
|
|
1517
|
+
echo " up to date (${_COPY_COUNT_UPTODATE} files)"
|
|
1518
|
+
else
|
|
1519
|
+
local _parts=""
|
|
1520
|
+
[[ "$_COPY_COUNT_UPDATED" -gt 0 ]] && _parts="${_COPY_COUNT_UPDATED} updated"
|
|
1521
|
+
[[ "$_COPY_COUNT_COPIED" -gt 0 ]] && {
|
|
1522
|
+
[[ -n "$_parts" ]] && _parts="${_parts}, "
|
|
1523
|
+
_parts="${_parts}${_COPY_COUNT_COPIED} installed"
|
|
1524
|
+
}
|
|
1525
|
+
[[ "$_COPY_COUNT_UPTODATE" -gt 0 ]] && {
|
|
1526
|
+
[[ -n "$_parts" ]] && _parts="${_parts}, "
|
|
1527
|
+
_parts="${_parts}${_COPY_COUNT_UPTODATE} unchanged"
|
|
1528
|
+
}
|
|
1529
|
+
echo " ${_parts}"
|
|
1530
|
+
fi
|
|
1531
|
+
fi
|
|
1532
|
+
|
|
1533
|
+
if [[ "$blocked" -eq 1 ]]; then
|
|
1534
|
+
return 5
|
|
1535
|
+
fi
|
|
1536
|
+
return 0
|
|
1537
|
+
}
|
|
1538
|
+
|
|
1539
|
+
# ---------------------------------------------------------------------------
|
|
1540
|
+
# Uninstall
|
|
1541
|
+
# ---------------------------------------------------------------------------
|
|
1542
|
+
|
|
1543
|
+
# uninstall_tool <manifest> <tool> <target>
|
|
1544
|
+
# Removes all files recorded under tools.<tool>.paths that still exist.
|
|
1545
|
+
# For root agent files: removes only when sha256 still matches recorded value.
|
|
1546
|
+
# Prunes now-empty AID dirs. Removes the manifest if no tools remain.
|
|
1547
|
+
uninstall_tool() {
|
|
1548
|
+
local manifest="$1" tool="$2" target="$3"
|
|
1549
|
+
|
|
1550
|
+
manifest_exists "$manifest" || return 6
|
|
1551
|
+
|
|
1552
|
+
# Read all paths for this tool.
|
|
1553
|
+
local -a paths=()
|
|
1554
|
+
while IFS= read -r p; do
|
|
1555
|
+
[[ -n "$p" ]] && paths+=("$p")
|
|
1556
|
+
done < <(manifest_read_tool_paths "$manifest" "$tool")
|
|
1557
|
+
|
|
1558
|
+
if [[ "${#paths[@]}" -eq 0 ]]; then
|
|
1559
|
+
echo "Nothing to uninstall for ${tool} (no paths recorded)" >&2
|
|
1560
|
+
manifest_remove_tool "$manifest" "$tool"
|
|
1561
|
+
return 0
|
|
1562
|
+
fi
|
|
1563
|
+
|
|
1564
|
+
# Determine root agent file name.
|
|
1565
|
+
local root_agent
|
|
1566
|
+
root_agent="$(_root_agent_file "$tool")"
|
|
1567
|
+
|
|
1568
|
+
# Per-uninstall counters.
|
|
1569
|
+
local _uninst_removed=0
|
|
1570
|
+
local _uninst_leftinplace=0
|
|
1571
|
+
|
|
1572
|
+
# Remove each path.
|
|
1573
|
+
for p in "${paths[@]}"; do
|
|
1574
|
+
local full="${target}/${p}"
|
|
1575
|
+
if [[ ! -e "$full" ]]; then
|
|
1576
|
+
[[ "${AID_VERBOSE:-0}" -eq 1 ]] && echo "Already absent: ${full}"
|
|
1577
|
+
continue
|
|
1578
|
+
fi
|
|
1579
|
+
# Check if this is the root agent file -> apply FR11 uninstall check.
|
|
1580
|
+
local base
|
|
1581
|
+
base="$(basename "$p")"
|
|
1582
|
+
if [[ "$base" == "$root_agent" && "$p" == "$root_agent" ]]; then
|
|
1583
|
+
local recorded_sha
|
|
1584
|
+
recorded_sha="$(manifest_read_root_agent "$manifest" "$tool" "$root_agent")"
|
|
1585
|
+
if [[ -n "$recorded_sha" ]]; then
|
|
1586
|
+
local disk_sha
|
|
1587
|
+
disk_sha="$(sha256_file "$full")"
|
|
1588
|
+
if [[ "$disk_sha" != "$recorded_sha" ]]; then
|
|
1589
|
+
_uninst_leftinplace=$((_uninst_leftinplace + 1))
|
|
1590
|
+
# "Left in place" always shown (important for user awareness).
|
|
1591
|
+
echo "Left in place (modified or not AID-owned): ${full}"
|
|
1592
|
+
continue
|
|
1593
|
+
fi
|
|
1594
|
+
fi
|
|
1595
|
+
fi
|
|
1596
|
+
rm -f "$full"
|
|
1597
|
+
_uninst_removed=$((_uninst_removed + 1))
|
|
1598
|
+
[[ "${AID_VERBOSE:-0}" -eq 1 ]] && echo "Removed: ${full}"
|
|
1599
|
+
done
|
|
1600
|
+
|
|
1601
|
+
# Print concise uninstall summary (always shown).
|
|
1602
|
+
if [[ "$_uninst_removed" -gt 0 ]]; then
|
|
1603
|
+
echo " ${_uninst_removed} files removed"
|
|
1604
|
+
fi
|
|
1605
|
+
|
|
1606
|
+
# Prune now-empty AID-owned dirs (in reverse depth order).
|
|
1607
|
+
local -a aid_dirs=()
|
|
1608
|
+
case "$tool" in
|
|
1609
|
+
claude-code) aid_dirs+=(".claude") ;;
|
|
1610
|
+
codex) aid_dirs+=(".codex" ".agents") ;;
|
|
1611
|
+
cursor) aid_dirs+=(".cursor") ;;
|
|
1612
|
+
copilot-cli) aid_dirs+=(".github") ;;
|
|
1613
|
+
antigravity) aid_dirs+=(".agent") ;;
|
|
1614
|
+
esac
|
|
1615
|
+
|
|
1616
|
+
for d in "${aid_dirs[@]}"; do
|
|
1617
|
+
local full_dir="${target}/${d}"
|
|
1618
|
+
if [[ -d "$full_dir" ]]; then
|
|
1619
|
+
# Remove if empty (find will list any remaining files).
|
|
1620
|
+
local remaining_files
|
|
1621
|
+
remaining_files="$(find "$full_dir" -type f 2>/dev/null | head -1)"
|
|
1622
|
+
if [[ -z "$remaining_files" ]]; then
|
|
1623
|
+
rm -rf "$full_dir"
|
|
1624
|
+
[[ "${AID_VERBOSE:-0}" -eq 1 ]] && echo "Removed dir: ${full_dir}"
|
|
1625
|
+
fi
|
|
1626
|
+
fi
|
|
1627
|
+
done
|
|
1628
|
+
|
|
1629
|
+
# Remove this tool from manifest.
|
|
1630
|
+
manifest_remove_tool "$manifest" "$tool"
|
|
1631
|
+
|
|
1632
|
+
# If no manifest remains, remove the .aid version marker too.
|
|
1633
|
+
if [[ ! -f "$manifest" ]]; then
|
|
1634
|
+
local version_marker
|
|
1635
|
+
version_marker="$(dirname "$manifest")/.aid-version"
|
|
1636
|
+
rm -f "$version_marker"
|
|
1637
|
+
# Remove .aid dir if empty.
|
|
1638
|
+
local aid_meta_dir
|
|
1639
|
+
aid_meta_dir="$(dirname "$manifest")"
|
|
1640
|
+
if [[ -d "$aid_meta_dir" ]]; then
|
|
1641
|
+
local rem
|
|
1642
|
+
rem="$(find "$aid_meta_dir" -type f 2>/dev/null | head -1)"
|
|
1643
|
+
[[ -z "$rem" ]] && rmdir "$aid_meta_dir" 2>/dev/null || true
|
|
1644
|
+
fi
|
|
1645
|
+
fi
|
|
1646
|
+
}
|