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 +62 -9
- package/dist/ask.js +0 -46
- package/dist/binding.js +0 -30
- package/dist/clack.d.ts +44 -0
- package/dist/clack.js +97 -0
- package/dist/decide.js +0 -33
- package/dist/index.d.ts +6 -0
- package/dist/index.js +1 -7
- package/dist/inquirer-errors.d.ts +38 -0
- package/dist/inquirer-errors.js +21 -0
- package/dist/inquirer-hooks.d.ts +86 -0
- package/dist/inquirer-hooks.js +124 -0
- package/dist/inquirer-keys.d.ts +31 -0
- package/dist/inquirer-keys.js +22 -0
- package/dist/inquirer-screen.d.ts +131 -0
- package/dist/inquirer-screen.js +122 -0
- package/dist/inquirer-theme.d.ts +58 -0
- package/dist/inquirer-theme.js +51 -0
- package/dist/inquirer.d.ts +72 -0
- package/dist/inquirer.js +207 -0
- package/dist/plugin.d.ts +81 -0
- package/dist/plugin.js +99 -0
- package/dist/raw.d.ts +3 -17
- package/dist/raw.js +7 -50
- package/dist/runtime.d.ts +31 -0
- package/dist/runtime.js +6 -0
- package/dist/schema.json +1 -0
- package/dist/spec.d.ts +23 -2
- package/dist/spec.js +1 -18
- package/dist/terminal.d.ts +8 -2
- package/dist/terminal.js +3 -33
- package/package.json +22 -2
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/
|
|
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
|
|
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:
|
|
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(
|
|
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
|
|
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
|
-
- **
|
|
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.
|
|
211
|
-
|
|
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
|
package/dist/clack.d.ts
ADDED
|
@@ -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
|
-
|
|
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 {};
|