docks-kit 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (49) hide show
  1. package/AGENTS.md +99 -0
  2. package/LICENSE +21 -0
  3. package/README.md +124 -0
  4. package/SoT/.agents/skills.txt +14 -0
  5. package/SoT/.claude/CLAUDE.md +146 -0
  6. package/SoT/.claude/fetch-usage.sh +66 -0
  7. package/SoT/.claude/hooks/notify.sh +14 -0
  8. package/SoT/.claude/mcp-servers.json +10 -0
  9. package/SoT/.claude/settings.json +258 -0
  10. package/SoT/.claude/statusline.sh +175 -0
  11. package/SoT/.codex/AGENTS.md +75 -0
  12. package/SoT/.codex/agents/.gitkeep +1 -0
  13. package/SoT/.codex/config.toml +45 -0
  14. package/SoT/.codex/plugins/marketplace.json +50 -0
  15. package/SoT/.codex/rules/docks.rules +116 -0
  16. package/SoT/models.json +28 -0
  17. package/SoT/toolchain.json +26 -0
  18. package/cli/docs/flags.md +48 -0
  19. package/cli/docs/install.md +56 -0
  20. package/cli/docs/models.md +42 -0
  21. package/cli/docs/modifiers.md +33 -0
  22. package/cli/docs/overview.md +37 -0
  23. package/cli/docs/platforms.md +31 -0
  24. package/cli/docs/plugins.md +44 -0
  25. package/cli/docs/sync-layers.md +43 -0
  26. package/cli/docs/toolchain.md +54 -0
  27. package/cli/src/commands/docs.ts +65 -0
  28. package/cli/src/commands/model.ts +60 -0
  29. package/cli/src/commands/models.ts +46 -0
  30. package/cli/src/commands/plugins.ts +34 -0
  31. package/cli/src/commands/skills.ts +37 -0
  32. package/cli/src/commands/status.ts +79 -0
  33. package/cli/src/commands/sync.ts +134 -0
  34. package/cli/src/commands/toolchain.ts +42 -0
  35. package/cli/src/engine.ts +36 -0
  36. package/cli/src/kitHome.ts +31 -0
  37. package/cli/src/main.ts +60 -0
  38. package/cli/src/manifests.ts +105 -0
  39. package/cli/src/md.d.ts +4 -0
  40. package/cli/tsconfig.json +14 -0
  41. package/docks-kit +61 -0
  42. package/lib/claude.sh +846 -0
  43. package/lib/codex.sh +518 -0
  44. package/lib/common.sh +229 -0
  45. package/lib/engine.sh +153 -0
  46. package/lib/skills.sh +373 -0
  47. package/lib/toolchain.sh +222 -0
  48. package/notification.mp3 +0 -0
  49. package/package.json +39 -0
package/lib/skills.sh ADDED
@@ -0,0 +1,373 @@
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
+ }
@@ -0,0 +1,222 @@
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
+ }
Binary file
package/package.json ADDED
@@ -0,0 +1,39 @@
1
+ {
2
+ "name": "docks-kit",
3
+ "version": "0.1.0",
4
+ "description": "Portable AI coding agent config kit — SoT sync engine + typed CLI for Claude Code, Codex, and universal agent skills",
5
+ "type": "module",
6
+ "license": "MIT",
7
+ "repository": {
8
+ "type": "git",
9
+ "url": "git+https://github.com/DocksDocks/public.git"
10
+ },
11
+ "bin": {
12
+ "docks-kit": "cli/src/main.ts"
13
+ },
14
+ "files": [
15
+ "cli/src",
16
+ "cli/docs",
17
+ "cli/tsconfig.json",
18
+ "lib",
19
+ "SoT",
20
+ "notification.mp3",
21
+ "docks-kit",
22
+ "AGENTS.md",
23
+ "README.md"
24
+ ],
25
+ "scripts": {
26
+ "typecheck": "tsc --noEmit -p cli",
27
+ "build:binaries": "bash cli/build-binaries.sh"
28
+ },
29
+ "dependencies": {
30
+ "@effect/cli": "0.75.2",
31
+ "@effect/platform": "0.96.1",
32
+ "@effect/platform-bun": "0.90.0",
33
+ "effect": "3.21.4"
34
+ },
35
+ "devDependencies": {
36
+ "@types/bun": "^1.3.0",
37
+ "typescript": "^5.7.2"
38
+ }
39
+ }