clack
Repository guidance for @clack/prompts (^1.7.0, see package.json) as used across packages/hr-skills/src/cli/*.ts and packages/hr-skills-build/src/server/build/sync.ts.
This repo's clack usage is narrow and deliberate. Every CLI here (
discover,recommend,execute-plan,generate-plan,run-evaluation,sync,validate) is a single-shot, non-interactive script driven byprocess.argv— none of them collect input via clack's interactive prompts. Full upstream docs: Getting Started, Prompts, Best Practices.
What this repo actually uses
Only five clack APIs appear anywhere in this codebase — confirmed by grepping packages/hr-skills-build/src/server:
| API | Purpose | Used in |
|---|---|---|
p.intro(title) | Announce the CLI at start | Every cli/*.ts entry point, plus server/build/sync.ts and server/validation/validate.ts |
p.outro(message) | Announce completion at end — including every failure path, or the box is left unterminated | Same as p.intro |
p.spinner() / .start() / .stop() | Show progress during buildRegistry(), validation, plan generation, and so on | Created via cliSpinner() in discover.ts, execute-plan.ts, generate-plan.ts, recommend.ts, run-evaluation.ts, server/validation/validate.ts |
p.note(message, title?) | Print a multi-line info block (search results, recommendations, usage) | discover.ts, recommend.ts, execute-plan.ts, generate-plan.ts, run-evaluation.ts, cli-bootstrap.ts |
p.log.{info,error,warn,success,message} | Single-line status/error output | All cli/*.ts, routed through cli/cli-bootstrap.ts |
p.note titles are SCREAMING CASE at every call site (RESULTS FOR …, EXECUTION PLAN, USAGE). Keep it that way.
What this repo does NOT use — do not introduce it
- No
text(),select(),confirm(),multiselect(),password(), or any other interactive prompt. These CLIs must stay scriptable (CI,bunx, piped output); blocking on stdin input would break that. If a task genuinely needs a human choice, that belongs in.claude/commands/*.md(Claude Code slash commands), not inpackages/hr-skills-build's CLIs. - No
isCancel(). It only matters when interactive prompts are in play — irrelevant here. Don't add it "for safety"; it signals dead code review comments. - No
@clack/corelow-level primitives (TextPrompt, customPromptsubclasses).@clack/promptsalone covers everything this repo needs.
If a future CLI genuinely needs interactive input, that's a deliberate scope change — flag it explicitly rather than silently adding text()/select() to keep the "always scriptable" invariant intact.
Repository pattern
Every CLI entry point in src/cli/*.ts follows this shape (see cli/cli-bootstrap.ts):
typescriptimport * as p from '@clack/prompts'; import { type CliUsage, cliSpinner, printUsageAndExit, runCli } from './cli-bootstrap.js'; const USAGE: CliUsage = { title: 'My Command', usage: 'bun run my-command "<argument>"', example: 'bun run my-command "example"', }; async function main() { const intent = process.argv[2]; // Reject a bare flag in the positional slot too, not just an empty one. if (!intent || intent.startsWith('--')) { printUsageAndExit(USAGE); } p.intro(USAGE.title); const spinner = cliSpinner(); spinner.start('Building Skill Registry...'); const registry = await buildRegistry(); spinner.stop(`Registry ready (${registry.skillCount} skills)`); // ... do the work, p.note(...) for structured output ... p.outro('Done'); } runCli(main, USAGE);
- One
CliUsageper command, shared byprintUsageAndExit,runCli, andp.intro(USAGE.title), so the--helpscreen and the bad-invocation error can't drift apart. - Pass
USAGEtorunCli. That is what implements--help/-h: it prints usage and exits 0 beforemain()runs. Omit it and the command treats--helpas a positional argument — which is howbun run evaluate --helponce ran the whole evaluation suite andbun run plan --helpgenerated a plan for the literal intent"--help". - Use
cliSpinner(), neverp.spinner()directly. A running spinner repaints every 80ms by erasing the lines beneath it, and clack's exit handler then stamps "Canceled" over the area — so an error thrown mid-spin is invisible.cliSpinner()registers the spinner sorunClican stop it with the real message. printUsageAndExit/runClistandardize the bad-invocation and uncaught-error paths — seecli-bootstrap. Don't hand-rollp.log.error(...); process.exit(1);again; that duplication is exactly whatcli-bootstrap.tswas extracted to remove (seedry-refactoring). Prefer letting a typed error propagate torunCliover catching it locally.p.intro/p.outrobookend the whole run — call each exactly once, on success and failure alike.p.log.*output renders with clack's│gutter but no┌above and no└below, so anexit(1)with no outro leaves a visibly broken box.- Validate flag values, not just presence:
--limit 0and--limit abcused to reach the search layer as0/NaNand produce "no results", blaming the query for a bad flag. Reject unknown flags too, rather than ignoring them. spinner.stop(message)should report a concrete result (count, duration), not just "Done" — matches the existingRegistry ready (${registry.skillCount} skills)style.- A CLI module that anything else imports (
validation/validate.tsis imported by its own test) keeps itsif (import.meta.main)guard aroundrunCli, and parsesprocess.argvinside it — otherwise a plainimportruns the command or parses the host process's flags.
Key prompts
Adding a new CLI
- "Scaffold a new
src/cli/*.tsentry point following this repo's clack pattern." - "Wire this new CLI through
cli-bootstrap.ts'srunCli/printUsageAndExit."
Reviewing clack usage
- "Check whether this CLI change introduces an interactive prompt that would break scriptability."
- "Review this
p.spinner()usage for a concrete stop message." - "Is this
p.log.*call redundant with whatcli-bootstrap.tsalready provides?"
Tips
- Keep
p.intro/p.outromessages short — they're a visual bookend, not a status report. - Prefer
p.note()over multiplep.log.message()calls when printing a related block (search results, a plan summary) — it groups visually. - Never gate a
cli/*.tsscript on user input; that's the one hard rule this skill exists to protect.


