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 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..7631199 100644 --- a/demo/demo.tape +++ b/demo/demo.tape @@ -13,15 +13,22 @@ 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)" 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 +Type "skilltree teach >/dev/null 2>&1" +Enter Type "cd $(mktemp -d)" Enter Sleep 500ms @@ -97,7 +104,7 @@ Type "skilltree deps tree" Enter Sleep 2s -# --- Scene 7: Show installed files --- +# --- Scene 7: Show install targets --- Type "" Enter @@ -105,7 +112,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 @@ -113,6 +144,17 @@ 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 + +# 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 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/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/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 }); }); 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.