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:

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:

The choice is constrained by:


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

Negative

Neutral


Alternatives considered

Alternative 1: @inquirer/prompts

The modern, modular successor to the long-standing inquirer package. Maintained by the original Inquirer team.

Pros

Cons

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

Cons

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

Cons

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

Cons

Reason for not recommending: dominated by both alternatives on different axes (UX vs. maturity).


Implementation notes

If accepted:

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:


References

Internal

External