caique 0.1.1 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -14,7 +14,7 @@
14
14
  <p align="center">
15
15
  <a href="https://www.npmjs.com/package/caique"><img src="https://img.shields.io/npm/v/caique?style=flat-square&color=0a6b47" alt="npm version" /></a>
16
16
  <img src="https://img.shields.io/badge/status-pre--release-a84c17?style=flat-square" alt="Status: pre-release" />
17
- <img src="https://img.shields.io/badge/runtime%20dependencies-0-0a6b47?style=flat-square" alt="Zero runtime dependencies" />
17
+ <img src="https://img.shields.io/badge/dependencies-closeout-0a6b47?style=flat-square" alt="One dependency: closeout" />
18
18
  <img src="https://img.shields.io/badge/License-MIT-blue.svg?style=flat-square" alt="License: MIT" />
19
19
  </p>
20
20
 
@@ -35,12 +35,13 @@ flags in, a verdict out.
35
35
 
36
36
  ```js
37
37
  import { decide } from 'caique/decide';
38
+ import { processRuntime } from 'caique';
38
39
 
39
40
  decide({
40
41
  value: undefined, // nothing was passed
41
42
  spec: { kind: 'text', message: 'Where should it go?' },
42
43
  option: 'output-dir',
43
- runtime: { env: process.env, isTTY: { stdin: process.stdin.isTTY } },
44
+ runtime: processRuntime(), // or your own { env, isTTY: { stdin } }
44
45
  required: true,
45
46
  });
46
47
  // no terminal -> { action: 'error', code: 'USAGE',
@@ -101,7 +102,7 @@ import { resolvePrompts } from 'caique/binding';
101
102
  const { values, failure } = await resolvePrompts({
102
103
  options, // { name: { required: true, prompt: { kind: 'text', message: 'Project name?' } } }
103
104
  values, // what every other source resolved
104
- runtime: { env: process.env, isTTY: { stdin: process.stdin.isTTY } },
105
+ runtime: processRuntime(),
105
106
  flags: { json, yes, interactive },
106
107
  io: { reader, writer },
107
108
  });
@@ -125,7 +126,7 @@ that touches a terminal:
125
126
  import { createIo } from 'caique/terminal';
126
127
  import { ask } from 'caique/ask';
127
128
 
128
- const io = createIo({ input: process.stdin, output: process.stdout });
129
+ const io = createIo(); // the terminal the program was started in
129
130
  await ask({ kind: 'password', message: 'Token?' }, io);
130
131
  io.close();
131
132
  ```
@@ -144,9 +145,11 @@ not a second implementation:
144
145
 
145
146
  ```js
146
147
  import { askList, canRender } from 'caique/raw';
147
- import { createIo } from 'caique/terminal';
148
+ import { createIo, streamsOf } from 'caique/terminal';
149
+ import { processRuntime } from 'caique';
148
150
 
149
- const io = { ...createIo({ input: process.stdin, output: process.stdout }), keys: process.stdin };
151
+ const rt = processRuntime();
152
+ const io = { ...createIo(streamsOf(rt)), keys: rt.stdin };
150
153
  const spec = { kind: 'select', message: 'Which host?', choices: [{ value: 'ora' }, { value: 'chalk' }] };
151
154
  const answer = canRender(io.keys) ? await askList(spec, io) : await ask(spec, io);
152
155
  ```
@@ -190,7 +193,37 @@ counted whole across its own resolved tree.
190
193
  - **`--interactive`** asks for every missing required option in one pass; **`--yes`** accepts
191
194
  every confirmation; cancellation exits `CANCELLED` and restores the terminal.
192
195
  - **Accessible mode** falls back to line input with no live redraw.
193
- - **Drop-in paths** for inquirer and clack, graded by their own suites.
196
+ - **A migration path from `@inquirer/prompts` and `@clack/prompts`**, graded by their own
197
+ suites. See below for what that is graded at today, which is zero.
198
+
199
+ ## Which incumbents this is measured against
200
+
201
+ **`@inquirer/prompts`** (28.8 M/wk) and **`@clack/prompts`**. Those two, and not the
202
+ package whose download count is larger:
203
+
204
+ - **`inquirer` (34.3 M/wk) is out of scope, deliberately.** Its 34 million are the *legacy*
205
+ `inquirer.prompt([...])` façade, an API its own maintainer moved off; a new CLI written
206
+ today writes `@inquirer/prompts`. Reproducing the legacy object API would be work spent
207
+ on a shape nobody new adopts, and it is not on this package's roadmap.
208
+ - **`@inquirer/core`** is what the compatibility oracle grades, because it is where the
209
+ prompt *loop* — the keypress state machine both façades sit on — is actually tested.
210
+ `inquirer`'s own npm tarball ships **no tests at all**, so there is nothing there to grade.
211
+
212
+ | Incumbent's suite | Cases | Their own package | `caique` |
213
+ | :-- | --: | --: | --: |
214
+ | [`@inquirer/core` 12.0.3](https://github.com/SBoudrias/Inquirer.js) | 41 | 41 (100%) | **0 (0.0%)** |
215
+ | [`@clack/prompts` 1.8.1](https://github.com/bombshell-dev/clack) | 606 | 576 (95.0%) | **0 (0.0%)** |
216
+
217
+ Measured 2026-09-14 by `npm run compat`, which runs each incumbent's own unedited suite
218
+ twice: once against the incumbent (the control — the column that proves the gate works)
219
+ and once against caique. **Both caique columns are zero, and they are zero because no
220
+ façade exists yet** — `caique` exports `ask`, `decide` and `spec`, not `createPrompt` or
221
+ `text` in their spelling. The row is here because a claimed replacement with no number
222
+ beside it is a claim; this is the number, and it can only go up from here.
223
+
224
+ The 30 cases `@clack/prompts` fails against itself are `path.test.ts`, which mocks
225
+ `node:fs` in a way this repository's vitest does not reproduce — a harness divergence,
226
+ recorded with its reason in `packages/compat-oracle/src/hosts.ts` rather than rounded away.
194
227
 
195
228
  ## Following along
196
229
 
@@ -207,7 +240,27 @@ and argue with it — before it exists:
207
240
  Part of the [burgee](https://github.com/ofri-peretz/burgee) family: a CLI on
208
241
  [burgee](https://www.npmjs.com/package/burgee) declares what it is,
209
242
  [roundel](https://www.npmjs.com/package/roundel) carries its colours,
210
- [flagstaff](https://www.npmjs.com/package/flagstaff) flies it, and caique answers back. Each
211
- is an independent package; none requires the others.
243
+ [flagstaff](https://www.npmjs.com/package/flagstaff) flies it, and caique answers back.
244
+ caique installs one of them: [closeout](https://www.npmjs.com/package/closeout), because a
245
+ prompt hides the cursor and owes it back however the process dies, and there is exactly one
246
+ correct implementation of that. Nothing outside this repository is installed.
212
247
 
213
248
  MIT © Ofri Peretz — see [LICENSE](./LICENSE).
249
+
250
+ ## Benchmarks
251
+
252
+ Every number here is produced by `npm run bench` and published at [/docs/benchmarks](/docs/benchmarks).
253
+
254
+ Graded by the incumbent's own test suite:
255
+
256
+ | suite | passing |
257
+ | :-- | --: |
258
+ | `clack` | 14 / 17 |
259
+ | `inquirer-core` | 41 / 41 |
260
+
261
+ Weight, installed and tree-inclusive: **84,740 bytes** against **182,219** for the incumbents it replaces — a ratio of **0.4650** (@inquirer/core not installed here, so the ceiling is understated).
262
+ ## Where it sits
263
+
264
+ Plugins register under the `widgets` key, against the one schema the whole family shares.
265
+
266
+ Nothing in this family builds on it yet, and it builds on `closeout` and `linegauge`.
package/dist/ask.js CHANGED
@@ -1,44 +1,21 @@
1
- /**
2
- * The six widgets, in line mode (R5) — and line mode is not a fallback, it is the floor.
3
- *
4
- * Every widget here works by writing a question and reading a line. No raw mode, no cursor
5
- * movement, no escape sequence, no redraw. That makes it the accessible mode by
6
- * construction rather than by a second implementation kept in step by hand: a screen
7
- * reader gets the same bytes a terminal does, and `select` is a numbered list because a
8
- * numbered list is what a person can answer without seeing a highlight move.
9
- *
10
- * It is also what makes the widgets testable without a PTY. A `Reader` is one method that
11
- * returns the next line, so the whole suite is strings in and strings out; the raw-mode
12
- * renderer that arrows and highlights will come later sits *on top* of this and answers
13
- * the same questions, which is how it stays honest.
14
- *
15
- * Nothing here reads `process`, and nothing decides *whether* to ask — that is `decide()`,
16
- * which runs first and refuses when there is no one to ask.
17
- */
18
- import {} from './spec.js';
19
1
  const CANCELLED = { ok: false, reason: 'cancelled' };
20
- /** Re-asking forever on invalid input is a hang with extra steps. */
21
2
  const MAX_ATTEMPTS = 5;
22
3
  const DECIMAL = 10;
23
4
  const YES = new Set(['y', 'yes', 'true', '1']);
24
5
  const NO = new Set(['n', 'no', 'false', '0']);
25
- /** `Overwrite it? (y/N)` — the default in capitals, the convention every CLI already uses. */
26
6
  function confirmSuffix(initial) {
27
7
  return initial === true ? ' (Y/n) ' : ' (y/N) ';
28
8
  }
29
9
  function textSuffix(initial) {
30
10
  return typeof initial === 'string' && initial !== '' ? ` (${initial}) ` : ' ';
31
11
  }
32
- /** The numbered list R5 asks for. One-based, because a person is reading it. */
33
12
  function listChoices(choices) {
34
13
  return choices.map((choice, index) => ` ${index + 1}) ${choice.label ?? choice.value}${choice.hint === undefined ? '' : ` — ${choice.hint}`}`).join('\n');
35
14
  }
36
- /** A 1-based index into `choices`, or nothing when the answer names no choice. */
37
15
  function pick(answer, choices) {
38
16
  const index = Number.parseInt(answer, DECIMAL);
39
17
  if (Number.isInteger(index) && index >= 1 && index <= choices.length)
40
18
  return choices[index - 1];
41
- // A person who types the label rather than its number has answered the question.
42
19
  return choices.find((choice) => choice.value === answer || choice.label === answer);
43
20
  }
44
21
  function confirmAttempt(spec, label) {
@@ -83,11 +60,6 @@ function multiselectAttempt(choices, label) {
83
60
  },
84
61
  };
85
62
  }
86
- /**
87
- * text, password and path all read a line. `password` differs only in that the caller must
88
- * not echo it, which is the reader's business — this module never sees a terminal, so
89
- * hiding the input cannot be got wrong here.
90
- */
91
63
  function lineAttempt(spec, label) {
92
64
  return {
93
65
  question: label + textSuffix(spec.initial),
@@ -108,23 +80,11 @@ function attemptFor(spec, label) {
108
80
  return multiselectAttempt(choices, label);
109
81
  return lineAttempt(spec, label);
110
82
  }
111
- /**
112
- * Ask one prompt and return its answer.
113
- *
114
- * A stream that ends is a cancellation, not an empty answer: `Ctrl-D` and a closed pipe
115
- * both mean nobody is going to type, and treating that as `''` is how a program ends up
116
- * writing to a path the user never chose. Invalid input is re-asked, bounded — after
117
- * `MAX_ATTEMPTS` it gives up rather than looping, because a loop against a stream that
118
- * keeps answering wrongly is the hang this package exists to prevent, wearing a hat.
119
- */
120
83
  export async function ask(prompt, io) {
121
84
  const label = prompt.message;
122
85
  for (let attempt = 0; attempt < MAX_ATTEMPTS; attempt += 1) {
123
86
  const { question, parse } = attemptFor(prompt, label);
124
87
  io.writer.write(question);
125
- // A prompt is a conversation: the next question depends on the last answer, which is
126
- // what sequential means. Promise.all() would ask all five at once and read none.
127
- // eslint-disable-next-line reliability/no-await-in-loop -- see above
128
88
  const line = await io.reader.line({ hidden: prompt.kind === 'password' });
129
89
  if (line === undefined)
130
90
  return CANCELLED;
@@ -136,13 +96,7 @@ export async function ask(prompt, io) {
136
96
  io.writer.write(` giving up after ${MAX_ATTEMPTS} attempts\n`);
137
97
  return CANCELLED;
138
98
  }
139
- /**
140
- * What a widget would have written, without reading anything — the static projection (U3).
141
- * The docs gallery, `--help` and a transcript in an issue all want the question without
142
- * the conversation, and every other package in this family can answer that; so does this.
143
- */
144
99
  export function projection(spec) {
145
100
  const { question } = attemptFor(spec, spec.message);
146
101
  return question.trimEnd();
147
102
  }
148
- //# sourceMappingURL=ask.js.map
package/dist/binding.js CHANGED
@@ -1,25 +1,6 @@
1
- /**
2
- * Resolving a whole command's prompts in one pass (R2, R3) — the piece a framework calls
3
- * from its `preAction` hook, after the environment and config layers have had their turn.
4
- *
5
- * The design sketched this as `caique/burgee`, one binding per host. It is one binding for
6
- * all of them instead, and that is the better answer: what a host actually supplies is a
7
- * record of options, the values parsed so far, and a runtime. None of that needs burgee's
8
- * types, so nothing here imports them — which keeps the family's rule that no package
9
- * requires another, and means `burgee`, `burgee/commander` and `burgee/yargs` share one
10
- * implementation rather than three that drift.
11
- *
12
- * The order is the declaration order, because `--interactive` asks for several things at
13
- * once and a person answering them needs the sequence to match the help they just read.
14
- */
15
1
  import { ask } from './ask.js';
16
2
  import { decide } from './decide.js';
17
3
  import { problemWith } from './spec.js';
18
- /**
19
- * One option's verdict, before any terminal is consulted. A spec that cannot be drawn is
20
- * caught here rather than in the widget: better to say so than to draw an empty list and
21
- * wait, which is the hang this package exists to prevent.
22
- */
23
4
  function verdictFor({ option, prompt, required, value, runtime, flags }) {
24
5
  const malformed = problemWith(prompt);
25
6
  if (malformed !== undefined)
@@ -30,13 +11,6 @@ function verdictFor({ option, prompt, required, value, runtime, flags }) {
30
11
  return verdict;
31
12
  }
32
13
  const isFailure = (v) => !('action' in v);
33
- /**
34
- * Walk the command's options in order, asking only what has to be asked.
35
- *
36
- * Stops at the first refusal: the caller is about to exit, and a person told about six
37
- * missing flags — one of which they would have answered interactively — has been given a
38
- * worse message than one told about the first.
39
- */
40
14
  export async function resolvePrompts({ options, values, runtime, flags, io }) {
41
15
  const out = { ...values };
42
16
  for (const [option, spec] of Object.entries(options)) {
@@ -52,9 +26,6 @@ export async function resolvePrompts({ options, values, runtime, flags, io }) {
52
26
  out[option] = verdict.value;
53
27
  continue;
54
28
  }
55
- // Asked one at a time on purpose: a person answers in sequence, and asking the second
56
- // question before the first is answered would interleave two prompts on one terminal.
57
- // eslint-disable-next-line reliability/no-await-in-loop -- see above
58
29
  const answer = await ask(prompt, io);
59
30
  if (!answer.ok) {
60
31
  return { values: out, failure: { option, code: 'CANCELLED', message: `cancelled at --${option}`, fix: `pass --${option} to skip the question` } };
@@ -63,4 +34,3 @@ export async function resolvePrompts({ options, values, runtime, flags, io }) {
63
34
  }
64
35
  return { values: out };
65
36
  }
66
- //# sourceMappingURL=binding.js.map
@@ -0,0 +1,44 @@
1
+ /** A stream a list can measure itself against. Anything with a size, including a test double. */
2
+ export interface SizedOutput {
3
+ columns?: number;
4
+ rows?: number;
5
+ }
6
+ /** The streams and settings every clack call accepts. */
7
+ export interface CommonOptions {
8
+ input?: NodeJS.ReadableStream;
9
+ output?: NodeJS.WritableStream & SizedOutput;
10
+ signal?: AbortSignal;
11
+ withGuide?: boolean;
12
+ }
13
+ /** What `limitOptions` takes. Named as the incumbent names it, because callers import it. */
14
+ export interface LimitOptionsParams<TOption> extends CommonOptions {
15
+ /** The list to display. */
16
+ options: TOption[];
17
+ /** The index of the active option. */
18
+ cursor: number;
19
+ /** Renders one option, told whether it is the active one. */
20
+ style: (option: TOption, active: boolean) => string;
21
+ /** The most options to show, before the terminal's own height is taken into account. */
22
+ maxItems?: number | undefined;
23
+ /** Columns taken by something else on the line — a bar, an indent. */
24
+ columnPadding?: number | undefined;
25
+ /** Rows taken by something else on the screen — a message, a footer. */
26
+ rowPadding?: number | undefined;
27
+ }
28
+ /**
29
+ * The window of options a list should draw, as lines, with `...` where it was cut.
30
+ *
31
+ * The order of the four decisions is the behaviour, and every one of them is a graded case:
32
+ *
33
+ * 1. **How many options may show at all** — the smaller of `maxItems` and the rows left
34
+ * after `rowPadding`, but never fewer than five. "clamps to 5 rows minimum" is that
35
+ * floor, and it is why a list in a seven-row terminal still shows something.
36
+ * 2. **Where the window starts** — it only slides once the cursor comes within three of the
37
+ * bottom, and it stops at the end of the list rather than running past it.
38
+ * 3. **Which ends get an ellipsis** — each `...` costs a line, so it is counted before the
39
+ * options are, not after.
40
+ * 4. **What to give up when the options wrapped** — options are dropped whole, away from the
41
+ * cursor first, and an ellipsis appears wherever something was dropped. The three
42
+ * "multi-line item clamping" cases are start, middle and end of that.
43
+ */
44
+ export declare function limitOptions<TOption>({ cursor, options, style, output, maxItems, columnPadding, rowPadding, }: LimitOptionsParams<TOption>): string[];
package/dist/clack.js ADDED
@@ -0,0 +1,97 @@
1
+ import { styleText } from 'node:util';
2
+ import { wrap } from 'linegauge/wrap';
3
+ import { processRuntime } from './runtime.js';
4
+ const DEFAULT_COLUMNS = 80;
5
+ const DEFAULT_ROWS = 20;
6
+ const DEFAULT_ROW_PADDING = 4;
7
+ const MINIMUM_VISIBLE = 5;
8
+ const SCROLL_MARGIN = 3;
9
+ const columnsOf = (output) => (typeof output.columns === 'number' ? output.columns : DEFAULT_COLUMNS);
10
+ const rowsOf = (output) => (typeof output.rows === 'number' ? output.rows : DEFAULT_ROWS);
11
+ function shrink({ rendered, lines, from, to, budget, fromEnd = false }) {
12
+ let lineCount = lines;
13
+ let removals = 0;
14
+ const order = fromEnd ? [...Array.from({ length: Math.max(to - from, 0) }, (_unused, n) => to - 1 - n)] : [...Array.from({ length: Math.max(to - from, 0) }, (_unused, n) => from + n)];
15
+ for (const i of order) {
16
+ lineCount -= rendered[i]?.length ?? 0;
17
+ removals++;
18
+ if (lineCount <= budget)
19
+ break;
20
+ }
21
+ return { lineCount, removals };
22
+ }
23
+ function draw({ options, style, cursor, from, to, columns }) {
24
+ const rendered = [];
25
+ for (let i = from; i < to; i++) {
26
+ const option = options[i];
27
+ const styled = option === undefined ? '' : style(option, i === cursor);
28
+ rendered.push(wrap(styled, columns, { hard: true, trim: false }).split('\n'));
29
+ }
30
+ return rendered;
31
+ }
32
+ function fitToScreen(fit) {
33
+ const { rendered, cursorIndex } = fit;
34
+ let remaining = fit.lineCount;
35
+ let budget = fit.budget;
36
+ let ellipsisAbove = fit.ellipsisAbove;
37
+ let ellipsisBelow = fit.ellipsisBelow;
38
+ let removedAbove = 0;
39
+ let removedBelow = 0;
40
+ const above = () => shrink({ rendered, lines: remaining, from: 0, to: cursorIndex, budget });
41
+ const below = () => shrink({ rendered, lines: remaining, from: cursorIndex + 1, to: rendered.length, budget, fromEnd: true });
42
+ const fromAbove = () => {
43
+ ({ lineCount: remaining, removals: removedAbove } = above());
44
+ if (remaining <= budget)
45
+ return;
46
+ if (!ellipsisBelow)
47
+ budget -= 1;
48
+ ({ lineCount: remaining, removals: removedBelow } = below());
49
+ };
50
+ const fromBelow = () => {
51
+ if (!ellipsisBelow)
52
+ budget -= 1;
53
+ ({ lineCount: remaining, removals: removedBelow } = below());
54
+ if (remaining <= budget)
55
+ return;
56
+ budget -= 1;
57
+ ({ lineCount: remaining, removals: removedAbove } = above());
58
+ };
59
+ (ellipsisAbove ? fromAbove : fromBelow)();
60
+ if (removedAbove > 0) {
61
+ ellipsisAbove = true;
62
+ rendered.splice(0, removedAbove);
63
+ }
64
+ if (removedBelow > 0) {
65
+ ellipsisBelow = true;
66
+ rendered.splice(rendered.length - removedBelow, removedBelow);
67
+ }
68
+ return { ellipsisAbove, ellipsisBelow };
69
+ }
70
+ export function limitOptions({ cursor, options, style, output = processRuntime().stdout, maxItems = Number.POSITIVE_INFINITY, columnPadding = 0, rowPadding = DEFAULT_ROW_PADDING, }) {
71
+ const columns = columnsOf(output) - columnPadding;
72
+ const ellipsis = styleText('dim', '...');
73
+ const availableRows = Math.max(rowsOf(output) - rowPadding, 0);
74
+ const visible = Math.max(Math.min(maxItems, availableRows), MINIMUM_VISIBLE);
75
+ let windowStart = 0;
76
+ if (cursor >= visible - SCROLL_MARGIN)
77
+ windowStart = Math.max(Math.min(cursor - visible + SCROLL_MARGIN, options.length - visible), 0);
78
+ let ellipsisAbove = visible < options.length && windowStart > 0;
79
+ let ellipsisBelow = visible < options.length && windowStart + visible < options.length;
80
+ const windowEnd = Math.min(windowStart + visible, options.length);
81
+ const first = windowStart + (ellipsisAbove ? 1 : 0);
82
+ const last = windowEnd - (ellipsisBelow ? 1 : 0);
83
+ const rendered = draw({ options, style, cursor, from: first, to: last, columns });
84
+ let lineCount = (ellipsisAbove ? 1 : 0) + (ellipsisBelow ? 1 : 0) + rendered.reduce((total, lines) => total + lines.length, 0);
85
+ if (lineCount > availableRows) {
86
+ ({ ellipsisAbove, ellipsisBelow } = fitToScreen({ rendered, lineCount, cursorIndex: cursor - first, budget: availableRows, ellipsisAbove, ellipsisBelow }));
87
+ }
88
+ const out = [];
89
+ if (ellipsisAbove)
90
+ out.push(ellipsis);
91
+ for (const lines of rendered)
92
+ for (const line of lines)
93
+ out.push(line);
94
+ if (ellipsisBelow)
95
+ out.push(ellipsis);
96
+ return out;
97
+ }
package/dist/decide.js CHANGED
@@ -1,40 +1,10 @@
1
- /**
2
- * Whether to prompt at all (R2, R3, R6) — the one decision this package exists to get
3
- * right, and the only one that has to be correct before a single character is drawn.
4
- *
5
- * **An agent asked to type is an agent that hangs.** Every prompt library assumes a person
6
- * is there; when one is not, the process waits on a stdin that will never produce a line,
7
- * and the caller sees a timeout with no output and no clue. That is the failure this
8
- * function refuses to allow: with no terminal, a missing value is an *error naming the
9
- * flag*, immediately, in words the caller can act on.
10
- *
11
- * It is pure — a value, a runtime slice and the flags in, a verdict out — so the whole
12
- * truth table is a unit test rather than a PTY. `decide.test.ts` walks all of it.
13
- */
14
1
  import { flagOf } from './spec.js';
15
2
  const SKIP = { action: 'skip' };
16
3
  const PROMPT = { action: 'prompt' };
17
- /** Present and not empty — the same convention roundel's policy applies to every switch. */
18
4
  const set = (value) => value !== undefined && value !== '';
19
- /** A value from any source at all: a flag, an env var, a config file, a default. */
20
5
  function alreadyAnswered(value) {
21
- // `false` and `0` and `''` are answers. Only "nothing was supplied" is not.
22
6
  return value !== undefined && value !== null;
23
7
  }
24
- /**
25
- * The rule, in order, and the order is the argument:
26
- *
27
- * 1. A value from any source wins. Prompting for something already answered is how a
28
- * script that sets an env var still ends up waiting for input.
29
- * 2. `--json` never prompts. It means "a machine is reading this", and there is no
30
- * answer a machine can type.
31
- * 3. `--yes` answers a `confirm`, and only a `confirm` — it is not a licence to invent
32
- * a path or a password.
33
- * 4. No terminal on stdin, or `CI` set, means nobody is there: refuse, naming the flag.
34
- * 5. `--interactive` reaches past 4 only when there *is* a terminal; it is an override
35
- * for "you would not have asked", never for "there is no one to ask".
36
- * 6. Otherwise, if it is required or `--interactive` was asked for, prompt.
37
- */
38
8
  export function decide({ value, spec, option, runtime, flags = {}, required = false }) {
39
9
  if (alreadyAnswered(value))
40
10
  return SKIP;
@@ -52,8 +22,6 @@ export function decide({ value, spec, option, runtime, flags = {}, required = fa
52
22
  return { action: 'answer', value: true };
53
23
  const nobodyThere = !runtime.isTTY.stdin || set(runtime.env['CI']);
54
24
  if (nobodyThere) {
55
- // `--interactive` cannot conjure a person. Saying so is the difference between a
56
- // useful refusal and a flag that looks like it did nothing.
57
25
  const because = interactive ? ' (--interactive needs a terminal on stdin)' : '';
58
26
  return {
59
27
  action: 'error',
@@ -66,4 +34,3 @@ export function decide({ value, spec, option, runtime, flags = {}, required = fa
66
34
  return PROMPT;
67
35
  return SKIP;
68
36
  }
69
- //# sourceMappingURL=decide.js.map
package/dist/index.d.ts CHANGED
@@ -10,3 +10,9 @@ export * from './decide.js';
10
10
  export * from './raw.js';
11
11
  export * from './spec.js';
12
12
  export * from './terminal.js';
13
+ /**
14
+ * Named, not starred: `runtime.ts` and `decide.ts` both call their slice `Runtime`, and two
15
+ * star exports of one name is an ambiguity TypeScript resolves by dropping it. The root
16
+ * keeps `decide`'s — the one a caller of `decide()` constructs — and takes the function.
17
+ */
18
+ export { processRuntime } from './runtime.js';
package/dist/index.js CHANGED
@@ -1,13 +1,7 @@
1
- /**
2
- * caique — prompts that are flags first. The first working slice is `decide()`: the pure
3
- * rule that decides whether a person can be asked at all, which is what keeps a CLI built
4
- * on this from hanging when an agent runs it. Widgets and the two façades follow; see
5
- * `.sdlc/intents/caique/`.
6
- */
7
1
  export * from './ask.js';
8
2
  export * from './binding.js';
9
3
  export * from './decide.js';
10
4
  export * from './raw.js';
11
5
  export * from './spec.js';
12
6
  export * from './terminal.js';
13
- //# sourceMappingURL=index.js.map
7
+ export { processRuntime } from './runtime.js';
@@ -0,0 +1,38 @@
1
+ /**
2
+ * `@inquirer/core`'s five error classes, re-stated.
3
+ *
4
+ * They are part of the façade's *identity*, not its decoration: `core.test.ts` asserts
5
+ * `rejects.toThrow(AbortPromptError)` and `rejects.toBeInstanceOf(ValidationError)`, so a
6
+ * caller that catches by class has to catch ours by the same class. Each carries the name
7
+ * and the default message the incumbent gives it, because `toThrowErrorMatchingInlineSnapshot`
8
+ * prints `[HookError: …]` — the class name and the message, together.
9
+ *
10
+ * caique's own error envelope (`CliError`, `CANCELLED`, `USAGE`) is unchanged and lives in
11
+ * `binding.ts`. This file is the incumbent's vocabulary, spoken at the compatibility subpath
12
+ * and nowhere else.
13
+ */
14
+ /** Thrown when the `AbortSignal` handed to a prompt aborts. */
15
+ export declare class AbortPromptError extends Error {
16
+ name: string;
17
+ message: string;
18
+ constructor(options?: {
19
+ cause?: unknown;
20
+ });
21
+ }
22
+ /** Thrown when a caller calls `.cancel()` on the returned promise. */
23
+ export declare class CancelPromptError extends Error {
24
+ name: string;
25
+ message: string;
26
+ }
27
+ /** Thrown when the process is going away under the prompt — Ctrl-C, or a signalled exit. */
28
+ export declare class ExitPromptError extends Error {
29
+ name: string;
30
+ }
31
+ /** Thrown when a hook is called outside a prompt render, where there is no store to read. */
32
+ export declare class HookError extends Error {
33
+ name: string;
34
+ }
35
+ /** Thrown when a hook is called correctly but handed something it cannot use. */
36
+ export declare class ValidationError extends Error {
37
+ name: string;
38
+ }
@@ -0,0 +1,21 @@
1
+ export class AbortPromptError extends Error {
2
+ name = 'AbortPromptError';
3
+ message = 'Prompt was aborted';
4
+ constructor(options) {
5
+ super();
6
+ this.cause = options?.cause;
7
+ }
8
+ }
9
+ export class CancelPromptError extends Error {
10
+ name = 'CancelPromptError';
11
+ message = 'Prompt was canceled';
12
+ }
13
+ export class ExitPromptError extends Error {
14
+ name = 'ExitPromptError';
15
+ }
16
+ export class HookError extends Error {
17
+ name = 'HookError';
18
+ }
19
+ export class ValidationError extends Error {
20
+ name = 'ValidationError';
21
+ }
@@ -0,0 +1,86 @@
1
+ import { type Interface } from 'node:readline';
2
+ /** The readline interface a prompt renders over. The engine only ever reads `input`. */
3
+ export type PromptReadline = Interface & {
4
+ input: NodeJS.ReadableStream;
5
+ output: NodeJS.WritableStream;
6
+ };
7
+ /** A cleanup returned by an effect, or nothing. */
8
+ type Cleanup = (() => void) | undefined | void;
9
+ /**
10
+ * Run `cb` with a fresh hook store bound to `rl`.
11
+ *
12
+ * `cb` is handed a `cycle` function rather than being called after one: the store's
13
+ * `handleChange` has to *be* "render again", and only the caller knows how to render.
14
+ */
15
+ export declare function withHooks<T>(rl: PromptReadline, cb: (cycle: (render: () => void) => void) => T): T;
16
+ /** The readline interface of the prompt currently rendering. Throws outside one. */
17
+ export declare function readline(): PromptReadline;
18
+ /**
19
+ * Wrap a function so every state change it makes collapses into a single re-render, and so
20
+ * it stays reachable from whatever async context later calls it.
21
+ */
22
+ export declare function withUpdates<Args extends unknown[], R>(fn: (...args: Args) => R): (...args: Args) => R;
23
+ /** One hook's slot in the store, addressed by the order the render function called it in. */
24
+ export interface Pointer<T> {
25
+ get: () => T;
26
+ set: (value: T) => void;
27
+ initialized: boolean;
28
+ }
29
+ /** Take the next hook slot, hand it to `cb`, and advance the index. */
30
+ export declare function withPointer<T, R>(cb: (pointer: Pointer<T>) => R): R;
31
+ /** Ask for a re-render. */
32
+ export declare function handleChange(): void;
33
+ /**
34
+ * The effect queue.
35
+ *
36
+ * Effects never run during the render that queued them — "useEffect: is not called
37
+ * synchronously during render" is a case — so they are collected and flushed by the loop
38
+ * after `screen.render()`. `clearAll` is what a settling prompt calls, and it runs every
39
+ * cleanup exactly once.
40
+ */
41
+ export declare const effectScheduler: {
42
+ /** Queue `cb` to run after this render, cleaning up whatever the same slot left behind. */
43
+ queue(cb: (rl: PromptReadline) => Cleanup): void;
44
+ /** Flush the queue, coalescing every state change the effects make into one re-render. */
45
+ run(): void;
46
+ /** Run every cleanup and forget both lists. Idempotent, because settling can race. */
47
+ clearAll(): void;
48
+ };
49
+ /** A state value that is not itself a function, so the setter can tell a reducer apart. */
50
+ type NotFunction<T> = T extends (...args: never) => unknown ? never : T;
51
+ /** The setter `useState` returns: a new value, or a reducer over the current one. */
52
+ export type SetState<Value> = (newValue: NotFunction<Value> | ((current: Value) => Value)) => void;
53
+ /**
54
+ * State that survives a re-render, addressed by call order.
55
+ *
56
+ * The setter accepts a value or a reducer, and does nothing at all when the next value is
57
+ * `Object.is`-equal to the current one — which is why `setValue(NaN)` on a `NaN` state does
58
+ * not re-render, and why a reducer returning its argument is free.
59
+ */
60
+ export declare function useState<Value>(defaultValue: NotFunction<Value> | (() => Value)): [Value, SetState<Value>];
61
+ /** The no-argument form, for state a later keypress fills. */
62
+ export declare function useState<Value>(defaultValue?: NotFunction<Value> | (() => Value)): [Value | undefined, SetState<Value | undefined>];
63
+ /**
64
+ * A side effect, queued when its dependency array changes and cleaned up before it re-runs.
65
+ *
66
+ * The comparison is `Object.is` per element, like the incumbent's, so a `useRef` handed back
67
+ * as a dependency is stable and an object literal is not.
68
+ */
69
+ export declare function useEffect(cb: (rl: PromptReadline) => Cleanup, depArray: readonly unknown[]): void;
70
+ /**
71
+ * A memoised value.
72
+ *
73
+ * Compared with `!==` rather than `Object.is`, and by length first, exactly as upstream
74
+ * does it — a drop-in that quietly tightened the comparison would recompute where the
75
+ * incumbent does not, and the graded case counts the calls.
76
+ */
77
+ export declare function useMemo<Value>(fn: () => Value, dependencies: readonly unknown[]): Value;
78
+ /** A box that survives re-renders. One `useState` slot holding an object that never changes. */
79
+ export declare function useRef<Value>(value: Value): {
80
+ current: Value;
81
+ };
82
+ /** The no-argument form, for a box a later render fills — `useRef<Timeout | undefined>()`. */
83
+ export declare function useRef<Value>(value?: Value): {
84
+ current: Value | undefined;
85
+ };
86
+ export {};