caique
Coming from…

Coming from clack

A clack (@clack/prompts) alternative with two ways across: caique/clack, clack's prompts under clack's names on caique's own engine, graded by clack's own suite; or caique's flag-first prompts, where a non-TTY caller gets an error naming the flag instead of a hang.

caique is a clack alternative built on one rule: a prompt is a flag first. There are two ways across, and you can use both in one program:

  1. Change the import. caique/clack exports @clack/prompts' prompts, writers, glyphs and settings under clack's own names and call signatures, built on caique's keypress loop with no dependency outside the burgee family. Your prompts keep working.
  2. Move a prompt to a flag. caique's own API takes a prompt as a spec attached to a flag, so an agent answers it on the command line and a non-TTY run fails naming the flag instead of waiting. That is a rewrite, one prompt at a time, and it is what the rest of caique is for.

Change the import

// before
import { cancel, confirm, group, intro, isCancel, outro, select, spinner, text } from '@clack/prompts';
// after
import { cancel, confirm, group, intro, isCancel, outro, select, spinner, text } from 'caique/clack';
clack (@clack/prompts)caique/clack
text, password, confirm, multiline, date, paththe same, same options and answers
select, selectKey, multiselect, groupMultiselectthe same
autocomplete, autocompleteMultiselectthe same
intro, outro, cancel, note, log, streamthe same
spinner, tasks, groupthe same — the spinner animates only on a terminal outside CI, and prints each message once anywhere else
isCancel, CANCEL_SYMBOL, limitOptionsthe same
settings, updateSettingscaique's own: they govern caique/clack's prompts, not @clack/core's
S_*, symbol, symbolBar, unicode, unicodeOr, isTTY, isCI, formatInstructionFooter, SELECT_INSTRUCTIONS, MULTISELECT_INSTRUCTIONSthe same
box, progress, taskLognot built — no graded case reaches them

Two things differ, on purpose. The frames are clack's layout — the guide, the state glyph, the dimmed answer — and not its exact bytes, so a test of yours that snapshots clack's output will see different escape sequences. And updateSettings must be imported from caique/clack: one imported from @clack/core changes a package caique does not depend on, and our prompts cannot see it.

Is caique compatible with clack?

caique/clack is graded against @clack/prompts' own suite, vendored at 1.8.1 and unmodified apart from the import specifier. 17 of its 19 files — 289 of its 444 assertions — are snapshots of clack's exact frames and are subtracted by name; what is left grades behaviour:

passingrate
caique/clack16 / 16100.0%
@clack/prompts itself (control)16 / 16100.0%

From Compatibility, which npm run compat:page generates from the oracle's last run; that page is the authority. The 16 are all of limit-options.test.ts and two of guide.test.ts, which render all twelve prompts and check the guide with and without withGuide. A third guide.test.ts case is subtracted by its exact title. It sets updateSettings({ withGuide: false }) from @clack/core and asserts our prompts obey it. That is the case above, and it grades @clack/core's state, not @clack/prompts. The row is level, so burgee migrate rewrites @clack/prompts to caique/clack. It leaves on clack a file that imports box, progress or taskLog. It never rewrites @clack/core, and it refuses a clack file that also imports @clack/core (reason sibling-state) rather than move its prompts away from the settings that file sets. The refusal's fix reads: import { updateSettings } from 'caique/clack' instead of '@clack/core', then re-run burgee migrate.

Move a prompt to a flag

clack (@clack/prompts)caique
text, password, pathask({ kind: 'text' | 'password' | 'path', message, initial, validate }, io) from caique/ask. password is read without echo by createIo() from caique/terminal
confirmask({ kind: 'confirm', message, initial }, io). With --yes passed to decide(), it is answered true without asking
select, multiselectask({ kind: 'select' | 'multiselect', message, choices: [{ value, label, hint }] }, io) asks by number. askList(spec, io, multi) from caique/raw adds arrow keys where there is a terminal
groupresolvePrompts({ options, values, runtime, flags, io }) from caique/binding, which asks in declaration order and stops at the first refusal
isCancelcheck the result: ask() returns { ok: false, reason: 'cancelled' }, and resolvePrompts() returns a failure with code CANCELLED

A text and a confirm, before and after

// clack
import { cancel, confirm, isCancel, text } from '@clack/prompts';

const name = await text({ message: 'Project name?' });
if (isCancel(name)) { cancel('Cancelled.'); process.exit(0); }
const git = await confirm({ message: 'Initialise git?' });
if (isCancel(git)) { cancel('Cancelled.'); process.exit(0); }
// caique
import { processRuntime } from 'caique';
import { resolvePrompts } from 'caique/binding';
import { createIo } from 'caique/terminal';

const io = createIo();
const { values, failure } = await resolvePrompts({
  options: {
    name: { required: true, prompt: { kind: 'text', message: 'Project name?' } },
    git: { required: true, prompt: { kind: 'confirm', message: 'Initialise git?', initial: true } },
  },
  values: supplied,              // what --name, --git, env and config already answered
  runtime: processRuntime(),
  flags: { json, yes },          // the run's --json and --yes
  io,
});
io.close();
if (failure) {
  console.error(`${failure.message}\n${failure.fix ?? ''}`);
  process.exit(failure.code === 'CANCELLED' ? 0 : 2);
}

The two questions are the same, but the second program can also be answered without a person. An option supplied already answers is never asked. --yes answers the confirm. With no terminal, or under CI, the first unanswered option comes back as a failure, --name is required when there is no terminal, rather than a wait. One difference to carry over deliberately: caique asks only for options marked required, unless the run passed --interactive.

What you gain over clack

A prompt that waits for a human who is not there is a hang, and a hung agent is a failed task. caique's decide() rules on every prompt before anything is drawn:

  • An agent answers before it is asked. A value from any source — flag, env, config — means the prompt is skipped.
  • --json never prompts. A missing value is an error, --name is required under --json, with a fix naming the flag.
  • No terminal is never a prompt. Without a terminal on stdin, or with CI set, the verdict is an error naming the flag and the fix, never a wait on stdin. --interactive cannot override that; it can only ask for more where a terminal exists.
  • Checked exhaustively. All 256 combinations of value, kind, TTY, CI, --json, --yes, --interactive and required are generated and checked in decide.test.ts.
  • Line mode is the floor. No raw mode, no cursor movement, no redraw, so the accessible rendering and the terminal rendering are the same bytes. A closed stdin is a cancellation, not an empty answer, and invalid input is re-asked five times, then given up.

caique/clack is 62,238 B against @clack/prompts' 101,684 B across six packages, and a program that uses caique's own API never loads a byte of it; caique reaches no package outside the burgee family.

When to switch from clack

  • Your CLI runs in CI, in a container or under an agent, and a prompt there must fail fast with the flag that fixes it — move those prompts to flags.
  • You want off clack's dependency tree today without touching your prompts — change the import to caique/clack.
  • Stay on clack if you snapshot its exact frames, or need box, progress or taskLog.

The rule, the widgets and the inquirer path are on caique.

On this page