From cde88e4c728cc08a638a4821c2a2cdd65d03c87f Mon Sep 17 00:00:00 2001 From: Marios Iliofotou Date: Sat, 23 May 2026 10:13:37 -0700 Subject: [PATCH 1/6] docs: refresh demo scenes + README list/vendor rows - demo.tape: replace bare ls with `skilltree targets list`, append `doctor` and `list` scenes so the demo reflects current CLI surface (PRs since c303916: doctor, gitignore drift check, packs footer, etc). - README commands table: note the new "Defined packs" footer on `list` and the `--target` requirement on `vendor` with multiple install_targets. No code change; re-record (`make gh-demo`) deferred to sir. --- README.md | 4 ++-- demo/demo.tape | 28 ++++++++++++++++++++++++++-- 2 files changed, 28 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 5d8acb9..76991d5 100644 --- a/README.md +++ b/README.md @@ -370,12 +370,12 @@ skilltree scan --apply ./skills/ # auto-update frontmatter | `skilltree verify` | Check installed files against lockfile | | `skilltree check` | Lint `skilltree.yml` for design-time issues (asymmetric publish, frontmatter) | | `skilltree doctor` | Preflight: schema + lint + lockfile sync + targets + gitignore + registries + frontmatter + bundled-skill freshness | -| `skilltree list` | List installed dependencies | +| `skilltree list` | List installed dependencies; appends a "Defined packs" footer for publisher repos with a non-empty `packs:` section | | `skilltree projects` | List skilltree-managed projects discoverable on this machine (read-only) | | `skilltree deps tree` | Show dependency tree | | `skilltree why ` | Reverse-lookup which top-level dep pulled in `` | | `skilltree scan ` | Detect undeclared deps in skill body text | -| `skilltree vendor` | Enter vendor mode (copy deps, commit to git) | +| `skilltree vendor` | Enter vendor mode (copy deps, commit to git); requires `--target ` when multiple `install_targets` are configured | | `skilltree unvendor` | Exit vendor mode (restore symlinks + gitignore) | | `skilltree teach` | Install the skilltree skill to all detected coding agents | | `skilltree targets list` | Show detected and configured coding agents | diff --git a/demo/demo.tape b/demo/demo.tape index 2f63909..951b754 100644 --- a/demo/demo.tape +++ b/demo/demo.tape @@ -97,7 +97,7 @@ Type "skilltree deps tree" Enter Sleep 2s -# --- Scene 7: Show installed files --- +# --- Scene 7: Show install targets --- Type "" Enter @@ -105,7 +105,31 @@ Type "# 7. Skills installed to every detected agent" Enter Sleep 500ms -Type "ls .claude/skills/ .codex/skills/" +Type "skilltree targets list" +Enter +Sleep 2s + +# --- Scene 8: Doctor — preflight health check --- + +Type "" +Enter +Type "# 8. Doctor — schema, lockfile, targets, gitignore, registries, frontmatter" +Enter +Sleep 500ms + +Type "skilltree doctor" +Enter +Sleep 3s + +# --- Scene 9: List installed deps --- + +Type "" +Enter +Type "# 9. List installed deps with versions + sources" +Enter +Sleep 500ms + +Type "skilltree list" Enter Sleep 2s From df72888292d3803c140320af871774f874c2c98a Mon Sep 17 00:00:00 2001 From: Marios Iliofotou Date: Sat, 23 May 2026 10:29:47 -0700 Subject: [PATCH 2/6] fix(init): adapt 'Include all?' prompt hint to actual option count MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The "Include all? [Y/n/1,3,5]" prompt hardcoded "1,3,5" as the comma-separated-index sample, which advertised indices that don't exist when fewer than 5 options are printed. With two detected agents the user saw `[1] / [2]` followed by `[Y/n/1,3,5]`, suggesting indices 3 and 5 were valid picks. - Add `buildSelectionHint(n)` that sizes the sample to the actual count (n=2 → "1,2", n=3..4 → "1,3", n>=5 → "1,3,5"). - Apply it at both prompt sites (`promptForTargetSelection`, `promptForSelection`). - Update demo.tape to detect cursor instead of codex — codex's install dir is `.agents/` (codex reads from there), which made the gitignore output in the demo look like a bug to viewers. --- demo/demo.tape | 4 ++-- src/commands/init.ts | 25 ++++++++++++++++++++++--- tests/commands/init-agents.test.ts | 5 +++++ tests/commands/init.test.ts | 22 ++++++++++++++++++++++ 4 files changed, 51 insertions(+), 5 deletions(-) diff --git a/demo/demo.tape b/demo/demo.tape index 951b754..a7c016c 100644 --- a/demo/demo.tape +++ b/demo/demo.tape @@ -18,7 +18,7 @@ Set TypingSpeed 35ms Hide Type "export DEMO_HOME=$(mktemp -d)" Enter -Type "mkdir -p $DEMO_HOME/.claude $DEMO_HOME/.codex" +Type "mkdir -p $DEMO_HOME/.claude $DEMO_HOME/.cursor" Enter Type "export HOME=$DEMO_HOME" Enter @@ -137,6 +137,6 @@ Sleep 2s Type "" Enter -Type "# Added 1 skill, got 2 — works across Claude Code, Codex, and more" +Type "# Added 1 skill, got 2 — works across Claude Code, Cursor, Codex, and more" Enter Sleep 3s diff --git a/src/commands/init.ts b/src/commands/init.ts index 3a682ad..405a9d9 100644 --- a/src/commands/init.ts +++ b/src/commands/init.ts @@ -167,7 +167,8 @@ async function initGlobal(globalDirOverride?: string): Promise { * 4. Multiple detected + `--yes` → enrol all. Preserves the pre-#74 behaviour * as an opt-in. * 5. Multiple detected + interactive (TTY or `askFn`) → prompt - * "Include all? [Y/n/1,3,5]". + * "Include all? [Y/n/]" (sample sized to detected count + * via `buildSelectionHint`). * 6. Multiple detected + non-interactive (CI / pipe) → default to `[claude]`. * Reversible with `skilltree targets detect` later. * @@ -241,7 +242,7 @@ async function promptForTargetSelection(agents: string[], ask: AskFn): Promise Promise; async function promptForSelection(entries: LocalEntry[], ask: AskFn): Promise { printDiscovered(entries); - const answer = (await ask("Include all? [Y/n/1,3,5] ")).trim(); + const answer = (await ask(`Include all? [${buildSelectionHint(entries.length)}] `)).trim(); return parseSelectionAnswer(answer, entries); } @@ -347,6 +348,24 @@ export function parseSelectionAnswer(answer: string, entries: LocalEntry[]): Loc * (scan discoveries) and `string[]` (agent enrolment). Empty-result handling * lives in the caller, not here. */ +/** + * Build the example-indices portion of the `Include all? [Y/n/...]` prompt. + * The "1,3,5" sample teaches the comma-separated subset grammar, but the + * indices have to actually exist in the printed list — otherwise users see + * `[1] / [2]` followed by a hint that suggests `3` and `5` are valid too. + * + * Strategy: walk odd indices up to `n` (capped at 5 for brevity), and fall + * back to `1,2` when `n === 2` so the hint still demonstrates the comma + * separator without inventing a missing index. + */ +export function buildSelectionHint(n: number): string { + if (n <= 1) return "Y/n"; + if (n === 2) return "Y/n/1,2"; + const samples: number[] = []; + for (let i = 1; i <= Math.min(n, 5); i += 2) samples.push(i); + return `Y/n/${samples.join(",")}`; +} + export function parseIndexedSelection(answer: string, items: T[]): T[] { const trimmed = answer.trim(); const lower = trimmed.toLowerCase(); diff --git a/tests/commands/init-agents.test.ts b/tests/commands/init-agents.test.ts index 82e7d6a..f36735d 100644 --- a/tests/commands/init-agents.test.ts +++ b/tests/commands/init-agents.test.ts @@ -85,6 +85,11 @@ describe("init auto-detection", () => { }); expect(question).toContain("Include all?"); + // The hint must not advertise indices the user can't pick. With 2 + // detected agents the printed list is [1] / [2], so the hint should + // stay inside that range. + expect(question).toContain("1,2"); + expect(question).not.toContain("1,3,5"); const manifest = await readManifest(dir); expect(manifest.install_targets).toContain("claude"); expect(manifest.install_targets).toContain("codex"); diff --git a/tests/commands/init.test.ts b/tests/commands/init.test.ts index 19f681c..cb0893c 100644 --- a/tests/commands/init.test.ts +++ b/tests/commands/init.test.ts @@ -4,6 +4,7 @@ import { mkdir, mkdtemp, readFile, rm, writeFile } from "node:fs/promises"; import { tmpdir } from "node:os"; import { join } from "node:path"; import { + buildSelectionHint, initCommand, parseAgentSelectionAnswer, parseIndexedSelection, @@ -296,6 +297,27 @@ describe("initCommand", () => { }); }); + // The "Include all? [Y/n/1,3,5]" hint is a *teaching* hint: it shows the + // comma-separated grammar via example indices. Hard-coding "1,3,5" leaks + // indices that don't exist when the printed list is shorter — e.g., with + // two detected agents the user sees `[1] claude / [2] codex` followed by + // `[Y/n/1,3,5]`, suggesting indices 3 and 5 are valid. The hint must + // derive its sample from the actual item count. + describe("buildSelectionHint", () => { + const cases: Array<[number, string]> = [ + [2, "Y/n/1,2"], + [3, "Y/n/1,3"], + [4, "Y/n/1,3"], + [5, "Y/n/1,3,5"], + [10, "Y/n/1,3,5"], + ]; + for (const [n, expected] of cases) { + test(`n=${n} → "${expected}"`, () => { + expect(buildSelectionHint(n)).toBe(expected); + }); + } + }); + test("--scan with askFn drives the interactive prompt and prints the listing", async () => { // Exercises the prompt pipeline end-to-end (printDiscovered + // promptForSelection + parseSelectionAnswer) without touching stdin. From f288d384d33a47a099a47cb223a3fa1ffd7cd56a Mon Sep 17 00:00:00 2001 From: Marios Iliofotou Date: Sat, 23 May 2026 10:52:33 -0700 Subject: [PATCH 3/6] fix(doctor): clarify bundled-skill 'missing' wording MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The bundled-skill check's "missing" state rendered as "Claude Code: not installed" / "Cursor: not installed", which read as "Claude Code / Cursor is not installed" — but the user is plainly using the agent. What's actually missing is the skilltree skill for that agent. Switch to "skill missing", e.g. "Claude Code: skill missing; Cursor: skill missing". The "Run `skilltree teach` to install the skilltree skill" remediation already made the intent clear; this brings the per-agent line in line with it. --- src/commands/doctor.ts | 2 +- tests/commands/doctor.test.ts | 5 +++++ 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/src/commands/doctor.ts b/src/commands/doctor.ts index 4982839..7749b5c 100644 --- a/src/commands/doctor.ts +++ b/src/commands/doctor.ts @@ -465,7 +465,7 @@ function formatAgentState(s: AgentSkillState, cliVersion: string): string { const label = getAgentLabel(s.agent) ?? s.agent; switch (s.kind) { case "missing": - return `${label}: not installed`; + return `${label}: skill missing`; case "no-version": return `${label}: predates version tracking`; case "stale": diff --git a/tests/commands/doctor.test.ts b/tests/commands/doctor.test.ts index 06f363b..81d35c7 100644 --- a/tests/commands/doctor.test.ts +++ b/tests/commands/doctor.test.ts @@ -1012,6 +1012,11 @@ describe("runDoctor — bundled-skill freshness (Fluorine)", () => { expect(row?.status).toBe("warn"); expect(row?.detail ?? "").toMatch(/claude/i); expect(row?.fix ?? "").toMatch(/skilltree teach/); + // The wording must not read as "the AGENT is not installed". The user + // is plainly using the agent (they're running skilltree doctor from + // inside it); what's missing is the skilltree SKILL for that agent. + expect(row?.detail ?? "").not.toMatch(/not installed/i); + expect(row?.detail ?? "").toMatch(/skill/i); await rm(home, { recursive: true, force: true }); }); From 6a0f2bb6a4b1a57a552ca47b4287ee3e216ef180 Mon Sep 17 00:00:00 2001 From: Marios Iliofotou Date: Sat, 23 May 2026 11:06:01 -0700 Subject: [PATCH 4/6] docs(demo): pre-run skilltree teach in hidden setup MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Without this, doctor's bundled-skill check warns about the skilltree skill being absent from the demo's fake \$DEMO_HOME — a state no returning user would actually be in (`teach` is a one-time global install). The warning was technically correct but misleading: a viewer reasonably thinks "I just ran install, why is there still something to install?" Pre-teaching in the Hide block mirrors real-world state. --- demo/demo.tape | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/demo/demo.tape b/demo/demo.tape index a7c016c..39e8bf4 100644 --- a/demo/demo.tape +++ b/demo/demo.tape @@ -13,7 +13,12 @@ Set LetterSpacing 0 Set Padding 20 Set TypingSpeed 35ms -# --- Hidden setup: clean HOME so registries start empty --- +# --- Hidden setup: clean HOME so registries start empty. +# We also pre-run `skilltree teach` to mirror a returning user's +# state — the bundled skilltree skill is a one-time global install, +# not something `skilltree install` (re)does per project. Without +# this, Scene 8's doctor would warn about a missing skill that a +# real user wouldn't actually be missing. Hide Type "export DEMO_HOME=$(mktemp -d)" @@ -22,6 +27,8 @@ Type "mkdir -p $DEMO_HOME/.claude $DEMO_HOME/.cursor" Enter Type "export HOME=$DEMO_HOME" Enter +Type "skilltree teach >/dev/null 2>&1" +Enter Type "cd $(mktemp -d)" Enter Sleep 500ms From c554df6e9d98f9a24f9bb8ae68a17b52c8c9d1ed Mon Sep 17 00:00:00 2001 From: Marios Iliofotou Date: Sat, 23 May 2026 11:16:46 -0700 Subject: [PATCH 5/6] docs(demo): add bottom padding so play button doesn't overlay text The player's play-button overlay sits at the bottom-center of the final frame and obscured the last 2-3 lines of demo output. Push the closing message up with a few blank prompts so it stays readable. --- demo/demo.tape | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/demo/demo.tape b/demo/demo.tape index 39e8bf4..7631199 100644 --- a/demo/demo.tape +++ b/demo/demo.tape @@ -147,3 +147,14 @@ Enter Type "# Added 1 skill, got 2 — works across Claude Code, Cursor, Codex, and more" Enter Sleep 3s + +# Bottom padding — the player's play-button overlay sits at the bottom-center +# of the final frame and obscures the last 2-3 lines. Push the real content +# up with a few quiet blank prompts so the closing message stays readable. +Type "" +Enter +Type "" +Enter +Type "" +Enter +Sleep 1.5s From 01e74e66e39bff740b483b5375c7cfd61ca07565 Mon Sep 17 00:00:00 2001 From: Marios Iliofotou Date: Sat, 23 May 2026 11:27:11 -0700 Subject: [PATCH 6/6] build(demo): compress GIF with gifsicle to stay under camo limit MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit GitHub's camo image proxy silently returns 404 for source images larger than ~5 MB, which means the README image stops rendering on the repo page even though the asset is live on gh-pages. The new scenes pushed the GIF from 4.8 MB to 5.5 MB and tripped this. Add a gifsicle -O3 --lossy=80 --colors 128 pass at the end of the `demo` target so every recording lands well under the limit (5.5 MB → 3.0 MB on the current tape). gifsicle joins vhs + ffmpeg as a required local tool — brew install hint included in the prereq check. --- Makefile | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/Makefile b/Makefile index 61f18c2..5a4c8f5 100644 --- a/Makefile +++ b/Makefile @@ -100,13 +100,18 @@ setup-bump: ## Post-bump variant of setup: binary + completions only, skips `tea demo: ## Record demo GIF + MP4 locally (no publish) @command -v vhs >/dev/null || (echo "Error: vhs not installed. Run: brew install vhs"; exit 1) @command -v ffmpeg >/dev/null || (echo "Error: ffmpeg not installed. Run: brew install ffmpeg"; exit 1) + @command -v gifsicle >/dev/null || (echo "Error: gifsicle not installed. Run: brew install gifsicle"; exit 1) vhs demo/demo.tape ffmpeg -y -i demo/demo.gif -movflags faststart -pix_fmt yuv420p \ -vf "scale=trunc(iw/2)*2:trunc(ih/2)*2" demo/demo.mp4 + # GitHub camo proxy rejects images > ~5 MB with a silent 404 — README + # image stops rendering on the repo page. Compress in place so we stay + # safely under the limit no matter how the tape grows. + gifsicle -O3 --lossy=80 --colors 128 -b demo/demo.gif @echo "" @echo " \033[32m✔\033[0m Recorded locally" - @echo " GIF: demo/demo.gif" - @echo " MP4: demo/demo.mp4" + @echo " GIF: demo/demo.gif ($$(du -h demo/demo.gif | cut -f1))" + @echo " MP4: demo/demo.mp4 ($$(du -h demo/demo.mp4 | cut -f1))" @echo " Run 'make gh-demo' to publish to GitHub Pages" gh-demo: demo ## Record demo and publish to GitHub Pages