ADR 0010: Prompts Library — @clack/prompts
| Field | Value |
|---|---|
| Status | Accepted |
| Date | 2026-05-12 |
| Deciders | Project maintainers |
| Supersedes | — |
| Superseded by | — |
| Related ADRs | 0001 (Runtime + language), 0009 (Argument parser), 0011 (Schemas) |
Summary
Use @clack/prompts for the interactive prompt surface in @aidokit/cli. Among the three candidates considered (@clack/prompts, @inquirer/prompts, prompts by terkelg), @clack/prompts offers the strongest first-run UX, has explicit support for the wizard-style group flows aidokit init needs, and ships a small ESM-native footprint. @inquirer/prompts is the safer fallback if maintenance signals about @clack/prompts deteriorate before v1.0.
Maintainer confirmed @clack/prompts on 2026-05-12. If maintenance signals deteriorate before v1.0, @inquirer/prompts is the planned fallback.
Context
aidokit init is the only command that drives a substantial interactive flow at v0.1. Per .docs/ARCHITECTURE.md §8 (Data flow: aidokit init), Step 4 asks the user for:
- Adapter (default to detected; only
claude-codeat v0.1). - Conformance level (Minimum / Standard / Strict — only Standard offered at v0.1).
- Stack packs (multi-select, defaults from detection).
- MCP servers (multi-select, defaults from catalog + stack-pack suggestions).
- Beads opt-in (yes/no).
Each prompt has a sensible default; the user often accepts all defaults. A good library makes that "spacebar through it" experience pleasant; a poor library makes the user read every prompt carefully. Because this is the first thing every user does, UX matters disproportionately.
Downstream (v0.5+), aidokit sync, aido mcp add, aidokit new, and aidokit migrate add more prompts. The library must scale to ~20-30 distinct prompts across the surface.
Non-functional requirements:
- ESM-native (per ADR-0001).
- TypeScript types ship with the package.
- Tree-shakeable (we shouldn't pay for prompt types we don't use).
- TTY detection respected (no prompts when
stdinis not a TTY;cli-reference.md§2.4). - Reasonable accessibility: screen-reader-friendly, no exclusive reliance on colour.
The choice is constrained by:
- ADR-0001 (ESM, TypeScript).
- ADR-0009 (Commander parses args; prompts run inside command handlers).
Decision
1. Use @clack/prompts
import * as p from '@clack/prompts';
p.intro('aidokit init');
const adapter = await p.select({
message: 'Adapter:',
options: [{ value: 'claude-code', label: 'Claude Code', hint: 'default' }],
initialValue: 'claude-code',
});
if (p.isCancel(adapter)) {
p.cancel('Cancelled.');
process.exit(40);
}
const conformance = await p.select({
/* ... */
});
const stackPacks = await p.multiselect({
/* ... */
});
const mcps = await p.multiselect({
/* ... */
});
const beads = await p.confirm({ message: 'Install Beads task graph?', initialValue: true });
p.outro('Setup complete.');
2. Use the spinner from @clack/prompts, not ora
@clack/prompts ships a built-in spinner consistent with its prompt aesthetic. Mixing it with ora (a separate spinner library mentioned in .docs/ARCHITECTURE.md §4) would cause visual mismatch. The spinner from @clack/prompts replaces ora in @aidokit/cli. We remove the ora reference from ARCHITECTURE.md as a follow-up.
3. Cancel handling is explicit
@clack/prompts returns a sentinel symbol when the user cancels (Ctrl+C). Every prompt site MUST check via p.isCancel(value) and exit with code 40 (USER_CANCELLED per .docs/ARCHITECTURE.md §16.2). A helper wraps this pattern:
async function ask<T>(promise: Promise<T | symbol>): Promise<T> {
const value = await promise;
if (p.isCancel(value)) {
p.cancel('Cancelled.');
process.exit(40);
}
return value as T;
}
4. No prompts when not a TTY
The CLI shell checks process.stdin.isTTY before any prompt. In non-TTY contexts (CI, piped invocations), --yes is required to pre-fill answers; otherwise the CLI exits with code 2 (BAD_INVOCATION) and a clear message.
5. Pinned to a tested minor range
@aidokit/cli pins to @clack/prompts@^0.7 (or the current minor at v0.1 release). Minor upgrades require a Changeset and a manual smoke test of aidokit init because UI regressions don't show up in unit tests.
Consequences
Positive
- Best-in-class first-run UX. Clack's group flow, default rendering, and post-prompt summary look polished out of the box. The "spacebar through it" path is fast and pleasant.
- Tiny.
@clack/promptsinstall footprint is small; tree-shakeable. - ESM-native. Designed for modern Node, no CJS interop quirks.
- Used by mainstream tools. Astro, Vite, and several others ship Clack. Failure modes have community pressure on them.
- Consistent visual language. Prompts, spinners, and "intro/outro" share an aesthetic without manual coordination.
Negative
- Newer than Inquirer. Less battle-tested. Mitigated: maintainer is responsive; the
0.xversioning is a signal to pin tightly until 1.0. - Opinionated aesthetic. Some users may find the rounded-corner box drawing busy. Mitigated: clack supports a "less decorated" mode; we can fall back if user feedback demands it.
- Spinner is part of the same package. A small library lock-in: replacing the prompt library later means replacing the spinner too. Acceptable.
- Some edge cases (e.g., very long option lists) render with truncation that other libraries handle differently. Real but small concern; can paginate manually if needed.
Neutral
- Cancel handling is explicit. Every site must guard. This is repetitive but the helper above reduces it to one line per prompt.
- Accessibility story is decent but not exceptional. The same is true of all three candidates; the underlying limitation is terminal UIs, not the library.
Alternatives considered
Alternative 1: @inquirer/prompts
The modern, modular successor to the long-standing inquirer package. Maintained by the original Inquirer team.
Pros
- Largest user base; most battle-tested.
- One function per prompt type (
select,input,confirm,checkbox, etc.), tree-shakeable. - TypeScript types are excellent.
- Stable v9.x; reliable upgrade story.
Cons
- Default aesthetic is plainer than Clack — no "intro/outro" framing, prompts have a more utilitarian look.
- No built-in group/wizard flow primitive; the developer composes one out of individual prompts.
- Slightly more verbose API for the same UX.
Reason for not recommending: the maturity advantage is real but the UX delta matters more for a first-run experience. If @clack/prompts shows maintenance issues before v0.1 ships, swap to @inquirer/prompts — the migration is mechanical because both libraries expose similar primitives.
Alternative 2: prompts (terkelg)
A long-standing, simple, lightweight prompts library.
Pros
- Smallest install footprint.
- Simple API.
Cons
- Maintenance pace has slowed; last meaningful release predates many ESM-era conventions.
- No built-in spinner; would need
oraor similar for status indicators. - UX is functional but plain; multi-select in particular feels dated.
Reason for not recommending: the maintenance signal is concerning, and the UX advantage is firmly with @clack/prompts.
Alternative 3: Bespoke prompts via readline and ANSI sequences
Hand-rolled prompts using Node's built-in readline module.
Pros
- Zero dependencies.
Cons
- Reinvents arrow-key handling, multi-select state, validation feedback, cancel handling, accessibility considerations.
- The first nontrivial prompt becomes a 200-line file.
Reason for not recommending: the cost-benefit math is identical to ADR-0009's custom-parser rejection. Build on a library.
Alternative 4: enquirer
Another long-standing alternative; similar to Inquirer in API and shape.
Pros
- Mature, decent customisation.
Cons
- Maintenance has been quiet recently.
- No clear advantage over
@inquirer/promptsor@clack/prompts.
Reason for not recommending: dominated by both alternatives on different axes (UX vs. maturity).
Implementation notes
If accepted:
- Add
@clack/promptstopackages/cli/package.jsonas a direct dependency. - Remove the implicit
orarecommendation from.docs/ARCHITECTURE.md§4 in a follow-up doc patch. - Implement the
ask()helper inpackages/cli/src/util/prompt.ts. - Each command's interactive path uses
@clack/promptsconsistently — no mixing withinquireror hand-rolled prompts.
If @inquirer/prompts is accepted instead, the command-side code structure is similar; only the import names and cancel-handling shape change.
Spinner integration
import * as p from '@clack/prompts';
const s = p.spinner();
s.start('Emitting CLAUDE.md and .claude/ ...');
try {
await emit(files);
s.stop('Emitted files.');
} catch (e) {
s.stop('Emission failed.', 1);
throw e;
}
Testing
Prompt-heavy flows are not unit-tested. Integration tests for aidokit init run with --yes and pre-filled flags, bypassing the interactive path. Manual smoke tests cover the interactive path before each release.
Validation
This decision is validated when:
- [ ]
aidokit initruns end-to-end with@clack/promptsagainst a clean Node 20 install on macOS, Linux, and Windows (the latter via cmd.exe and Windows Terminal). - [ ] Ctrl+C at any prompt exits with code 40 and a "Cancelled." message; no stack trace leaks.
- [ ] No
oradependency remains inpackages/cli/package.json. - [ ] A piped invocation (
echo | npx aidokit init) exits with code 2 and a clear message advising--yes.
References
Internal
- ADR 0001 — Runtime + language
- ADR 0009 — Argument parser
- ADR 0011 — Schema library
.docs/ARCHITECTURE.md§4 — Package responsibilities (mentionsora; superseded by this ADR if accepted).docs/ARCHITECTURE.md§8 —aidokit initdata flow (interactive selection step).docs/ARCHITECTURE.md§21 — open question on prompts library (this ADR closes it).docs/docs/specs/cli-reference.md§2.4 — interactivity rules
External
@clack/prompts— https://github.com/natemoo-re/clack@inquirer/prompts— https://github.com/SBoudrias/Inquirer.jsprompts— https://github.com/terkelg/promptsenquirer— https://github.com/enquirer/enquirer