@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,31 @@
|
|
|
1
|
+
import type { Instance } from "ink";
|
|
2
|
+
import type { ReactElement } from "react";
|
|
3
|
+
import { PlainPresenter } from "../plain.js";
|
|
4
|
+
import type { ChecklistController, LiveFrameController, ProgressController, StepInput, TaskController } from "../presenter.js";
|
|
5
|
+
/** Mount a bounded Ink app on stderr (stdout stays reserved for payloads). */
|
|
6
|
+
export declare function mountInk(node: ReactElement): Instance;
|
|
7
|
+
/**
|
|
8
|
+
* Clear the live Ink region and unmount, leaving the terminal clean for the
|
|
9
|
+
* caller to write the settled plain lines.
|
|
10
|
+
*
|
|
11
|
+
* Ink throttles renders (~30fps) with a trailing call, so `instance.clear()`
|
|
12
|
+
* followed by `unmount()` is racy: unmount flushes the pending trailing
|
|
13
|
+
* render *after* the clear, repainting the live frame — which the caller's
|
|
14
|
+
* raw write then duplicates on screen. Rerendering an empty node first makes
|
|
15
|
+
* that flushed render paint emptiness (erasing the region), so nothing stale
|
|
16
|
+
* survives the unmount.
|
|
17
|
+
*/
|
|
18
|
+
export declare function settleInk(instance: Instance): void;
|
|
19
|
+
/**
|
|
20
|
+
* Rich presenter. Extends the plain presenter for static output (identical
|
|
21
|
+
* line rendering) and swaps the live surfaces for Ink-mounted components.
|
|
22
|
+
*/
|
|
23
|
+
export declare class InkPresenter extends PlainPresenter {
|
|
24
|
+
readonly interactive: boolean;
|
|
25
|
+
checklist(steps: readonly StepInput[], options?: {
|
|
26
|
+
title?: string;
|
|
27
|
+
}): ChecklistController;
|
|
28
|
+
task(text: string): TaskController;
|
|
29
|
+
progress(label: string): ProgressController;
|
|
30
|
+
liveFrame(): LiveFrameController;
|
|
31
|
+
}
|
|
@@ -0,0 +1,250 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
/**
|
|
3
|
+
* The Ink presenter: rich rendering for interactive TTYs. Static output is
|
|
4
|
+
* written as styled lines (identical to the plain presenter, so transcripts
|
|
5
|
+
* stay consistent); live surfaces — checklist, task, progress — mount a
|
|
6
|
+
* bounded Ink app each, and settle by clearing the live region, unmounting,
|
|
7
|
+
* and writing the final state as ordinary lines. Nothing stays mounted after
|
|
8
|
+
* a surface settles, so the terminal can always be handed to a spawned coding
|
|
9
|
+
* agent cleanly.
|
|
10
|
+
*/
|
|
11
|
+
import { render } from "ink";
|
|
12
|
+
import { createElement, Fragment } from "react";
|
|
13
|
+
import { formatBytes } from "../format.js";
|
|
14
|
+
import { PlainPresenter } from "../plain.js";
|
|
15
|
+
import { uiStream } from "../runtime.js";
|
|
16
|
+
import { cyan, dim, glyph, gray, green, red, yellow } from "../theme.js";
|
|
17
|
+
import { ChecklistView, LiveFrameView, ProgressView, TaskView } from "./components.js";
|
|
18
|
+
import { Store } from "./store.js";
|
|
19
|
+
/** Mount a bounded Ink app on stderr (stdout stays reserved for payloads). */
|
|
20
|
+
export function mountInk(node) {
|
|
21
|
+
return render(node, {
|
|
22
|
+
stdout: uiStream(),
|
|
23
|
+
stderr: uiStream(),
|
|
24
|
+
stdin: process.stdin,
|
|
25
|
+
exitOnCtrlC: false,
|
|
26
|
+
patchConsole: false
|
|
27
|
+
});
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* Clear the live Ink region and unmount, leaving the terminal clean for the
|
|
31
|
+
* caller to write the settled plain lines.
|
|
32
|
+
*
|
|
33
|
+
* Ink throttles renders (~30fps) with a trailing call, so `instance.clear()`
|
|
34
|
+
* followed by `unmount()` is racy: unmount flushes the pending trailing
|
|
35
|
+
* render *after* the clear, repainting the live frame — which the caller's
|
|
36
|
+
* raw write then duplicates on screen. Rerendering an empty node first makes
|
|
37
|
+
* that flushed render paint emptiness (erasing the region), so nothing stale
|
|
38
|
+
* survives the unmount.
|
|
39
|
+
*/
|
|
40
|
+
export function settleInk(instance) {
|
|
41
|
+
instance.rerender(createElement(Fragment));
|
|
42
|
+
instance.unmount();
|
|
43
|
+
instance.cleanup();
|
|
44
|
+
}
|
|
45
|
+
function stepFinalLine(step) {
|
|
46
|
+
let symbol;
|
|
47
|
+
switch (step.status) {
|
|
48
|
+
case "pending":
|
|
49
|
+
symbol = gray(glyph.pending());
|
|
50
|
+
break;
|
|
51
|
+
case "active":
|
|
52
|
+
symbol = dim(glyph.arrow());
|
|
53
|
+
break;
|
|
54
|
+
case "done":
|
|
55
|
+
symbol = green(glyph.tick());
|
|
56
|
+
break;
|
|
57
|
+
case "failed":
|
|
58
|
+
symbol = red(glyph.cross());
|
|
59
|
+
break;
|
|
60
|
+
case "skipped":
|
|
61
|
+
symbol = yellow(glyph.bullet());
|
|
62
|
+
break;
|
|
63
|
+
default: {
|
|
64
|
+
const exhaustive = step.status;
|
|
65
|
+
throw new Error(`unknown step status: ${String(exhaustive)}`);
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
const detail = step.detail !== undefined ? ` ${dim(step.detail)}` : "";
|
|
69
|
+
const elapsed = step.startedAt !== undefined && step.endedAt !== undefined && step.endedAt - step.startedAt >= 50
|
|
70
|
+
? gray(` ${((step.endedAt - step.startedAt) / 1000).toFixed(1)}s`)
|
|
71
|
+
: "";
|
|
72
|
+
return `${symbol} ${step.label}${detail}${elapsed}`;
|
|
73
|
+
}
|
|
74
|
+
class InkChecklist {
|
|
75
|
+
store;
|
|
76
|
+
instance;
|
|
77
|
+
stream = uiStream();
|
|
78
|
+
settled = false;
|
|
79
|
+
constructor(steps, title) {
|
|
80
|
+
this.store = new Store({
|
|
81
|
+
...(title !== undefined ? { title } : {}),
|
|
82
|
+
steps: steps.map((step) => ({ id: step.id, label: step.label, status: "pending" }))
|
|
83
|
+
});
|
|
84
|
+
this.instance = mountInk(_jsx(ChecklistView, { store: this.store }));
|
|
85
|
+
}
|
|
86
|
+
transition(id, status, detail) {
|
|
87
|
+
this.store.set((state) => ({
|
|
88
|
+
...state,
|
|
89
|
+
steps: state.steps.map((step) => {
|
|
90
|
+
if (step.id !== id)
|
|
91
|
+
return step;
|
|
92
|
+
const now = Date.now();
|
|
93
|
+
const startedAt = step.startedAt ?? (status !== "pending" ? now : undefined);
|
|
94
|
+
const endedAt = status === "done" || status === "failed" || status === "skipped"
|
|
95
|
+
? (step.endedAt ?? now)
|
|
96
|
+
: step.endedAt;
|
|
97
|
+
return {
|
|
98
|
+
...step,
|
|
99
|
+
status,
|
|
100
|
+
...(detail !== undefined ? { detail } : {}),
|
|
101
|
+
...(startedAt !== undefined ? { startedAt } : {}),
|
|
102
|
+
...(endedAt !== undefined ? { endedAt } : {})
|
|
103
|
+
};
|
|
104
|
+
})
|
|
105
|
+
}));
|
|
106
|
+
}
|
|
107
|
+
setActive(id, detail) {
|
|
108
|
+
this.transition(id, "active", detail);
|
|
109
|
+
}
|
|
110
|
+
setDone(id, detail) {
|
|
111
|
+
this.transition(id, "done", detail);
|
|
112
|
+
}
|
|
113
|
+
setFailed(id, detail) {
|
|
114
|
+
this.transition(id, "failed", detail);
|
|
115
|
+
}
|
|
116
|
+
setSkipped(id, detail) {
|
|
117
|
+
this.transition(id, "skipped", detail);
|
|
118
|
+
}
|
|
119
|
+
setDetail(id, detail) {
|
|
120
|
+
this.store.set((state) => ({
|
|
121
|
+
...state,
|
|
122
|
+
steps: state.steps.map((step) => (step.id === id ? { ...step, detail } : step))
|
|
123
|
+
}));
|
|
124
|
+
}
|
|
125
|
+
stop() {
|
|
126
|
+
if (this.settled)
|
|
127
|
+
return;
|
|
128
|
+
this.settled = true;
|
|
129
|
+
settleInk(this.instance);
|
|
130
|
+
const state = this.store.get();
|
|
131
|
+
const lines = [];
|
|
132
|
+
if (state.title !== undefined)
|
|
133
|
+
lines.push(state.title);
|
|
134
|
+
for (const step of state.steps)
|
|
135
|
+
lines.push(stepFinalLine(step));
|
|
136
|
+
this.stream.write(lines.join("\n") + "\n");
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
class InkTask {
|
|
140
|
+
store;
|
|
141
|
+
instance;
|
|
142
|
+
stream = uiStream();
|
|
143
|
+
settled = false;
|
|
144
|
+
constructor(text) {
|
|
145
|
+
this.store = new Store({ text });
|
|
146
|
+
this.instance = mountInk(_jsx(TaskView, { store: this.store }));
|
|
147
|
+
}
|
|
148
|
+
update(text) {
|
|
149
|
+
this.store.set((state) => ({ ...state, text }));
|
|
150
|
+
}
|
|
151
|
+
settle(line) {
|
|
152
|
+
if (this.settled)
|
|
153
|
+
return;
|
|
154
|
+
this.settled = true;
|
|
155
|
+
settleInk(this.instance);
|
|
156
|
+
if (line !== undefined)
|
|
157
|
+
this.stream.write(`${line}\n`);
|
|
158
|
+
}
|
|
159
|
+
succeed(text) {
|
|
160
|
+
this.settle(`${green(glyph.tick())} ${text ?? this.store.get().text}`);
|
|
161
|
+
}
|
|
162
|
+
fail(text) {
|
|
163
|
+
this.settle(`${red(glyph.cross())} ${text ?? this.store.get().text}`);
|
|
164
|
+
}
|
|
165
|
+
warn(text) {
|
|
166
|
+
this.settle(`${yellow(glyph.warn())} ${text ?? this.store.get().text}`);
|
|
167
|
+
}
|
|
168
|
+
info(text) {
|
|
169
|
+
this.settle(`${cyan(glyph.bullet())} ${text ?? this.store.get().text}`);
|
|
170
|
+
}
|
|
171
|
+
stop() {
|
|
172
|
+
this.settle(undefined);
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
class InkProgress {
|
|
176
|
+
store;
|
|
177
|
+
instance;
|
|
178
|
+
stream = uiStream();
|
|
179
|
+
settled = false;
|
|
180
|
+
constructor(label) {
|
|
181
|
+
this.store = new Store({ label, downloaded: 0, startedAt: Date.now() });
|
|
182
|
+
this.instance = mountInk(_jsx(ProgressView, { store: this.store }));
|
|
183
|
+
}
|
|
184
|
+
update(progress) {
|
|
185
|
+
this.store.set((state) => ({
|
|
186
|
+
...state,
|
|
187
|
+
downloaded: progress.downloaded,
|
|
188
|
+
...(progress.total !== undefined && progress.total > 0 ? { total: progress.total } : {})
|
|
189
|
+
}));
|
|
190
|
+
}
|
|
191
|
+
settle(line) {
|
|
192
|
+
if (this.settled)
|
|
193
|
+
return;
|
|
194
|
+
this.settled = true;
|
|
195
|
+
settleInk(this.instance);
|
|
196
|
+
if (line !== undefined)
|
|
197
|
+
this.stream.write(`${line}\n`);
|
|
198
|
+
}
|
|
199
|
+
succeed(text) {
|
|
200
|
+
const state = this.store.get();
|
|
201
|
+
this.settle(`${green(glyph.tick())} ${text ?? `${state.label} ${dim(`(${formatBytes(state.downloaded)})`)}`}`);
|
|
202
|
+
}
|
|
203
|
+
fail(text) {
|
|
204
|
+
const state = this.store.get();
|
|
205
|
+
this.settle(`${red(glyph.cross())} ${text ?? `${state.label} ${gray("(failed)")}`}`);
|
|
206
|
+
}
|
|
207
|
+
stop() {
|
|
208
|
+
this.settle(undefined);
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
class InkLiveFrame {
|
|
212
|
+
store = new Store({ lines: [] });
|
|
213
|
+
instance = mountInk(_jsx(LiveFrameView, { store: this.store }));
|
|
214
|
+
stream = uiStream();
|
|
215
|
+
settled = false;
|
|
216
|
+
render(content) {
|
|
217
|
+
if (this.settled)
|
|
218
|
+
return;
|
|
219
|
+
const lines = [...(typeof content === "function" ? content() : content)];
|
|
220
|
+
this.store.set(() => ({ lines }));
|
|
221
|
+
}
|
|
222
|
+
stop() {
|
|
223
|
+
if (this.settled)
|
|
224
|
+
return;
|
|
225
|
+
this.settled = true;
|
|
226
|
+
settleInk(this.instance);
|
|
227
|
+
const lines = this.store.get().lines;
|
|
228
|
+
if (lines.length > 0)
|
|
229
|
+
this.stream.write(`${lines.join("\n")}\n`);
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
/**
|
|
233
|
+
* Rich presenter. Extends the plain presenter for static output (identical
|
|
234
|
+
* line rendering) and swaps the live surfaces for Ink-mounted components.
|
|
235
|
+
*/
|
|
236
|
+
export class InkPresenter extends PlainPresenter {
|
|
237
|
+
interactive = true;
|
|
238
|
+
checklist(steps, options = {}) {
|
|
239
|
+
return new InkChecklist(steps, options.title);
|
|
240
|
+
}
|
|
241
|
+
task(text) {
|
|
242
|
+
return new InkTask(text);
|
|
243
|
+
}
|
|
244
|
+
progress(label) {
|
|
245
|
+
return new InkProgress(label);
|
|
246
|
+
}
|
|
247
|
+
liveFrame() {
|
|
248
|
+
return new InkLiveFrame();
|
|
249
|
+
}
|
|
250
|
+
}
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import type { ReactElement } from "react";
|
|
2
|
+
import { Store } from "./store.js";
|
|
3
|
+
export type PromptOption<T> = {
|
|
4
|
+
value: T;
|
|
5
|
+
label: string;
|
|
6
|
+
hint?: string;
|
|
7
|
+
};
|
|
8
|
+
type SelectProps<T> = {
|
|
9
|
+
message: string;
|
|
10
|
+
options: ReadonlyArray<PromptOption<T>>;
|
|
11
|
+
defaultIndex: number;
|
|
12
|
+
onSubmit: (value: T, label: string) => void;
|
|
13
|
+
onAbort: () => void;
|
|
14
|
+
/** Esc pressed (wizard back-navigation); omitted = Esc is ignored. */
|
|
15
|
+
onBack?: (() => void) | undefined;
|
|
16
|
+
};
|
|
17
|
+
export declare function SelectPrompt<T>({ message, options, defaultIndex, onSubmit, onAbort, onBack }: SelectProps<T>): ReactElement;
|
|
18
|
+
type MultiSelectProps<T> = {
|
|
19
|
+
message: string;
|
|
20
|
+
options: ReadonlyArray<PromptOption<T>>;
|
|
21
|
+
defaultSelected: ReadonlySet<number>;
|
|
22
|
+
onSubmit: (values: T[], labels: string[]) => void;
|
|
23
|
+
onAbort: () => void;
|
|
24
|
+
};
|
|
25
|
+
export declare function MultiSelectPrompt<T>({ message, options, defaultSelected, onSubmit, onAbort }: MultiSelectProps<T>): ReactElement;
|
|
26
|
+
type ConfirmProps = {
|
|
27
|
+
message: string;
|
|
28
|
+
defaultValue: boolean;
|
|
29
|
+
onSubmit: (value: boolean) => void;
|
|
30
|
+
onAbort: () => void;
|
|
31
|
+
onBack?: (() => void) | undefined;
|
|
32
|
+
};
|
|
33
|
+
export declare function ConfirmPrompt({ message, defaultValue, onSubmit, onAbort, onBack }: ConfirmProps): ReactElement;
|
|
34
|
+
type TextProps = {
|
|
35
|
+
message: string;
|
|
36
|
+
defaultValue: string;
|
|
37
|
+
placeholder?: string;
|
|
38
|
+
onSubmit: (value: string) => void;
|
|
39
|
+
onAbort: () => void;
|
|
40
|
+
onBack?: (() => void) | undefined;
|
|
41
|
+
};
|
|
42
|
+
export declare function TextPrompt({ message, defaultValue, placeholder, onSubmit, onAbort, onBack }: TextProps): ReactElement;
|
|
43
|
+
/** Live option feed: the facade refreshes `options` while the picker is open. */
|
|
44
|
+
export type FuzzyFeed<T> = {
|
|
45
|
+
options: ReadonlyArray<PromptOption<T>>;
|
|
46
|
+
/** True while a background refresh is in flight (renders a spinner note). */
|
|
47
|
+
loading: boolean;
|
|
48
|
+
/** A dim note about the feed, e.g. "refreshing model catalog…". */
|
|
49
|
+
note?: string;
|
|
50
|
+
};
|
|
51
|
+
type FuzzySelectProps<T> = {
|
|
52
|
+
message: string;
|
|
53
|
+
feed: Store<FuzzyFeed<T>>;
|
|
54
|
+
placeholder?: string | undefined;
|
|
55
|
+
onSubmit: (value: T, label: string) => void;
|
|
56
|
+
onAbort: () => void;
|
|
57
|
+
onBack?: (() => void) | undefined;
|
|
58
|
+
};
|
|
59
|
+
export declare function FuzzySelectPrompt<T>({ message, feed, placeholder, onSubmit, onAbort, onBack }: FuzzySelectProps<T>): ReactElement;
|
|
60
|
+
type GhostTextProps = {
|
|
61
|
+
message: string;
|
|
62
|
+
/** Ranked suggestion candidates; the first prefix-completion wins. */
|
|
63
|
+
suggestions: ReadonlyArray<string>;
|
|
64
|
+
placeholder?: string | undefined;
|
|
65
|
+
defaultValue: string;
|
|
66
|
+
onSubmit: (value: string) => void;
|
|
67
|
+
onAbort: () => void;
|
|
68
|
+
onBack?: (() => void) | undefined;
|
|
69
|
+
};
|
|
70
|
+
/** The inline (ghost) completion for `value`: the remainder of the best prefix match. */
|
|
71
|
+
export declare function ghostRemainder(value: string, suggestions: ReadonlyArray<string>): string | undefined;
|
|
72
|
+
export declare function GhostTextPrompt({ message, suggestions, placeholder, defaultValue, onSubmit, onAbort, onBack }: GhostTextProps): ReactElement;
|
|
73
|
+
export {};
|