@velum-labs/routekit-cli-ui 0.9.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/LICENSE +201 -0
- package/README.md +20 -0
- package/dist/format.d.ts +27 -0
- package/dist/format.js +112 -0
- package/dist/fuzzy.d.ts +23 -0
- package/dist/fuzzy.js +61 -0
- package/dist/index.d.ts +21 -0
- package/dist/index.js +29 -0
- package/dist/ink/components.d.ts +49 -0
- package/dist/ink/components.js +116 -0
- package/dist/ink/presenter.d.ts +31 -0
- package/dist/ink/presenter.js +250 -0
- package/dist/ink/prompts.d.ts +73 -0
- package/dist/ink/prompts.js +338 -0
- package/dist/ink/store.d.ts +13 -0
- package/dist/ink/store.js +22 -0
- package/dist/plain.d.ts +36 -0
- package/dist/plain.js +291 -0
- package/dist/presenter.d.ts +144 -0
- package/dist/presenter.js +124 -0
- package/dist/prompt.d.ts +95 -0
- package/dist/prompt.js +246 -0
- package/dist/runtime.d.ts +20 -0
- package/dist/runtime.js +47 -0
- package/dist/test/cli-ui.test.d.ts +1 -0
- package/dist/test/cli-ui.test.js +230 -0
- package/dist/test/ink.test.d.ts +1 -0
- package/dist/test/ink.test.js +96 -0
- package/dist/theme.d.ts +82 -0
- package/dist/theme.js +299 -0
- package/dist/wizard.d.ts +13 -0
- package/dist/wizard.js +54 -0
- package/package.json +50 -0
|
@@ -0,0 +1,338 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
/**
|
|
3
|
+
* Ink prompt components: select, multi-select, confirm, and free text. Each
|
|
4
|
+
* renders live while the user decides, then settles into a single answer line
|
|
5
|
+
* (message + chosen value) that persists after unmount — so a finished wizard
|
|
6
|
+
* reads as a tidy transcript of the choices made.
|
|
7
|
+
*
|
|
8
|
+
* Ctrl+C aborts via `onAbort` (the facade exits 130, matching the CLI's
|
|
9
|
+
* SIGINT convention).
|
|
10
|
+
*/
|
|
11
|
+
import { Box, Text, useInput } from "ink";
|
|
12
|
+
import { useState, useSyncExternalStore } from "react";
|
|
13
|
+
import { fuzzyFilter } from "../fuzzy.js";
|
|
14
|
+
import { glyph } from "../theme.js";
|
|
15
|
+
import { useSpinnerFrame } from "./components.js";
|
|
16
|
+
function AnswerLine({ message, answer }) {
|
|
17
|
+
return (_jsxs(Text, { children: [_jsx(Text, { color: "green", children: glyph.tick() }), " ", _jsx(Text, { bold: true, children: message }), " ", _jsxs(Text, { dimColor: true, children: ["\u00B7 ", answer] })] }));
|
|
18
|
+
}
|
|
19
|
+
/** How many options to show around the cursor before scrolling the window. */
|
|
20
|
+
const WINDOW = 10;
|
|
21
|
+
function windowBounds(cursor, total) {
|
|
22
|
+
if (total <= WINDOW)
|
|
23
|
+
return { start: 0, end: total };
|
|
24
|
+
const start = Math.max(0, Math.min(cursor - Math.floor(WINDOW / 2), total - WINDOW));
|
|
25
|
+
return { start, end: start + WINDOW };
|
|
26
|
+
}
|
|
27
|
+
export function SelectPrompt({ message, options, defaultIndex, onSubmit, onAbort, onBack }) {
|
|
28
|
+
const [cursor, setCursor] = useState(defaultIndex);
|
|
29
|
+
const [answer, setAnswer] = useState(undefined);
|
|
30
|
+
useInput((input, key) => {
|
|
31
|
+
if (answer !== undefined)
|
|
32
|
+
return;
|
|
33
|
+
if (key.ctrl && input === "c") {
|
|
34
|
+
onAbort();
|
|
35
|
+
return;
|
|
36
|
+
}
|
|
37
|
+
if (key.escape && onBack !== undefined) {
|
|
38
|
+
onBack();
|
|
39
|
+
return;
|
|
40
|
+
}
|
|
41
|
+
if (key.upArrow || input === "k") {
|
|
42
|
+
setCursor((previous) => (previous - 1 + options.length) % options.length);
|
|
43
|
+
}
|
|
44
|
+
else if (key.downArrow || input === "j") {
|
|
45
|
+
setCursor((previous) => (previous + 1) % options.length);
|
|
46
|
+
}
|
|
47
|
+
else if (key.return) {
|
|
48
|
+
const option = options[cursor];
|
|
49
|
+
if (option !== undefined) {
|
|
50
|
+
setAnswer(option.label);
|
|
51
|
+
onSubmit(option.value, option.label);
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
});
|
|
55
|
+
if (answer !== undefined)
|
|
56
|
+
return _jsx(AnswerLine, { message: message, answer: answer });
|
|
57
|
+
const { start, end } = windowBounds(cursor, options.length);
|
|
58
|
+
return (_jsxs(Box, { flexDirection: "column", children: [_jsx(Text, { bold: true, children: message }), start > 0 ? _jsxs(Text, { dimColor: true, children: [" \u2026 ", start, " more above"] }) : null, options.slice(start, end).map((option, offset) => {
|
|
59
|
+
const index = start + offset;
|
|
60
|
+
const active = index === cursor;
|
|
61
|
+
return (_jsxs(Text, { children: [_jsx(Text, { color: "cyan", children: active ? glyph.pointer() : " " }), " ", active ? _jsx(Text, { color: "cyan", children: option.label }) : _jsx(Text, { children: option.label }), option.hint !== undefined ? _jsxs(Text, { dimColor: true, children: [" \u2014 ", option.hint] }) : null] }, index));
|
|
62
|
+
}), end < options.length ? _jsxs(Text, { dimColor: true, children: [" \u2026 ", options.length - end, " more below"] }) : null, _jsx(Text, { dimColor: true, children: " (arrows to move, enter to select)" })] }));
|
|
63
|
+
}
|
|
64
|
+
export function MultiSelectPrompt({ message, options, defaultSelected, onSubmit, onAbort }) {
|
|
65
|
+
const [cursor, setCursor] = useState(0);
|
|
66
|
+
const [selected, setSelected] = useState(new Set(defaultSelected));
|
|
67
|
+
const [answer, setAnswer] = useState(undefined);
|
|
68
|
+
useInput((input, key) => {
|
|
69
|
+
if (answer !== undefined)
|
|
70
|
+
return;
|
|
71
|
+
if (key.ctrl && input === "c") {
|
|
72
|
+
onAbort();
|
|
73
|
+
return;
|
|
74
|
+
}
|
|
75
|
+
if (key.upArrow || input === "k") {
|
|
76
|
+
setCursor((previous) => (previous - 1 + options.length) % options.length);
|
|
77
|
+
}
|
|
78
|
+
else if (key.downArrow || input === "j") {
|
|
79
|
+
setCursor((previous) => (previous + 1) % options.length);
|
|
80
|
+
}
|
|
81
|
+
else if (input === " ") {
|
|
82
|
+
setSelected((previous) => {
|
|
83
|
+
const next = new Set(previous);
|
|
84
|
+
if (next.has(cursor))
|
|
85
|
+
next.delete(cursor);
|
|
86
|
+
else
|
|
87
|
+
next.add(cursor);
|
|
88
|
+
return next;
|
|
89
|
+
});
|
|
90
|
+
}
|
|
91
|
+
else if (input === "a") {
|
|
92
|
+
setSelected((previous) => previous.size === options.length ? new Set() : new Set(options.map((_, index) => index)));
|
|
93
|
+
}
|
|
94
|
+
else if (key.return) {
|
|
95
|
+
const indices = [...selected].sort((left, right) => left - right);
|
|
96
|
+
const chosen = indices
|
|
97
|
+
.map((index) => options[index])
|
|
98
|
+
.filter((option) => option !== undefined);
|
|
99
|
+
const labels = chosen.map((option) => option.label);
|
|
100
|
+
setAnswer(labels.length > 0 ? labels.join(", ") : "(none)");
|
|
101
|
+
onSubmit(chosen.map((option) => option.value), labels);
|
|
102
|
+
}
|
|
103
|
+
});
|
|
104
|
+
if (answer !== undefined)
|
|
105
|
+
return _jsx(AnswerLine, { message: message, answer: answer });
|
|
106
|
+
const { start, end } = windowBounds(cursor, options.length);
|
|
107
|
+
return (_jsxs(Box, { flexDirection: "column", children: [_jsx(Text, { bold: true, children: message }), start > 0 ? _jsxs(Text, { dimColor: true, children: [" \u2026 ", start, " more above"] }) : null, options.slice(start, end).map((option, offset) => {
|
|
108
|
+
const index = start + offset;
|
|
109
|
+
const active = index === cursor;
|
|
110
|
+
const checked = selected.has(index);
|
|
111
|
+
return (_jsxs(Text, { children: [_jsx(Text, { color: "cyan", children: active ? glyph.pointer() : " " }), " ", _jsx(Text, { color: checked ? "green" : undefined, children: checked ? glyph.checkboxOn() : glyph.checkboxOff() }), " ", active ? _jsx(Text, { color: "cyan", children: option.label }) : _jsx(Text, { children: option.label }), option.hint !== undefined ? _jsxs(Text, { dimColor: true, children: [" \u2014 ", option.hint] }) : null] }, index));
|
|
112
|
+
}), end < options.length ? _jsxs(Text, { dimColor: true, children: [" \u2026 ", options.length - end, " more below"] }) : null, _jsx(Text, { dimColor: true, children: " (space to toggle, a for all, enter to accept)" })] }));
|
|
113
|
+
}
|
|
114
|
+
export function ConfirmPrompt({ message, defaultValue, onSubmit, onAbort, onBack }) {
|
|
115
|
+
const [choice, setChoice] = useState(defaultValue);
|
|
116
|
+
const [answer, setAnswer] = useState(undefined);
|
|
117
|
+
useInput((input, key) => {
|
|
118
|
+
if (answer !== undefined)
|
|
119
|
+
return;
|
|
120
|
+
if (key.ctrl && input === "c") {
|
|
121
|
+
onAbort();
|
|
122
|
+
return;
|
|
123
|
+
}
|
|
124
|
+
if (key.escape && onBack !== undefined) {
|
|
125
|
+
onBack();
|
|
126
|
+
return;
|
|
127
|
+
}
|
|
128
|
+
// Batched input ("n\r") answers directly; a bare newline accepts the choice.
|
|
129
|
+
const lowered = input.trim().toLowerCase();
|
|
130
|
+
if (lowered === "y" || lowered.startsWith("y")) {
|
|
131
|
+
setAnswer("yes");
|
|
132
|
+
onSubmit(true);
|
|
133
|
+
}
|
|
134
|
+
else if (lowered === "n" || lowered.startsWith("n")) {
|
|
135
|
+
setAnswer("no");
|
|
136
|
+
onSubmit(false);
|
|
137
|
+
}
|
|
138
|
+
else if (key.leftArrow || key.rightArrow || key.tab) {
|
|
139
|
+
setChoice((previous) => !previous);
|
|
140
|
+
}
|
|
141
|
+
else if (key.return || /[\r\n]/.test(input)) {
|
|
142
|
+
setAnswer(choice ? "yes" : "no");
|
|
143
|
+
onSubmit(choice);
|
|
144
|
+
}
|
|
145
|
+
});
|
|
146
|
+
if (answer !== undefined)
|
|
147
|
+
return _jsx(AnswerLine, { message: message, answer: answer });
|
|
148
|
+
return (_jsxs(Text, { children: [_jsx(Text, { bold: true, children: message }), " ", _jsx(Text, { color: choice ? "cyan" : undefined, bold: choice, children: "yes" }), _jsx(Text, { dimColor: true, children: " / " }), _jsx(Text, { color: choice ? undefined : "cyan", bold: !choice, children: "no" }), _jsx(Text, { dimColor: true, children: " (y/n, arrows to switch, enter to accept)" })] }));
|
|
149
|
+
}
|
|
150
|
+
/** Strip control characters from typed/pasted input. */
|
|
151
|
+
function printable(input) {
|
|
152
|
+
let out = "";
|
|
153
|
+
for (const ch of input) {
|
|
154
|
+
const code = ch.codePointAt(0) ?? 0;
|
|
155
|
+
if (code >= 0x20 && code !== 0x7f)
|
|
156
|
+
out += ch;
|
|
157
|
+
}
|
|
158
|
+
return out;
|
|
159
|
+
}
|
|
160
|
+
export function TextPrompt({ message, defaultValue, placeholder, onSubmit, onAbort, onBack }) {
|
|
161
|
+
const [value, setValue] = useState("");
|
|
162
|
+
const [answer, setAnswer] = useState(undefined);
|
|
163
|
+
useInput((input, key) => {
|
|
164
|
+
if (answer !== undefined)
|
|
165
|
+
return;
|
|
166
|
+
if (key.ctrl && input === "c") {
|
|
167
|
+
onAbort();
|
|
168
|
+
return;
|
|
169
|
+
}
|
|
170
|
+
if (key.escape && onBack !== undefined) {
|
|
171
|
+
onBack();
|
|
172
|
+
return;
|
|
173
|
+
}
|
|
174
|
+
// Rapid/pasted input arrives as one chunk, possibly with an embedded
|
|
175
|
+
// newline: everything before the first newline is typed text, the newline
|
|
176
|
+
// submits (matching what typing the same keys slowly would do).
|
|
177
|
+
const newlineIndex = input.search(/[\r\n]/);
|
|
178
|
+
if (key.return || newlineIndex !== -1) {
|
|
179
|
+
const prefix = newlineIndex === -1 ? printable(input) : printable(input.slice(0, newlineIndex));
|
|
180
|
+
const merged = value + prefix;
|
|
181
|
+
const final = merged.length > 0 ? merged : defaultValue;
|
|
182
|
+
setAnswer(final.length > 0 ? final : "(empty)");
|
|
183
|
+
onSubmit(final);
|
|
184
|
+
return;
|
|
185
|
+
}
|
|
186
|
+
if (key.backspace || key.delete) {
|
|
187
|
+
setValue((previous) => previous.slice(0, -1));
|
|
188
|
+
return;
|
|
189
|
+
}
|
|
190
|
+
// Ignore other control sequences (arrows etc); append printable input.
|
|
191
|
+
if (input.length > 0 && !key.ctrl && !key.meta && !key.upArrow && !key.downArrow && !key.leftArrow && !key.rightArrow && !key.tab) {
|
|
192
|
+
setValue((previous) => previous + printable(input));
|
|
193
|
+
}
|
|
194
|
+
});
|
|
195
|
+
if (answer !== undefined)
|
|
196
|
+
return _jsx(AnswerLine, { message: message, answer: answer });
|
|
197
|
+
const hint = placeholder ?? (defaultValue.length > 0 ? defaultValue : undefined);
|
|
198
|
+
return (_jsxs(Text, { children: [_jsx(Text, { bold: true, children: message }), " ", value.length > 0 ? _jsx(Text, { children: value }) : hint !== undefined ? _jsx(Text, { dimColor: true, children: hint }) : null, _jsx(Text, { color: "cyan", children: "\u2588" })] }));
|
|
199
|
+
}
|
|
200
|
+
/** Render `label` with the fuzzy-matched positions highlighted. */
|
|
201
|
+
function HighlightedLabel({ label, positions, active }) {
|
|
202
|
+
if (positions.length === 0) {
|
|
203
|
+
return active ? _jsx(Text, { color: "cyan", children: label }) : _jsx(Text, { children: label });
|
|
204
|
+
}
|
|
205
|
+
const matched = new Set(positions);
|
|
206
|
+
const parts = [];
|
|
207
|
+
let run = "";
|
|
208
|
+
let runMatched = matched.has(0);
|
|
209
|
+
const flush = (index) => {
|
|
210
|
+
if (run.length === 0)
|
|
211
|
+
return;
|
|
212
|
+
parts.push(runMatched ? (_jsx(Text, { color: "cyan", bold: true, children: run }, index)) : active ? (_jsx(Text, { color: "cyan", children: run }, index)) : (_jsx(Text, { children: run }, index)));
|
|
213
|
+
run = "";
|
|
214
|
+
};
|
|
215
|
+
for (let index = 0; index < label.length; index++) {
|
|
216
|
+
const isMatch = matched.has(index);
|
|
217
|
+
if (isMatch !== runMatched) {
|
|
218
|
+
flush(index);
|
|
219
|
+
runMatched = isMatch;
|
|
220
|
+
}
|
|
221
|
+
run += label[index] ?? "";
|
|
222
|
+
}
|
|
223
|
+
flush(label.length);
|
|
224
|
+
return _jsx(Text, { children: parts });
|
|
225
|
+
}
|
|
226
|
+
export function FuzzySelectPrompt({ message, feed, placeholder, onSubmit, onAbort, onBack }) {
|
|
227
|
+
const state = useSyncExternalStore(feed.subscribe, feed.get, feed.get);
|
|
228
|
+
const [query, setQuery] = useState("");
|
|
229
|
+
const [cursor, setCursor] = useState(0);
|
|
230
|
+
const [answer, setAnswer] = useState(undefined);
|
|
231
|
+
const frame = useSpinnerFrame();
|
|
232
|
+
// Match against the label plus the hint (e.g. the equivalent command), but
|
|
233
|
+
// only highlight positions that fall inside the label itself.
|
|
234
|
+
const results = fuzzyFilter(query, state.options, (option) => option.hint !== undefined ? `${option.label} ${option.hint}` : option.label);
|
|
235
|
+
useInput((input, key) => {
|
|
236
|
+
if (answer !== undefined)
|
|
237
|
+
return;
|
|
238
|
+
if (key.ctrl && input === "c") {
|
|
239
|
+
onAbort();
|
|
240
|
+
return;
|
|
241
|
+
}
|
|
242
|
+
if (key.escape) {
|
|
243
|
+
if (query.length > 0)
|
|
244
|
+
setQuery("");
|
|
245
|
+
else if (onBack !== undefined)
|
|
246
|
+
onBack();
|
|
247
|
+
return;
|
|
248
|
+
}
|
|
249
|
+
if (key.upArrow || (key.ctrl && input === "p")) {
|
|
250
|
+
setCursor((previous) => (results.length === 0 ? 0 : (previous - 1 + results.length) % results.length));
|
|
251
|
+
return;
|
|
252
|
+
}
|
|
253
|
+
if (key.downArrow || (key.ctrl && input === "n")) {
|
|
254
|
+
setCursor((previous) => (results.length === 0 ? 0 : (previous + 1) % results.length));
|
|
255
|
+
return;
|
|
256
|
+
}
|
|
257
|
+
if (key.return) {
|
|
258
|
+
const picked = results[Math.min(cursor, Math.max(0, results.length - 1))];
|
|
259
|
+
if (picked !== undefined) {
|
|
260
|
+
setAnswer(picked.item.label);
|
|
261
|
+
onSubmit(picked.item.value, picked.item.label);
|
|
262
|
+
}
|
|
263
|
+
return;
|
|
264
|
+
}
|
|
265
|
+
if (key.backspace || key.delete) {
|
|
266
|
+
setQuery((previous) => previous.slice(0, -1));
|
|
267
|
+
setCursor(0);
|
|
268
|
+
return;
|
|
269
|
+
}
|
|
270
|
+
if (input.length > 0 && !key.ctrl && !key.meta && !key.tab) {
|
|
271
|
+
setQuery((previous) => previous + printable(input));
|
|
272
|
+
setCursor(0);
|
|
273
|
+
}
|
|
274
|
+
});
|
|
275
|
+
if (answer !== undefined)
|
|
276
|
+
return _jsx(AnswerLine, { message: message, answer: answer });
|
|
277
|
+
const boundedCursor = Math.min(cursor, Math.max(0, results.length - 1));
|
|
278
|
+
const { start, end } = windowBounds(boundedCursor, results.length);
|
|
279
|
+
return (_jsxs(Box, { flexDirection: "column", children: [_jsxs(Text, { children: [_jsx(Text, { bold: true, children: message }), " ", query.length > 0 ? (_jsx(Text, { children: query })) : placeholder !== undefined ? (_jsx(Text, { dimColor: true, children: placeholder })) : null, _jsx(Text, { color: "cyan", children: "\u2588" }), state.loading ? (_jsxs(Text, { dimColor: true, children: [" ", frame, " ", state.note ?? "refreshing…"] })) : null] }), results.length === 0 ? (_jsxs(Text, { dimColor: true, children: [" no matches", query.length > 0 ? ` for "${query}"` : "", " (esc clears)"] })) : null, start > 0 ? _jsxs(Text, { dimColor: true, children: [" \u2026 ", start, " more above"] }) : null, results.slice(start, end).map((result, offset) => {
|
|
280
|
+
const index = start + offset;
|
|
281
|
+
const active = index === boundedCursor;
|
|
282
|
+
return (_jsxs(Text, { children: [_jsx(Text, { color: "cyan", children: active ? glyph.pointer() : " " }), " ", _jsx(HighlightedLabel, { label: result.item.label, positions: result.match.positions.filter((position) => position < result.item.label.length), active: active }), result.item.hint !== undefined ? _jsxs(Text, { dimColor: true, children: [" \u2014 ", result.item.hint] }) : null] }, `${index}-${result.item.label}`));
|
|
283
|
+
}), end < results.length ? _jsxs(Text, { dimColor: true, children: [" \u2026 ", results.length - end, " more below"] }) : null, _jsx(Text, { dimColor: true, children: " (type to filter, arrows to move, enter to select)" })] }));
|
|
284
|
+
}
|
|
285
|
+
/** The inline (ghost) completion for `value`: the remainder of the best prefix match. */
|
|
286
|
+
export function ghostRemainder(value, suggestions) {
|
|
287
|
+
if (value.length === 0)
|
|
288
|
+
return undefined;
|
|
289
|
+
const lower = value.toLowerCase();
|
|
290
|
+
for (const suggestion of suggestions) {
|
|
291
|
+
if (suggestion.toLowerCase().startsWith(lower) && suggestion.length > value.length) {
|
|
292
|
+
return suggestion.slice(value.length);
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
return undefined;
|
|
296
|
+
}
|
|
297
|
+
export function GhostTextPrompt({ message, suggestions, placeholder, defaultValue, onSubmit, onAbort, onBack }) {
|
|
298
|
+
const [value, setValue] = useState("");
|
|
299
|
+
const [answer, setAnswer] = useState(undefined);
|
|
300
|
+
const ghost = ghostRemainder(value, suggestions);
|
|
301
|
+
useInput((input, key) => {
|
|
302
|
+
if (answer !== undefined)
|
|
303
|
+
return;
|
|
304
|
+
if (key.ctrl && input === "c") {
|
|
305
|
+
onAbort();
|
|
306
|
+
return;
|
|
307
|
+
}
|
|
308
|
+
if (key.escape && onBack !== undefined) {
|
|
309
|
+
onBack();
|
|
310
|
+
return;
|
|
311
|
+
}
|
|
312
|
+
if (key.tab || (key.rightArrow && ghost !== undefined)) {
|
|
313
|
+
if (ghost !== undefined)
|
|
314
|
+
setValue((previous) => previous + ghost);
|
|
315
|
+
return;
|
|
316
|
+
}
|
|
317
|
+
const newlineIndex = input.search(/[\r\n]/);
|
|
318
|
+
if (key.return || newlineIndex !== -1) {
|
|
319
|
+
const prefix = newlineIndex === -1 ? printable(input) : printable(input.slice(0, newlineIndex));
|
|
320
|
+
const merged = value + prefix;
|
|
321
|
+
const final = merged.length > 0 ? merged : defaultValue;
|
|
322
|
+
setAnswer(final.length > 0 ? final : "(empty)");
|
|
323
|
+
onSubmit(final);
|
|
324
|
+
return;
|
|
325
|
+
}
|
|
326
|
+
if (key.backspace || key.delete) {
|
|
327
|
+
setValue((previous) => previous.slice(0, -1));
|
|
328
|
+
return;
|
|
329
|
+
}
|
|
330
|
+
if (input.length > 0 && !key.ctrl && !key.meta && !key.upArrow && !key.downArrow && !key.leftArrow && !key.rightArrow) {
|
|
331
|
+
setValue((previous) => previous + printable(input));
|
|
332
|
+
}
|
|
333
|
+
});
|
|
334
|
+
if (answer !== undefined)
|
|
335
|
+
return _jsx(AnswerLine, { message: message, answer: answer });
|
|
336
|
+
const hint = placeholder ?? (defaultValue.length > 0 ? defaultValue : undefined);
|
|
337
|
+
return (_jsxs(Box, { flexDirection: "column", children: [_jsxs(Text, { children: [_jsx(Text, { bold: true, children: message }), " ", value.length > 0 ? _jsx(Text, { children: value }) : hint !== undefined ? _jsx(Text, { dimColor: true, children: hint }) : null, ghost !== undefined ? _jsx(Text, { dimColor: true, children: ghost }) : null, _jsx(Text, { color: "cyan", children: "\u2588" })] }), ghost !== undefined ? _jsx(Text, { dimColor: true, children: " (tab completes the suggestion)" }) : null] }));
|
|
338
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A minimal external store the imperative presenter controllers mutate and Ink
|
|
3
|
+
* components subscribe to (via `useSyncExternalStore`), bridging the CLI's
|
|
4
|
+
* imperative call sites with React's declarative rendering.
|
|
5
|
+
*/
|
|
6
|
+
export declare class Store<T> {
|
|
7
|
+
private state;
|
|
8
|
+
private readonly listeners;
|
|
9
|
+
constructor(initial: T);
|
|
10
|
+
get: () => T;
|
|
11
|
+
set: (updater: (previous: T) => T) => void;
|
|
12
|
+
subscribe: (listener: () => void) => (() => void);
|
|
13
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A minimal external store the imperative presenter controllers mutate and Ink
|
|
3
|
+
* components subscribe to (via `useSyncExternalStore`), bridging the CLI's
|
|
4
|
+
* imperative call sites with React's declarative rendering.
|
|
5
|
+
*/
|
|
6
|
+
export class Store {
|
|
7
|
+
state;
|
|
8
|
+
listeners = new Set();
|
|
9
|
+
constructor(initial) {
|
|
10
|
+
this.state = initial;
|
|
11
|
+
}
|
|
12
|
+
get = () => this.state;
|
|
13
|
+
set = (updater) => {
|
|
14
|
+
this.state = updater(this.state);
|
|
15
|
+
for (const listener of this.listeners)
|
|
16
|
+
listener();
|
|
17
|
+
};
|
|
18
|
+
subscribe = (listener) => {
|
|
19
|
+
this.listeners.add(listener);
|
|
20
|
+
return () => this.listeners.delete(listener);
|
|
21
|
+
};
|
|
22
|
+
}
|
package/dist/plain.d.ts
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import type { ChecklistController, ErrorPanelInput, KeyValueRow, LiveFrameController, Presenter, ProgressController, StatusKind, StepInput, TableOptions, TaskController } from "./presenter.js";
|
|
2
|
+
export declare function renderTableLines(rows: readonly (readonly string[])[], options?: TableOptions): string[];
|
|
3
|
+
/**
|
|
4
|
+
* Render the failure panel as styled lines: a red-framed box with the message,
|
|
5
|
+
* dim evidence lines, the hint, and a `try:` next command. Shared by the plain
|
|
6
|
+
* and Ink presenters (identical settled output) and reused by the top-level
|
|
7
|
+
* error handler.
|
|
8
|
+
*/
|
|
9
|
+
export declare function renderErrorPanelLines(input: ErrorPanelInput): string[];
|
|
10
|
+
export declare function renderKeyValueLines(rows: readonly KeyValueRow[]): string[];
|
|
11
|
+
export declare class PlainPresenter implements Presenter {
|
|
12
|
+
readonly interactive: boolean;
|
|
13
|
+
private readonly stream;
|
|
14
|
+
constructor(stream?: NodeJS.WriteStream);
|
|
15
|
+
private writeLine;
|
|
16
|
+
banner(subtitle?: string): void;
|
|
17
|
+
header(subtitle?: string): void;
|
|
18
|
+
heading(text: string): void;
|
|
19
|
+
line(text: string): void;
|
|
20
|
+
blank(): void;
|
|
21
|
+
note(text: string): void;
|
|
22
|
+
success(text: string): void;
|
|
23
|
+
warn(text: string): void;
|
|
24
|
+
error(text: string): void;
|
|
25
|
+
status(kind: StatusKind, label: string, detail?: string, hint?: string): void;
|
|
26
|
+
keyValue(rows: readonly KeyValueRow[]): void;
|
|
27
|
+
table(rows: readonly (readonly string[])[], options?: TableOptions): void;
|
|
28
|
+
box(title: string, lines: readonly string[]): void;
|
|
29
|
+
errorPanel(input: ErrorPanelInput): void;
|
|
30
|
+
checklist(steps: readonly StepInput[], options?: {
|
|
31
|
+
title?: string;
|
|
32
|
+
}): ChecklistController;
|
|
33
|
+
task(text: string): TaskController;
|
|
34
|
+
progress(label: string): ProgressController;
|
|
35
|
+
liveFrame(): LiveFrameController;
|
|
36
|
+
}
|