caique 0.2.0 → 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 +3 -5
- 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.js +0 -12
- 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.js +0 -64
- package/dist/raw.js +0 -60
- package/dist/runtime.js +0 -12
- package/dist/schema.json +1 -300
- package/dist/spec.js +0 -26
- package/dist/terminal.js +0 -36
- package/package.json +14 -3
package/README.md
CHANGED
|
@@ -255,14 +255,12 @@ Graded by the incumbent's own test suite:
|
|
|
255
255
|
|
|
256
256
|
| suite | passing |
|
|
257
257
|
| :-- | --: |
|
|
258
|
-
| `clack` |
|
|
259
|
-
| `inquirer-core` |
|
|
258
|
+
| `clack` | 14 / 17 |
|
|
259
|
+
| `inquirer-core` | 41 / 41 |
|
|
260
260
|
|
|
261
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
|
-
|
|
263
|
-
That ratio is not yet a claim: nothing here passes an incumbent suite, so it is the weight of a package that does not do the job.
|
|
264
262
|
## Where it sits
|
|
265
263
|
|
|
266
264
|
Plugins register under the `widgets` key, against the one schema the whole family shares.
|
|
267
265
|
|
|
268
|
-
Nothing in this family builds on it yet, and it builds on `closeout`.
|
|
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.js
CHANGED
|
@@ -1,19 +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
|
-
/**
|
|
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
7
|
export { processRuntime } from './runtime.js';
|
|
19
|
-
//# sourceMappingURL=index.js.map
|
|
@@ -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 {};
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
import { AsyncLocalStorage, AsyncResource } from 'node:async_hooks';
|
|
2
|
+
import { HookError, ValidationError } from './inquirer-errors.js';
|
|
3
|
+
const hookStorage = new AsyncLocalStorage();
|
|
4
|
+
export function withHooks(rl, cb) {
|
|
5
|
+
const store = { rl, hooks: [], hooksCleanup: [], hooksEffect: [], index: 0, handleChange: () => undefined };
|
|
6
|
+
return hookStorage.run(store, () => cb((render) => {
|
|
7
|
+
store.handleChange = () => {
|
|
8
|
+
store.index = 0;
|
|
9
|
+
render();
|
|
10
|
+
};
|
|
11
|
+
store.handleChange();
|
|
12
|
+
}));
|
|
13
|
+
}
|
|
14
|
+
function getStore() {
|
|
15
|
+
const store = hookStorage.getStore();
|
|
16
|
+
if (store === undefined)
|
|
17
|
+
throw new HookError('[Inquirer] Hook functions can only be called from within a prompt');
|
|
18
|
+
return store;
|
|
19
|
+
}
|
|
20
|
+
export function readline() {
|
|
21
|
+
return getStore().rl;
|
|
22
|
+
}
|
|
23
|
+
export function withUpdates(fn) {
|
|
24
|
+
const wrapped = (...args) => {
|
|
25
|
+
const store = getStore();
|
|
26
|
+
let shouldUpdate = false;
|
|
27
|
+
const previous = store.handleChange;
|
|
28
|
+
store.handleChange = () => {
|
|
29
|
+
shouldUpdate = true;
|
|
30
|
+
};
|
|
31
|
+
const returnValue = fn(...args);
|
|
32
|
+
if (shouldUpdate)
|
|
33
|
+
previous();
|
|
34
|
+
store.handleChange = previous;
|
|
35
|
+
return returnValue;
|
|
36
|
+
};
|
|
37
|
+
return AsyncResource.bind(wrapped);
|
|
38
|
+
}
|
|
39
|
+
export function withPointer(cb) {
|
|
40
|
+
const store = getStore();
|
|
41
|
+
const { index } = store;
|
|
42
|
+
const pointer = {
|
|
43
|
+
get: () => store.hooks[index],
|
|
44
|
+
set: (value) => {
|
|
45
|
+
store.hooks[index] = value;
|
|
46
|
+
},
|
|
47
|
+
initialized: index in store.hooks,
|
|
48
|
+
};
|
|
49
|
+
const returnValue = cb(pointer);
|
|
50
|
+
store.index++;
|
|
51
|
+
return returnValue;
|
|
52
|
+
}
|
|
53
|
+
export function handleChange() {
|
|
54
|
+
getStore().handleChange();
|
|
55
|
+
}
|
|
56
|
+
export const effectScheduler = {
|
|
57
|
+
queue(cb) {
|
|
58
|
+
const store = getStore();
|
|
59
|
+
const { index } = store;
|
|
60
|
+
store.hooksEffect.push(() => {
|
|
61
|
+
store.hooksCleanup[index]?.();
|
|
62
|
+
const cleanup = cb(readline());
|
|
63
|
+
if (cleanup != null && typeof cleanup !== 'function')
|
|
64
|
+
throw new ValidationError('useEffect return value must be a cleanup function or nothing.');
|
|
65
|
+
store.hooksCleanup[index] = cleanup;
|
|
66
|
+
});
|
|
67
|
+
},
|
|
68
|
+
run() {
|
|
69
|
+
const store = getStore();
|
|
70
|
+
withUpdates(() => {
|
|
71
|
+
for (const effect of store.hooksEffect)
|
|
72
|
+
effect();
|
|
73
|
+
store.hooksEffect.length = 0;
|
|
74
|
+
})();
|
|
75
|
+
},
|
|
76
|
+
clearAll() {
|
|
77
|
+
const store = getStore();
|
|
78
|
+
for (const cleanup of store.hooksCleanup)
|
|
79
|
+
cleanup?.();
|
|
80
|
+
store.hooksEffect.length = 0;
|
|
81
|
+
store.hooksCleanup.length = 0;
|
|
82
|
+
},
|
|
83
|
+
};
|
|
84
|
+
const isFunction = (value) => typeof value === 'function';
|
|
85
|
+
export function useState(defaultValue) {
|
|
86
|
+
return withPointer((pointer) => {
|
|
87
|
+
const setState = AsyncResource.bind(function setState(newValue) {
|
|
88
|
+
const currentValue = pointer.get();
|
|
89
|
+
const nextValue = isFunction(newValue) ? newValue(currentValue) : newValue;
|
|
90
|
+
if (Object.is(currentValue, nextValue))
|
|
91
|
+
return;
|
|
92
|
+
pointer.set(nextValue);
|
|
93
|
+
handleChange();
|
|
94
|
+
});
|
|
95
|
+
if (pointer.initialized)
|
|
96
|
+
return [pointer.get(), setState];
|
|
97
|
+
const value = isFunction(defaultValue) ? defaultValue() : defaultValue;
|
|
98
|
+
pointer.set(value);
|
|
99
|
+
return [value, setState];
|
|
100
|
+
});
|
|
101
|
+
}
|
|
102
|
+
export function useEffect(cb, depArray) {
|
|
103
|
+
withPointer((pointer) => {
|
|
104
|
+
const oldDeps = pointer.get();
|
|
105
|
+
const hasChanged = !Array.isArray(oldDeps) || depArray.some((dep, i) => !Object.is(dep, oldDeps[i]));
|
|
106
|
+
if (hasChanged)
|
|
107
|
+
effectScheduler.queue(cb);
|
|
108
|
+
pointer.set(depArray);
|
|
109
|
+
});
|
|
110
|
+
}
|
|
111
|
+
export function useMemo(fn, dependencies) {
|
|
112
|
+
return withPointer((pointer) => {
|
|
113
|
+
const previous = pointer.get();
|
|
114
|
+
if (previous === undefined || !pointer.initialized || previous.dependencies.length !== dependencies.length || previous.dependencies.some((dep, i) => dep !== dependencies[i])) {
|
|
115
|
+
const value = fn();
|
|
116
|
+
pointer.set({ value, dependencies });
|
|
117
|
+
return value;
|
|
118
|
+
}
|
|
119
|
+
return previous.value;
|
|
120
|
+
});
|
|
121
|
+
}
|
|
122
|
+
export function useRef(value) {
|
|
123
|
+
return useState({ current: value })[0];
|
|
124
|
+
}
|