Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 7 additions & 2 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 <name>` | Reverse-lookup which top-level dep pulled in `<name>` |
| `skilltree scan <paths...>` | 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 <name>` 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 |
Expand Down
52 changes: 47 additions & 5 deletions demo/demo.tape
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -97,22 +104,57 @@ Type "skilltree deps tree"
Enter
Sleep 2s

# --- Scene 7: Show installed files ---
# --- Scene 7: Show install targets ---

Type ""
Enter
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

# --- Outro ---

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
2 changes: 1 addition & 1 deletion src/commands/doctor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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":
Expand Down
25 changes: 22 additions & 3 deletions src/commands/init.ts
Original file line number Diff line number Diff line change
Expand Up @@ -167,7 +167,8 @@ async function initGlobal(globalDirOverride?: string): Promise<void> {
* 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 indices>]" (sample sized to detected count
* via `buildSelectionHint`).
* 6. Multiple detected + non-interactive (CI / pipe) → default to `[claude]`.
* Reversible with `skilltree targets detect` later.
*
Expand Down Expand Up @@ -241,7 +242,7 @@ async function promptForTargetSelection(agents: string[], ask: AskFn): Promise<s
idx++;
}
console.log("");
const answer = (await ask("Include all? [Y/n/1,3,5] ")).trim();
const answer = (await ask(`Include all? [${buildSelectionHint(agents.length)}] `)).trim();
return parseAgentSelectionAnswer(answer, agents);
}

Expand Down Expand Up @@ -280,7 +281,7 @@ type AskFn = (question: string) => Promise<string>;

async function promptForSelection(entries: LocalEntry[], ask: AskFn): Promise<LocalEntry[]> {
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);
}

Expand Down Expand Up @@ -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<T>(answer: string, items: T[]): T[] {
const trimmed = answer.trim();
const lower = trimmed.toLowerCase();
Expand Down
5 changes: 5 additions & 0 deletions tests/commands/doctor.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 });
});

Expand Down
5 changes: 5 additions & 0 deletions tests/commands/init-agents.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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");
Expand Down
22 changes: 22 additions & 0 deletions tests/commands/init.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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.
Expand Down
Loading