docks-kit 0.1.0 → 0.1.2
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/AGENTS.md +14 -15
- package/README.md +12 -11
- package/SoT/.agents/skills.txt +3 -3
- package/SoT/.claude/settings.json +3 -26
- package/SoT/models.json +1 -1
- package/SoT/toolchain.json +3 -3
- package/cli/docs/install.md +39 -12
- package/cli/docs/overview.md +3 -4
- package/cli/docs/platforms.md +35 -22
- package/cli/src/commands/sync.ts +32 -4
- package/cli/src/commands/update.ts +116 -0
- package/cli/src/engine-native/DESIGN.md +89 -0
- package/cli/src/engine-native/claudeModel.ts +48 -0
- package/cli/src/engine-native/claudeSync.ts +771 -0
- package/cli/src/engine-native/codexSync.ts +439 -0
- package/cli/src/engine-native/codexToml.ts +67 -0
- package/cli/src/engine-native/exec.ts +54 -0
- package/cli/src/engine-native/index.ts +109 -0
- package/cli/src/engine-native/jq.ts +63 -0
- package/cli/src/engine-native/models.ts +73 -0
- package/cli/src/engine-native/modes.ts +133 -0
- package/cli/src/engine-native/output.ts +20 -0
- package/cli/src/engine-native/parseArgs.ts +223 -0
- package/cli/src/engine-native/settings.ts +41 -0
- package/cli/src/engine-native/skillsSync.ts +369 -0
- package/cli/src/engine-native/toolchain.ts +257 -0
- package/cli/src/engine.ts +35 -18
- package/cli/src/kitHome.ts +4 -4
- package/cli/src/main.ts +14 -1
- package/cli/src/manifests.ts +3 -2
- package/cli/tsconfig.json +1 -1
- package/docks-kit +6 -3
- package/package.json +8 -4
- package/lib/claude.sh +0 -846
- package/lib/codex.sh +0 -518
- package/lib/common.sh +0 -229
- package/lib/engine.sh +0 -153
- package/lib/skills.sh +0 -373
- package/lib/toolchain.sh +0 -222
package/lib/skills.sh
DELETED
|
@@ -1,373 +0,0 @@
|
|
|
1
|
-
#!/bin/bash
|
|
2
|
-
set -euo pipefail
|
|
3
|
-
|
|
4
|
-
# Source-order guards: this lib depends on common.sh's log/warn/err and on
|
|
5
|
-
# the entry script's REPO_DIR. Fail fast with a clear message if sourced standalone.
|
|
6
|
-
declare -F log >/dev/null 2>&1 || { printf '\033[1;31m[err]\033[0m %s\n' "lib/skills.sh must be sourced after lib/common.sh" >&2; exit 1; }
|
|
7
|
-
[[ -n "${REPO_DIR:-}" ]] || { printf '\033[1;31m[err]\033[0m %s\n' "REPO_DIR must be set before sourcing lib/skills.sh" >&2; exit 1; }
|
|
8
|
-
|
|
9
|
-
# Universal AI-agent skills bootstrap (agentskills.io standard).
|
|
10
|
-
# Reads SoT/.agents/skills.txt, runs `npx skills add` for each missing slug.
|
|
11
|
-
# Idempotent: pre-checks ~/.agents/skills/<basename> and skips if present.
|
|
12
|
-
|
|
13
|
-
skills::sync() {
|
|
14
|
-
# AGENTS_DIR is initialized in lib/common.sh as a kit-global with env override.
|
|
15
|
-
# The kit installs each skill for claude-code + codex (its SoT support
|
|
16
|
-
# matrix). Naming two agents makes the CLI keep the canonical copy at the
|
|
17
|
-
# universal ~/.agents/skills/ path — that's what our checks track.
|
|
18
|
-
SKILLS_DIR="$AGENTS_DIR/skills"
|
|
19
|
-
SKILLS_MANIFEST="$REPO_DIR/SoT/.agents/skills.txt"
|
|
20
|
-
SKILLS_SNAPSHOT="$AGENTS_DIR/.kit-managed-skills"
|
|
21
|
-
SKILLS_SYNCED=1
|
|
22
|
-
|
|
23
|
-
[[ -f "$SKILLS_MANIFEST" ]] || return
|
|
24
|
-
|
|
25
|
-
# A fresh machine may not have ~/.agents/skills/ yet; create it so the
|
|
26
|
-
# pre-check below has a dir to stat (the CLI also creates it on install).
|
|
27
|
-
[[ "$DRY_RUN" -eq 1 ]] || mkdir -p "$SKILLS_DIR"
|
|
28
|
-
|
|
29
|
-
skills::sync_universal
|
|
30
|
-
if [[ "$PRUNE" -eq 1 ]]; then
|
|
31
|
-
skills::reconcile_removals
|
|
32
|
-
fi
|
|
33
|
-
skills::sync_agent_browser_cli
|
|
34
|
-
skills::sync_effect_solutions_cli
|
|
35
|
-
skills::update_snapshot
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
# The `skills` npm package spec for npx invocations, pinned to the manifest's
|
|
39
|
-
# verified version (supply-chain: never run a floating @latest CLI every sync).
|
|
40
|
-
skills::_skills_cli() {
|
|
41
|
-
local v
|
|
42
|
-
v="$(toolchain::field skills-cli verified 2>/dev/null || true)"
|
|
43
|
-
printf '%s' "skills${v:+@$v}"
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
# Emit one cleaned slug per line from a manifest: skip blank/comment lines, strip
|
|
47
|
-
# inline #comments and ALL whitespace. Single source of truth so sync_universal,
|
|
48
|
-
# _load_slug_list, and update_snapshot can never diverge on parsing.
|
|
49
|
-
skills::_normalize_manifest() {
|
|
50
|
-
awk '
|
|
51
|
-
/^[[:space:]]*#/ { next }
|
|
52
|
-
/^[[:space:]]*$/ { next }
|
|
53
|
-
{ sub(/[[:space:]]*#.*$/, ""); gsub(/[[:space:]]+/, ""); if (length($0)) print }
|
|
54
|
-
' "$1"
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
skills::sync_universal() {
|
|
58
|
-
local slug basename added=0 already=0 failed=0 healed=0
|
|
59
|
-
|
|
60
|
-
if ! command -v node >/dev/null 2>&1; then
|
|
61
|
-
warn "node/npx not in PATH — skipping universal skills bootstrap (install Node.js to enable)"
|
|
62
|
-
return
|
|
63
|
-
fi
|
|
64
|
-
|
|
65
|
-
# DRY_RUN branches only at the action point, so dry-run and real see the same
|
|
66
|
-
# normalized slug set from skills::_normalize_manifest.
|
|
67
|
-
while IFS= read -r slug; do
|
|
68
|
-
basename="${slug##*/}"
|
|
69
|
-
|
|
70
|
-
if [[ "$DRY_RUN" -eq 1 ]]; then
|
|
71
|
-
if [[ -d "$SKILLS_DIR/$basename" ]]; then
|
|
72
|
-
echo "[dry-run] universal skill present: $basename"
|
|
73
|
-
skills::heal_claude_symlink "$basename" || true
|
|
74
|
-
else
|
|
75
|
-
echo "[dry-run] npx $(skills::_skills_cli) add $slug -g -y -a claude-code codex"
|
|
76
|
-
fi
|
|
77
|
-
continue
|
|
78
|
-
fi
|
|
79
|
-
|
|
80
|
-
if [[ -d "$SKILLS_DIR/$basename" ]]; then
|
|
81
|
-
already=$((already + 1))
|
|
82
|
-
if skills::heal_claude_symlink "$basename"; then
|
|
83
|
-
healed=$((healed + 1))
|
|
84
|
-
fi
|
|
85
|
-
continue
|
|
86
|
-
fi
|
|
87
|
-
|
|
88
|
-
# <source> is positional and MUST precede the variadic -a/--agent flag,
|
|
89
|
-
# or --agent swallows the slug. Naming both agents (claude-code codex)
|
|
90
|
-
# makes the CLI keep the canonical ~/.agents/skills/ copy and symlink it
|
|
91
|
-
# into ~/.claude/skills/; a single agent would copy-direct instead.
|
|
92
|
-
if npx --yes "$(skills::_skills_cli)" add "$slug" -g -y -a claude-code codex >/dev/null 2>&1; then
|
|
93
|
-
added=$((added + 1))
|
|
94
|
-
else
|
|
95
|
-
warn "Failed to install universal skill: $slug"
|
|
96
|
-
failed=$((failed + 1))
|
|
97
|
-
fi
|
|
98
|
-
done < <(skills::_normalize_manifest "$SKILLS_MANIFEST")
|
|
99
|
-
|
|
100
|
-
[[ "$DRY_RUN" -eq 1 ]] && return
|
|
101
|
-
|
|
102
|
-
SKILLS_PRESENT=$((added + already))
|
|
103
|
-
|
|
104
|
-
if [[ "$added" -gt 0 ]]; then
|
|
105
|
-
log "Universal skills synced (+$added new, $already already present)"
|
|
106
|
-
else
|
|
107
|
-
log "Universal skills already in sync ($already present)"
|
|
108
|
-
fi
|
|
109
|
-
if [[ "$healed" -gt 0 ]]; then
|
|
110
|
-
log "Claude per-tool symlinks healed (+$healed) — canonical present, ~/.claude/skills/<name> was missing or broken"
|
|
111
|
-
fi
|
|
112
|
-
if [[ "$failed" -gt 0 ]]; then
|
|
113
|
-
warn "$failed skill install(s) failed — re-run sync or install manually with: npx skills add <slug> -g -y -a claude-code codex"
|
|
114
|
-
fi
|
|
115
|
-
}
|
|
116
|
-
|
|
117
|
-
# Recreate ~/.claude/skills/<basename> → ../../.agents/skills/<basename>
|
|
118
|
-
# when canonical exists but the per-tool symlink is missing or broken.
|
|
119
|
-
# Matches the relative path the upstream `skills` CLI writes
|
|
120
|
-
# (vercel-labs/skills installer.ts:createSymlink uses relative(parentDir, target)).
|
|
121
|
-
# Idempotent: leaves correct symlinks alone; replaces stale/broken symlinks.
|
|
122
|
-
# Skips real (non-symlink) directories to avoid destroying user content.
|
|
123
|
-
# Returns 0 when a heal occurred, 1 otherwise (so callers can tally).
|
|
124
|
-
skills::heal_claude_symlink() {
|
|
125
|
-
local basename="$1"
|
|
126
|
-
local canonical="$SKILLS_DIR/$basename"
|
|
127
|
-
local claude_skills_dir="$HOME/.claude/skills"
|
|
128
|
-
local claude_link="$claude_skills_dir/$basename"
|
|
129
|
-
local rel_target="../../.agents/skills/$basename"
|
|
130
|
-
|
|
131
|
-
[[ -d "$canonical" ]] || return 1
|
|
132
|
-
|
|
133
|
-
if [[ -L "$claude_link" ]]; then
|
|
134
|
-
local current
|
|
135
|
-
current="$(readlink "$claude_link" 2>/dev/null || true)"
|
|
136
|
-
if [[ "$current" == "$rel_target" ]]; then
|
|
137
|
-
return 1
|
|
138
|
-
fi
|
|
139
|
-
if [[ "$DRY_RUN" -eq 1 ]]; then
|
|
140
|
-
echo "[dry-run] would replace stale Claude symlink: ~/.claude/skills/$basename -> $current (correct: $rel_target)"
|
|
141
|
-
return 0
|
|
142
|
-
fi
|
|
143
|
-
rm -f "$claude_link"
|
|
144
|
-
elif [[ -e "$claude_link" ]]; then
|
|
145
|
-
warn "~/.claude/skills/$basename exists as a real path (not a symlink) — leaving alone; remove manually if it's stale"
|
|
146
|
-
return 1
|
|
147
|
-
else
|
|
148
|
-
if [[ "$DRY_RUN" -eq 1 ]]; then
|
|
149
|
-
echo "[dry-run] would create missing Claude symlink: ~/.claude/skills/$basename -> $rel_target"
|
|
150
|
-
return 0
|
|
151
|
-
fi
|
|
152
|
-
fi
|
|
153
|
-
|
|
154
|
-
mkdir -p "$claude_skills_dir"
|
|
155
|
-
ln -s "$rel_target" "$claude_link"
|
|
156
|
-
return 0
|
|
157
|
-
}
|
|
158
|
-
|
|
159
|
-
# toolchain::ensure callback for agent-browser. Upgrade = npm refresh only (the
|
|
160
|
-
# Chrome download is not repeated); first install also pulls Chrome for Testing.
|
|
161
|
-
skills::_agent_browser_install() {
|
|
162
|
-
local mode="$1" version="${2:-}" verb="Installing"
|
|
163
|
-
[[ "$mode" == "upgrade" ]] && verb="Upgrading"
|
|
164
|
-
local pkg="agent-browser${version:+@$version}"
|
|
165
|
-
local install_flags=()
|
|
166
|
-
case "$(uname -s)" in
|
|
167
|
-
Linux*) install_flags+=(--with-deps) ;; # may prompt sudo for libnss3/libatk
|
|
168
|
-
esac
|
|
169
|
-
|
|
170
|
-
log "$verb agent-browser CLI via npm${version:+ (pinned $version)}..."
|
|
171
|
-
if ! npm install -g "$pkg" >/dev/null 2>&1; then
|
|
172
|
-
warn "npm install -g $pkg failed. Try manually: npm install -g $pkg"
|
|
173
|
-
return 1
|
|
174
|
-
fi
|
|
175
|
-
|
|
176
|
-
if [[ "$mode" == "install" ]]; then
|
|
177
|
-
log "Downloading Chrome for Testing (~175 MB; sudo may be requested for system libs on Linux)..."
|
|
178
|
-
if ! agent-browser install ${install_flags[@]+"${install_flags[@]}"}; then
|
|
179
|
-
warn "agent-browser install failed. Re-run manually: agent-browser install ${install_flags[*]:-}"
|
|
180
|
-
return 1
|
|
181
|
-
fi
|
|
182
|
-
fi
|
|
183
|
-
log "agent-browser CLI ready ($(agent-browser --version 2>/dev/null | awk '{print $NF}' || echo 'version unknown'))"
|
|
184
|
-
}
|
|
185
|
-
|
|
186
|
-
skills::sync_agent_browser_cli() {
|
|
187
|
-
# The agent-browser SKILL.md alone is just instructions — the CLI binary
|
|
188
|
-
# is what drives Chrome. Auto-install on first run because the slug is
|
|
189
|
-
# declared in skills.txt (declaration = "I want this on every machine").
|
|
190
|
-
grep -q '^vercel-labs/agent-browser$' "$SKILLS_MANIFEST" 2>/dev/null || return 0
|
|
191
|
-
|
|
192
|
-
if ! command -v npm >/dev/null 2>&1; then
|
|
193
|
-
[[ "$DRY_RUN" -eq 1 ]] || warn "npm not found — cannot auto-install agent-browser CLI. Install Node.js, then re-run sync."
|
|
194
|
-
return
|
|
195
|
-
fi
|
|
196
|
-
|
|
197
|
-
# `|| warn`: a failed npm install must not abort the remaining skills sync
|
|
198
|
-
# (snapshot write, effect-solutions) under set -e.
|
|
199
|
-
toolchain::ensure agent-browser skills::_agent_browser_install || warn "agent-browser bootstrap failed — continuing sync"
|
|
200
|
-
}
|
|
201
|
-
|
|
202
|
-
# Resolve a usable `bun` binary. `bun` lives in ~/.bun/bin, which is off the
|
|
203
|
-
# non-interactive PATH that sync/agent shells use, so `command -v` alone misses
|
|
204
|
-
# a perfectly good install. Fall back to the known install locations.
|
|
205
|
-
skills::_find_bun() {
|
|
206
|
-
local c
|
|
207
|
-
c="$(command -v bun 2>/dev/null || true)"
|
|
208
|
-
[[ -n "$c" ]] && { printf '%s\n' "$c"; return 0; }
|
|
209
|
-
for c in "${BUN_INSTALL:-$HOME/.bun}/bin/bun" "$HOME/.bun/bin/bun"; do
|
|
210
|
-
[[ -x "$c" ]] && { printf '%s\n' "$c"; return 0; }
|
|
211
|
-
done
|
|
212
|
-
return 1
|
|
213
|
-
}
|
|
214
|
-
|
|
215
|
-
# effect-solutions is the optional ground-truth Effect docs CLI the effect-kit
|
|
216
|
-
# skills consult opportunistically. It ships as a Bun bin with a
|
|
217
|
-
# `#!/usr/bin/env bun` shebang, so BOTH `bun` and the CLI must be on PATH to
|
|
218
|
-
# run it. bun's global bin (~/.cache/.bun/bin or ~/.bun/bin, depending on
|
|
219
|
-
# BUN_INSTALL/XDG_CACHE_HOME) and bun itself sit off the non-interactive PATH,
|
|
220
|
-
# and ~/.bashrc's "if not interactive, return" guard means rc PATH edits never
|
|
221
|
-
# reach agent shells. So we symlink both binaries into ~/.local/bin — already
|
|
222
|
-
# first on the agent PATH, matching Codex's official standalone install path. Gated on
|
|
223
|
-
# effect-kit being enabled in SoT; auto-installs Bun when absent (download-
|
|
224
|
-
# then-run, per the kit's no-`curl|bash` rule).
|
|
225
|
-
# Bootstrap Bun itself (download-then-run, per the kit's no-`curl|bash` rule).
|
|
226
|
-
# Echoes the bun path on success. Shared toolchain callback material: also used
|
|
227
|
-
# directly when only Bun is missing.
|
|
228
|
-
skills::_bun_bootstrap() {
|
|
229
|
-
local bun tmp_bun_installer pin
|
|
230
|
-
if bun="$(skills::_find_bun)"; then
|
|
231
|
-
printf '%s\n' "$bun"
|
|
232
|
-
return 0
|
|
233
|
-
fi
|
|
234
|
-
if ! command -v curl >/dev/null 2>&1; then
|
|
235
|
-
warn "Bun and curl both missing — cannot bootstrap Bun. Install Bun manually, then re-run sync."
|
|
236
|
-
return 1
|
|
237
|
-
fi
|
|
238
|
-
# The installer takes a release tag (bun-vX.Y.Z) as $1 — install the
|
|
239
|
-
# kit-verified version, not floating latest.
|
|
240
|
-
pin="$(toolchain::field bun verified 2>/dev/null || true)"
|
|
241
|
-
warn "Bun not found — installing Bun${pin:+ $pin (kit-verified)}..."
|
|
242
|
-
tmp_bun_installer=$(mktemp 2>/dev/null || echo "/tmp/bun-install-$$.sh")
|
|
243
|
-
curl -fsSL https://bun.sh/install -o "$tmp_bun_installer" \
|
|
244
|
-
&& bash "$tmp_bun_installer" ${pin:+"bun-v$pin"} >/dev/null 2>&1 || true
|
|
245
|
-
rm -f "$tmp_bun_installer"
|
|
246
|
-
if ! bun="$(skills::_find_bun)"; then
|
|
247
|
-
warn "Bun install failed. Install manually: curl -fsSL https://bun.sh/install -o /tmp/bun.sh && bash /tmp/bun.sh"
|
|
248
|
-
return 1
|
|
249
|
-
fi
|
|
250
|
-
log "Bun installed ($("$bun" --version 2>/dev/null || echo 'version unknown'))"
|
|
251
|
-
printf '%s\n' "$bun"
|
|
252
|
-
}
|
|
253
|
-
|
|
254
|
-
# toolchain::ensure callback for effect-solutions: install and upgrade are the
|
|
255
|
-
# same idempotent `bun add -g effect-solutions@<version>` (the gate-approved
|
|
256
|
-
# version arrives as $2) — the toolchain `track` policy is what finally gives
|
|
257
|
-
# this CLI the self-upgrade that agent-browser always had.
|
|
258
|
-
skills::_effect_solutions_install() {
|
|
259
|
-
local mode="$1" version="${2:-}" verb="Installing"
|
|
260
|
-
[[ "$mode" == "upgrade" ]] && verb="Upgrading"
|
|
261
|
-
local pkg="effect-solutions@${version:-latest}" bun gbin
|
|
262
|
-
|
|
263
|
-
bun="$(skills::_bun_bootstrap)" || return 1
|
|
264
|
-
|
|
265
|
-
log "$verb effect-solutions CLI via bun${version:+ (pinned $version)}..."
|
|
266
|
-
if ! "$bun" add -g "$pkg" >/dev/null 2>&1; then
|
|
267
|
-
warn "bun add -g $pkg failed. Try manually: bun add -g $pkg"
|
|
268
|
-
return 1
|
|
269
|
-
fi
|
|
270
|
-
|
|
271
|
-
# `bun pm -g bin` is the authoritative global-bin query (path varies with
|
|
272
|
-
# BUN_INSTALL/XDG_CACHE_HOME). Link the CLI and bun itself so the shebang
|
|
273
|
-
# resolves in non-interactive agent shells.
|
|
274
|
-
gbin="$("$bun" pm -g bin 2>/dev/null || true)"
|
|
275
|
-
if [[ -n "$gbin" && -x "$gbin/effect-solutions" ]]; then
|
|
276
|
-
mkdir -p "$HOME/.local/bin"
|
|
277
|
-
ln -sf "$bun" "$HOME/.local/bin/bun"
|
|
278
|
-
ln -sf "$gbin/effect-solutions" "$HOME/.local/bin/effect-solutions"
|
|
279
|
-
log "effect-solutions CLI ready (linked bun + effect-solutions into ~/.local/bin)"
|
|
280
|
-
else
|
|
281
|
-
warn "effect-solutions installed but binary not found under '${gbin:-<unknown>}' — link it onto PATH manually"
|
|
282
|
-
fi
|
|
283
|
-
}
|
|
284
|
-
|
|
285
|
-
skills::sync_effect_solutions_cli() {
|
|
286
|
-
local settings="$REPO_DIR/SoT/.claude/settings.json"
|
|
287
|
-
grep -Eq '"effect-kit@docks"[[:space:]]*:[[:space:]]*true' "$settings" 2>/dev/null || return 0
|
|
288
|
-
|
|
289
|
-
toolchain::ensure effect-solutions skills::_effect_solutions_install || warn "effect-solutions bootstrap failed — continuing sync"
|
|
290
|
-
}
|
|
291
|
-
|
|
292
|
-
# Populate <array_name> with stripped, non-empty slugs from <file>. Supports
|
|
293
|
-
# both manifest format (4-step strip + comment handling) and snapshot format
|
|
294
|
-
# (one slug per line, no comments — strip is a safe no-op).
|
|
295
|
-
skills::_load_slug_list() {
|
|
296
|
-
local file="$1" array_name="$2" slug
|
|
297
|
-
while IFS= read -r slug; do
|
|
298
|
-
eval "$array_name+=(\"\$slug\")"
|
|
299
|
-
done < <(skills::_normalize_manifest "$file")
|
|
300
|
-
}
|
|
301
|
-
|
|
302
|
-
# Uninstall a single snapshot slug from the universal CLI. Increments the
|
|
303
|
-
# caller's <ok_var>/<fail_var> via eval (bash 3.2-safe; no namerefs).
|
|
304
|
-
skills::_remove_one_slug() {
|
|
305
|
-
local slug="$1" ok_var="$2" fail_var="$3" basename="${1##*/}"
|
|
306
|
-
if [[ "$DRY_RUN" -eq 1 ]]; then
|
|
307
|
-
echo "[dry-run] kit-managed skill no longer in SoT — would remove: $basename"
|
|
308
|
-
return
|
|
309
|
-
fi
|
|
310
|
-
if npx --yes "$(skills::_skills_cli)" remove --global -y -a '*' -s "$basename" >/dev/null 2>&1; then
|
|
311
|
-
eval "$ok_var=\$(( \$$ok_var + 1 ))"
|
|
312
|
-
else
|
|
313
|
-
warn "Failed to remove kit-managed skill: $basename"
|
|
314
|
-
eval "$fail_var=\$(( \$$fail_var + 1 ))"
|
|
315
|
-
fi
|
|
316
|
-
}
|
|
317
|
-
|
|
318
|
-
skills::reconcile_removals() {
|
|
319
|
-
# Uninstall kit-managed skills (tracked in $SKILLS_SNAPSHOT) that are no longer
|
|
320
|
-
# declared in $SKILLS_MANIFEST. Never touches user-installed skills (not in
|
|
321
|
-
# snapshot) or CLI auto-installed meta-skills like find-skills.
|
|
322
|
-
local slug removed=0 failed=0
|
|
323
|
-
local -a current_slugs=()
|
|
324
|
-
local -a snapshot_slugs=()
|
|
325
|
-
|
|
326
|
-
if [[ ! -f "$SKILLS_SNAPSHOT" ]]; then
|
|
327
|
-
if [[ "$DRY_RUN" -eq 1 ]]; then
|
|
328
|
-
echo "[dry-run] (--prune) no kit-managed-skills snapshot yet; first real sync writes $SKILLS_SNAPSHOT, then future --prune runs reconcile against it"
|
|
329
|
-
fi
|
|
330
|
-
return
|
|
331
|
-
fi
|
|
332
|
-
|
|
333
|
-
skills::_load_slug_list "$SKILLS_MANIFEST" current_slugs
|
|
334
|
-
skills::_load_slug_list "$SKILLS_SNAPSHOT" snapshot_slugs
|
|
335
|
-
|
|
336
|
-
for slug in ${snapshot_slugs[@]+"${snapshot_slugs[@]}"}; do
|
|
337
|
-
if [[ ${#current_slugs[@]} -gt 0 ]] && printf '%s\n' "${current_slugs[@]}" | grep -qx "$slug"; then
|
|
338
|
-
continue
|
|
339
|
-
fi
|
|
340
|
-
skills::_remove_one_slug "$slug" removed failed
|
|
341
|
-
done
|
|
342
|
-
|
|
343
|
-
if [[ "$removed" -gt 0 ]]; then
|
|
344
|
-
log "Kit-managed skills removed (-$removed)"
|
|
345
|
-
fi
|
|
346
|
-
if [[ "$failed" -gt 0 ]]; then
|
|
347
|
-
warn "$failed skill remove(s) failed — re-run with --prune or run: npx skills remove -g -y -a '*' -s <name>"
|
|
348
|
-
fi
|
|
349
|
-
}
|
|
350
|
-
|
|
351
|
-
skills::update_snapshot() {
|
|
352
|
-
# Persist the current set of kit-declared slugs so the next --prune
|
|
353
|
-
# pass can detect removed entries. Format: one slug per line, sorted, stable.
|
|
354
|
-
[[ "$DRY_RUN" -eq 1 ]] && return
|
|
355
|
-
|
|
356
|
-
mkdir -p "$AGENTS_DIR"
|
|
357
|
-
skills::_normalize_manifest "$SKILLS_MANIFEST" | sort -u > "$SKILLS_SNAPSHOT"
|
|
358
|
-
}
|
|
359
|
-
|
|
360
|
-
skills::summary() {
|
|
361
|
-
[[ "${SKILLS_SYNCED:-0}" -eq 1 ]] || return
|
|
362
|
-
echo "Skills: ${SKILLS_DIR:-$HOME/.agents/skills}"
|
|
363
|
-
if [[ "$DRY_RUN" -eq 0 ]]; then
|
|
364
|
-
# SKILLS_PRESENT comes from skills::sync_universal's own tally; a bare
|
|
365
|
-
# `find` over ~/.agents/skills/ would also count user-installed skills.
|
|
366
|
-
echo " ${SKILLS_PRESENT:-0} universal skill(s) installed"
|
|
367
|
-
fi
|
|
368
|
-
}
|
|
369
|
-
|
|
370
|
-
skills::next_steps() {
|
|
371
|
-
[[ "${SKILLS_SYNCED:-0}" -eq 1 ]] || return
|
|
372
|
-
echo "Restart Claude Code (and Codex) to discover newly installed universal skills."
|
|
373
|
-
}
|
package/lib/toolchain.sh
DELETED
|
@@ -1,222 +0,0 @@
|
|
|
1
|
-
#!/bin/bash
|
|
2
|
-
set -euo pipefail
|
|
3
|
-
|
|
4
|
-
# Source-order guards: this lib depends on common.sh's log/warn/err and on
|
|
5
|
-
# the entry script's REPO_DIR. Fail fast with a clear message if sourced standalone.
|
|
6
|
-
declare -F log >/dev/null 2>&1 || { printf '\033[1;31m[err]\033[0m %s\n' "lib/toolchain.sh must be sourced after lib/common.sh" >&2; exit 1; }
|
|
7
|
-
[[ -n "${REPO_DIR:-}" ]] || { printf '\033[1;31m[err]\033[0m %s\n' "REPO_DIR must be set before sourcing lib/toolchain.sh" >&2; exit 1; }
|
|
8
|
-
|
|
9
|
-
# Verified-version-floor layer over the external toolchain (SoT/toolchain.json).
|
|
10
|
-
# The manifest holds DATA (kind/policy/floor/verified/pinnable); this lib owns
|
|
11
|
-
# the generic compare/gate/prompt machinery; tool-specific install commands stay
|
|
12
|
-
# in their owning lib (claude.sh knows rtk, skills.sh knows bun/agent-browser/
|
|
13
|
-
# effect-solutions) and are passed to toolchain::ensure as callbacks.
|
|
14
|
-
|
|
15
|
-
toolchain::_manifest() { printf '%s' "$REPO_DIR/SoT/toolchain.json"; }
|
|
16
|
-
|
|
17
|
-
toolchain::field() {
|
|
18
|
-
local tool="$1" field="$2"
|
|
19
|
-
jq -r --arg t "$tool" --arg f "$field" '.tools[$t][$f] // empty' "$(toolchain::_manifest)" 2>/dev/null
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
# Strictly-newer semver-ish compare (numeric per dotted field, like the old
|
|
23
|
-
# skills::_agent_browser_newer_npm) — returns 0 when $1 > $2. Deliberately not
|
|
24
|
-
# `sort -V` (absent on older BSD/macOS sort).
|
|
25
|
-
toolchain::_is_newer() {
|
|
26
|
-
local a="$1" b="$2" newer
|
|
27
|
-
[[ -n "$a" && -n "$b" && "$a" != "$b" ]] || return 1
|
|
28
|
-
newer=$(printf '%s\n%s\n' "$a" "$b" | sort -t. -k1,1n -k2,2n -k3,3n | tail -n1)
|
|
29
|
-
[[ "$newer" == "$a" ]]
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
# Presence probe. bun gets install-path fallbacks: it lives off the
|
|
33
|
-
# non-interactive PATH (~/.bun/bin), so `command -v` alone misses real installs.
|
|
34
|
-
toolchain::present() {
|
|
35
|
-
local tool="$1"
|
|
36
|
-
case "$tool" in
|
|
37
|
-
bun) command -v bun >/dev/null 2>&1 || [[ -x "${BUN_INSTALL:-$HOME/.bun}/bin/bun" || -x "$HOME/.bun/bin/bun" ]] ;;
|
|
38
|
-
*) command -v "$tool" >/dev/null 2>&1 ;;
|
|
39
|
-
esac
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
# Installed version, best-effort; empty when the tool is missing or the version
|
|
43
|
-
# is unparseable (ensure treats present-but-unparseable under `track` as stale
|
|
44
|
-
# and refreshes — self-healing rather than silently never upgrading).
|
|
45
|
-
toolchain::installed_version() {
|
|
46
|
-
# Every arm is `|| true`-guarded: callers assign via $(...) under
|
|
47
|
-
# set -e/pipefail, so a no-match grep or failing --version must never
|
|
48
|
-
# propagate a non-zero status — missing/unparseable just means empty output.
|
|
49
|
-
local tool="$1" bunbin
|
|
50
|
-
toolchain::present "$tool" || return 0
|
|
51
|
-
case "$tool" in
|
|
52
|
-
rtk) rtk --version 2>/dev/null | awk '{print $2}' || true ;;
|
|
53
|
-
claude) claude --version 2>/dev/null | awk '{print $1}' || true ;;
|
|
54
|
-
codex) codex --version 2>/dev/null | awk '{print $NF}' || true ;;
|
|
55
|
-
bun) { command -v bun >/dev/null 2>&1 && bun --version || "$HOME/.bun/bin/bun" --version; } 2>/dev/null || true ;;
|
|
56
|
-
agent-browser) agent-browser --version 2>/dev/null | awk '{print $NF}' || true ;;
|
|
57
|
-
effect-solutions)
|
|
58
|
-
bunbin="$(command -v bun 2>/dev/null || echo "$HOME/.bun/bin/bun")"
|
|
59
|
-
[[ -x "$bunbin" ]] && "$bunbin" pm -g ls 2>/dev/null | grep -oE 'effect-solutions@[0-9][0-9.]*' | head -1 | cut -d@ -f2 || true ;;
|
|
60
|
-
node) node --version 2>/dev/null | sed 's/^v//' || true ;;
|
|
61
|
-
npm) npm --version 2>/dev/null || true ;;
|
|
62
|
-
jq) jq --version 2>/dev/null | sed 's/^jq-//' || true ;;
|
|
63
|
-
curl) curl --version 2>/dev/null | awk 'NR==1{print $2}' || true ;;
|
|
64
|
-
tsc) tsc --version 2>/dev/null | awk '{print $2}' || true ;;
|
|
65
|
-
*) : ;;
|
|
66
|
-
esac
|
|
67
|
-
return 0
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
# Latest available version for managed tools; empty = unknown/offline (ensure
|
|
71
|
-
# then leaves the installed tool alone). Network calls capped at 5s.
|
|
72
|
-
toolchain::latest_version() {
|
|
73
|
-
local tool="$1"
|
|
74
|
-
case "$tool" in
|
|
75
|
-
rtk)
|
|
76
|
-
curl -fsSL --max-time 5 "https://api.github.com/repos/rtk-ai/rtk/releases/latest" 2>/dev/null \
|
|
77
|
-
| jq -r '.tag_name // empty' 2>/dev/null | sed 's/^v//' || true ;;
|
|
78
|
-
agent-browser|effect-solutions)
|
|
79
|
-
command -v npm >/dev/null 2>&1 && npm view "$tool" version 2>/dev/null || true ;;
|
|
80
|
-
*) : ;;
|
|
81
|
-
esac
|
|
82
|
-
return 0
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
# Gate a candidate install/upgrade against the kit-verified version. Echoes the
|
|
86
|
-
# version to install ("" = latest) and returns 0 to proceed, 1 to skip.
|
|
87
|
-
# mode=install : tool missing — a decline falls back to the pinned `verified`
|
|
88
|
-
# when pinnable (the machine still needs the tool)
|
|
89
|
-
# mode=upgrade : a decline stays on the installed version
|
|
90
|
-
toolchain::_gate() {
|
|
91
|
-
local tool="$1" mode="$2" latest="$3"
|
|
92
|
-
local verified pinnable answer
|
|
93
|
-
verified=$(toolchain::field "$tool" verified)
|
|
94
|
-
pinnable=$(toolchain::field "$tool" pinnable)
|
|
95
|
-
|
|
96
|
-
if [[ -z "$verified" ]] || ! toolchain::_is_newer "$latest" "$verified"; then
|
|
97
|
-
echo ""; return 0
|
|
98
|
-
fi
|
|
99
|
-
|
|
100
|
-
if [[ "$ASSUME_YES" -eq 1 ]]; then
|
|
101
|
-
warn "$tool $latest is newer than kit-verified $verified — proceeding (--yes)"
|
|
102
|
-
echo ""; return 0
|
|
103
|
-
fi
|
|
104
|
-
|
|
105
|
-
if [[ -t 0 ]]; then
|
|
106
|
-
printf '\033[1;33m[warn]\033[0m %s\n' "$tool $latest is not kit-verified (verified: $verified)." >&2
|
|
107
|
-
read -r -p "Install $tool $latest anyway? [y/N] " answer
|
|
108
|
-
case "$answer" in
|
|
109
|
-
[yY]*) echo ""; return 0 ;;
|
|
110
|
-
esac
|
|
111
|
-
fi
|
|
112
|
-
|
|
113
|
-
if [[ "$mode" == "install" && "$pinnable" == "true" ]]; then
|
|
114
|
-
warn "installing kit-verified $tool $verified instead of $latest"
|
|
115
|
-
echo "$verified"; return 0
|
|
116
|
-
fi
|
|
117
|
-
warn "skipping $tool ${mode} (latest $latest is above kit-verified $verified; pass --yes to accept, or update SoT/toolchain.json after testing)"
|
|
118
|
-
return 1
|
|
119
|
-
}
|
|
120
|
-
|
|
121
|
-
# Ensure a managed tool per manifest policy. install_fn is called as:
|
|
122
|
-
# <install_fn> <install|upgrade> <version> (empty version = latest)
|
|
123
|
-
toolchain::ensure() {
|
|
124
|
-
local tool="$1" install_fn="$2"
|
|
125
|
-
local policy installed latest target
|
|
126
|
-
|
|
127
|
-
policy=$(toolchain::field "$tool" policy)
|
|
128
|
-
|
|
129
|
-
if ! toolchain::present "$tool"; then
|
|
130
|
-
latest=$(toolchain::latest_version "$tool")
|
|
131
|
-
if [[ "$DRY_RUN" -eq 1 ]]; then
|
|
132
|
-
echo "[dry-run] would install $tool (${latest:-latest}, gated by toolchain.json verified pin)"
|
|
133
|
-
return 0
|
|
134
|
-
fi
|
|
135
|
-
if [[ -z "$latest" ]]; then
|
|
136
|
-
# Latest unknown (offline/rate-limited): the gate can't compare, so an
|
|
137
|
-
# empty target would install unverified latest. Pin the kit-verified
|
|
138
|
-
# version when we can; otherwise install latest with an explicit warn.
|
|
139
|
-
target=$(toolchain::field "$tool" verified)
|
|
140
|
-
if [[ -n "$target" && "$(toolchain::field "$tool" pinnable)" == "true" ]]; then
|
|
141
|
-
warn "$tool latest version unknown (offline?) — installing kit-verified $target instead"
|
|
142
|
-
else
|
|
143
|
-
target=""
|
|
144
|
-
warn "$tool latest version unknown (offline?) and not pinnable — installing latest unverified"
|
|
145
|
-
fi
|
|
146
|
-
else
|
|
147
|
-
target=$(toolchain::_gate "$tool" install "$latest") || return 0
|
|
148
|
-
fi
|
|
149
|
-
# ${target:-$latest}: install the exact version the gate approved, not a
|
|
150
|
-
# floating "latest" that may have moved since the check.
|
|
151
|
-
"$install_fn" install "${target:-$latest}"
|
|
152
|
-
return $?
|
|
153
|
-
fi
|
|
154
|
-
|
|
155
|
-
installed=$(toolchain::installed_version "$tool")
|
|
156
|
-
|
|
157
|
-
if [[ "$policy" != "track" ]]; then
|
|
158
|
-
[[ "$DRY_RUN" -eq 1 ]] && { echo "[dry-run] $tool present (${installed:-version unknown})"; return 0; }
|
|
159
|
-
log "$tool present (${installed:-version unknown})"
|
|
160
|
-
return 0
|
|
161
|
-
fi
|
|
162
|
-
|
|
163
|
-
latest=$(toolchain::latest_version "$tool")
|
|
164
|
-
if [[ -z "$latest" ]]; then
|
|
165
|
-
[[ "$DRY_RUN" -eq 1 ]] && { echo "[dry-run] $tool present (${installed:-version unknown}); latest unknown (offline?) — no action"; return 0; }
|
|
166
|
-
log "$tool present (${installed:-version unknown}; latest unknown — no action)"
|
|
167
|
-
return 0
|
|
168
|
-
fi
|
|
169
|
-
|
|
170
|
-
# Present but version unparseable → refresh to a known state.
|
|
171
|
-
if [[ -z "$installed" ]] || toolchain::_is_newer "$latest" "$installed"; then
|
|
172
|
-
if [[ "$DRY_RUN" -eq 1 ]]; then
|
|
173
|
-
echo "[dry-run] would upgrade $tool (${installed:-unknown} -> $latest, gated by toolchain.json verified pin)"
|
|
174
|
-
return 0
|
|
175
|
-
fi
|
|
176
|
-
target=$(toolchain::_gate "$tool" upgrade "$latest") || return 0
|
|
177
|
-
"$install_fn" upgrade "${target:-$latest}"
|
|
178
|
-
return $?
|
|
179
|
-
fi
|
|
180
|
-
|
|
181
|
-
[[ "$DRY_RUN" -eq 1 ]] && { echo "[dry-run] $tool up to date ($installed)"; return 0; }
|
|
182
|
-
log "$tool up to date ($installed)"
|
|
183
|
-
}
|
|
184
|
-
|
|
185
|
-
# Doctor table over every manifest tool (also consumed by `docks-kit status`).
|
|
186
|
-
# Status: ok | missing | below-floor(<floor>) | above-verified(<verified>).
|
|
187
|
-
toolchain::report() {
|
|
188
|
-
local tool kind os floor verified installed status
|
|
189
|
-
printf '%-28s %-9s %-14s %-9s %-9s %s\n' "TOOL" "KIND" "INSTALLED" "FLOOR" "VERIFIED" "STATUS"
|
|
190
|
-
while IFS= read -r tool; do
|
|
191
|
-
os=$(toolchain::field "$tool" os)
|
|
192
|
-
if [[ -n "$os" ]]; then
|
|
193
|
-
case "$(uname -s)" in
|
|
194
|
-
Linux*) [[ "$os" == "linux" ]] || continue ;;
|
|
195
|
-
Darwin*) [[ "$os" == "darwin" ]] || continue ;;
|
|
196
|
-
esac
|
|
197
|
-
fi
|
|
198
|
-
kind=$(toolchain::field "$tool" kind)
|
|
199
|
-
floor=$(toolchain::field "$tool" floor)
|
|
200
|
-
verified=$(toolchain::field "$tool" verified)
|
|
201
|
-
if [[ "$kind" == "pin" ]]; then
|
|
202
|
-
# No binary to probe — the tool is invoked via npx at the pinned version.
|
|
203
|
-
printf '%-28s %-9s %-14s %-9s %-9s %s\n' \
|
|
204
|
-
"$tool" "$kind" "(npx)" "${floor:--}" "${verified:--}" "pinned"
|
|
205
|
-
continue
|
|
206
|
-
fi
|
|
207
|
-
if toolchain::present "$tool"; then
|
|
208
|
-
installed=$(toolchain::installed_version "$tool")
|
|
209
|
-
status="ok"
|
|
210
|
-
if [[ -n "$floor" && -n "$installed" ]] && toolchain::_is_newer "$floor" "$installed"; then
|
|
211
|
-
status="below-floor"
|
|
212
|
-
elif [[ -n "$verified" && -n "$installed" ]] && toolchain::_is_newer "$installed" "$verified"; then
|
|
213
|
-
status="above-verified"
|
|
214
|
-
fi
|
|
215
|
-
else
|
|
216
|
-
installed="-"
|
|
217
|
-
status="missing"
|
|
218
|
-
fi
|
|
219
|
-
printf '%-28s %-9s %-14s %-9s %-9s %s\n' \
|
|
220
|
-
"$tool" "${kind:--}" "${installed:-?}" "${floor:--}" "${verified:--}" "$status"
|
|
221
|
-
done < <(jq -r '.tools | keys[]' "$(toolchain::_manifest)")
|
|
222
|
-
}
|