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/dist/inquirer.js
ADDED
|
@@ -0,0 +1,207 @@
|
|
|
1
|
+
import { AsyncResource } from 'node:async_hooks';
|
|
2
|
+
import { resolve as resolvePath } from 'node:path';
|
|
3
|
+
import { createInterface } from 'node:readline';
|
|
4
|
+
import exitHook from 'closeout/exit-hook';
|
|
5
|
+
import { AbortPromptError, CancelPromptError, ExitPromptError } from './inquirer-errors.js';
|
|
6
|
+
import { effectScheduler, useEffect, useRef, useState, withHooks, withUpdates } from './inquirer-hooks.js';
|
|
7
|
+
import { MuteStream, ScreenManager } from './inquirer-screen.js';
|
|
8
|
+
import { LINE, makeTheme } from './inquirer-theme.js';
|
|
9
|
+
import { processRuntime } from './runtime.js';
|
|
10
|
+
const SEPARATOR_WIDTH = 15;
|
|
11
|
+
const SPINNER_DELAY_MS = 300;
|
|
12
|
+
export class Separator {
|
|
13
|
+
separator = Array.from({ length: SEPARATOR_WIDTH }).join(LINE);
|
|
14
|
+
type = 'separator';
|
|
15
|
+
constructor(separator) {
|
|
16
|
+
if (separator !== undefined && separator !== '')
|
|
17
|
+
this.separator = separator;
|
|
18
|
+
}
|
|
19
|
+
static isSeparator(choice) {
|
|
20
|
+
return Boolean(choice) && typeof choice === 'object' && choice !== null && 'type' in choice && choice.type === 'separator';
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
export function usePrefix({ status = 'idle', theme }) {
|
|
24
|
+
const [showLoader, setShowLoader] = useState(false);
|
|
25
|
+
const [tick, setTick] = useState(0);
|
|
26
|
+
const { prefix, spinner } = makeTheme(theme);
|
|
27
|
+
useEffect(() => {
|
|
28
|
+
if (status !== 'loading') {
|
|
29
|
+
setShowLoader(false);
|
|
30
|
+
return undefined;
|
|
31
|
+
}
|
|
32
|
+
let tickInterval;
|
|
33
|
+
let inc = -1;
|
|
34
|
+
const delayTimeout = setTimeout(() => {
|
|
35
|
+
setShowLoader(true);
|
|
36
|
+
tickInterval = setInterval(() => {
|
|
37
|
+
inc = inc + 1;
|
|
38
|
+
setTick(inc % spinner.frames.length);
|
|
39
|
+
}, spinner.interval);
|
|
40
|
+
}, SPINNER_DELAY_MS);
|
|
41
|
+
return () => {
|
|
42
|
+
clearTimeout(delayTimeout);
|
|
43
|
+
clearInterval(tickInterval);
|
|
44
|
+
};
|
|
45
|
+
}, [status]);
|
|
46
|
+
if (showLoader === true)
|
|
47
|
+
return spinner.frames[tick ?? 0] ?? '';
|
|
48
|
+
const iconName = status === 'loading' ? 'idle' : status;
|
|
49
|
+
if (typeof prefix === 'string')
|
|
50
|
+
return prefix;
|
|
51
|
+
return prefix[iconName] ?? prefix['idle'] ?? '';
|
|
52
|
+
}
|
|
53
|
+
export function useKeypress(userHandler) {
|
|
54
|
+
const signal = useRef(userHandler);
|
|
55
|
+
signal.current = userHandler;
|
|
56
|
+
useEffect((rl) => {
|
|
57
|
+
let ignore = false;
|
|
58
|
+
const handler = withUpdates((_input, event) => {
|
|
59
|
+
if (ignore)
|
|
60
|
+
return;
|
|
61
|
+
void signal.current(event, rl);
|
|
62
|
+
});
|
|
63
|
+
rl.input.on('keypress', handler);
|
|
64
|
+
return () => {
|
|
65
|
+
ignore = true;
|
|
66
|
+
rl.input.removeListener('keypress', handler);
|
|
67
|
+
};
|
|
68
|
+
}, []);
|
|
69
|
+
}
|
|
70
|
+
function callerFile() {
|
|
71
|
+
const saved = Error.prepareStackTrace;
|
|
72
|
+
let frames = [];
|
|
73
|
+
try {
|
|
74
|
+
Error.prepareStackTrace = (_error, callSites) => {
|
|
75
|
+
frames = [...callSites];
|
|
76
|
+
return frames;
|
|
77
|
+
};
|
|
78
|
+
void new Error('trace').stack;
|
|
79
|
+
}
|
|
80
|
+
catch {
|
|
81
|
+
return undefined;
|
|
82
|
+
}
|
|
83
|
+
Error.prepareStackTrace = saved;
|
|
84
|
+
const fileName = frames[2]?.getFileName() ?? undefined;
|
|
85
|
+
if (fileName === undefined || fileName.startsWith('file://'))
|
|
86
|
+
return fileName;
|
|
87
|
+
return resolvePath(fileName);
|
|
88
|
+
}
|
|
89
|
+
function listenTo(target, event, listener) {
|
|
90
|
+
const [add, remove] = 'on' in target
|
|
91
|
+
? [target.on.bind(target), target.removeListener.bind(target)]
|
|
92
|
+
: [target.addEventListener.bind(target), target.removeEventListener.bind(target)];
|
|
93
|
+
add(event, listener);
|
|
94
|
+
return () => {
|
|
95
|
+
remove(event, listener);
|
|
96
|
+
};
|
|
97
|
+
}
|
|
98
|
+
const nativeSetImmediate = globalThis.setImmediate;
|
|
99
|
+
function readlineOver(input, output) {
|
|
100
|
+
const stream = output;
|
|
101
|
+
return createInterface({ terminal: true, input, output: stream });
|
|
102
|
+
}
|
|
103
|
+
function runPrompt(view, origin, config, context) {
|
|
104
|
+
const runtime = processRuntime();
|
|
105
|
+
const { input = runtime.stdin, signal } = context;
|
|
106
|
+
const cleanups = new Set();
|
|
107
|
+
const output = new MuteStream();
|
|
108
|
+
output.pipe((context.output ?? runtime.stdout));
|
|
109
|
+
const rl = readlineOver(input, output);
|
|
110
|
+
output.mute();
|
|
111
|
+
const screen = new ScreenManager(rl);
|
|
112
|
+
const { promise, resolve, reject } = Promise.withResolvers();
|
|
113
|
+
return withHooks(rl, (cycle) => {
|
|
114
|
+
const clearEffects = AsyncResource.bind(() => {
|
|
115
|
+
effectScheduler.clearAll();
|
|
116
|
+
});
|
|
117
|
+
const settlePrompt = (settle) => {
|
|
118
|
+
try {
|
|
119
|
+
clearEffects();
|
|
120
|
+
settle();
|
|
121
|
+
}
|
|
122
|
+
catch (error) {
|
|
123
|
+
reject(error);
|
|
124
|
+
}
|
|
125
|
+
};
|
|
126
|
+
const settler = (finish) => (settled) => {
|
|
127
|
+
settlePrompt(() => {
|
|
128
|
+
finish(settled);
|
|
129
|
+
});
|
|
130
|
+
};
|
|
131
|
+
const resolvePrompt = settler(resolve);
|
|
132
|
+
const rejectPrompt = settler(reject);
|
|
133
|
+
const endsWith = (make) => () => {
|
|
134
|
+
rejectPrompt(make());
|
|
135
|
+
};
|
|
136
|
+
const promptPromise = Object.assign(promise
|
|
137
|
+
.finally(() => {
|
|
138
|
+
for (const cleanup of cleanups)
|
|
139
|
+
cleanup();
|
|
140
|
+
screen.done({ clearContent: Boolean(context.clearPromptOnDone) });
|
|
141
|
+
output.end();
|
|
142
|
+
})
|
|
143
|
+
.then(() => promise), { cancel: endsWith(() => new CancelPromptError()) });
|
|
144
|
+
if (signal) {
|
|
145
|
+
const abort = endsWith(() => new AbortPromptError({ cause: signal.reason }));
|
|
146
|
+
if (signal.aborted) {
|
|
147
|
+
abort();
|
|
148
|
+
return promptPromise;
|
|
149
|
+
}
|
|
150
|
+
cleanups.add(listenTo(signal, 'abort', abort));
|
|
151
|
+
}
|
|
152
|
+
const forceClosed = (how) => {
|
|
153
|
+
rejectPrompt(new ExitPromptError(`User force closed the prompt with ${how}`));
|
|
154
|
+
};
|
|
155
|
+
cleanups.add(exitHook((code) => {
|
|
156
|
+
forceClosed(String(code));
|
|
157
|
+
}));
|
|
158
|
+
cleanups.add(listenTo(rl, 'SIGINT', () => {
|
|
159
|
+
forceClosed('SIGINT');
|
|
160
|
+
}));
|
|
161
|
+
cleanups.add(listenTo(rl, 'close', clearEffects));
|
|
162
|
+
const startCycle = () => {
|
|
163
|
+
cleanups.add(listenTo(rl.input, 'keypress', () => {
|
|
164
|
+
screen.checkCursorPos();
|
|
165
|
+
}));
|
|
166
|
+
let pendingDone = null;
|
|
167
|
+
cycle(() => {
|
|
168
|
+
let effectsSettled = false;
|
|
169
|
+
try {
|
|
170
|
+
const nextView = view(config, (value) => {
|
|
171
|
+
if (effectsSettled)
|
|
172
|
+
resolvePrompt(value);
|
|
173
|
+
else
|
|
174
|
+
pendingDone = { value };
|
|
175
|
+
});
|
|
176
|
+
if (nextView === undefined)
|
|
177
|
+
throw new Error(`Prompt functions must return a string.\n at ${origin ?? '<unknown>'}`);
|
|
178
|
+
const [content, bottomContent] = typeof nextView === 'string' ? [nextView, undefined] : nextView;
|
|
179
|
+
screen.render(content, bottomContent);
|
|
180
|
+
effectScheduler.run();
|
|
181
|
+
}
|
|
182
|
+
catch (error) {
|
|
183
|
+
rejectPrompt(error);
|
|
184
|
+
}
|
|
185
|
+
effectsSettled = true;
|
|
186
|
+
if (pendingDone !== null) {
|
|
187
|
+
const { value } = pendingDone;
|
|
188
|
+
pendingDone = null;
|
|
189
|
+
resolvePrompt(value);
|
|
190
|
+
}
|
|
191
|
+
});
|
|
192
|
+
};
|
|
193
|
+
if ('readableFlowing' in input)
|
|
194
|
+
nativeSetImmediate(startCycle);
|
|
195
|
+
else
|
|
196
|
+
startCycle();
|
|
197
|
+
return promptPromise;
|
|
198
|
+
});
|
|
199
|
+
}
|
|
200
|
+
export function createPrompt(view) {
|
|
201
|
+
const origin = callerFile();
|
|
202
|
+
return (config, context = {}) => runPrompt(view, origin, config, context);
|
|
203
|
+
}
|
|
204
|
+
export { AbortPromptError, CancelPromptError, ExitPromptError, HookError, ValidationError } from './inquirer-errors.js';
|
|
205
|
+
export { useEffect, useMemo, useRef, useState } from './inquirer-hooks.js';
|
|
206
|
+
export { getDefaultKeybindings, isBackspaceKey, isDownKey, isEnterKey, isNumberKey, isShiftKey, isSpaceKey, isTabKey, isUpKey, } from './inquirer-keys.js';
|
|
207
|
+
export { defaultTheme, getDefaultTheme, makeTheme } from './inquirer-theme.js';
|
package/dist/plugin.d.ts
ADDED
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import { type PromptSpec } from './spec.js';
|
|
2
|
+
/**
|
|
3
|
+
* The plugin contract version. One number for the family — the same `1` flagstaff and
|
|
4
|
+
* roundel declare, written out rather than imported for the reason in the file comment.
|
|
5
|
+
*/
|
|
6
|
+
export declare const CONTRACT = 1;
|
|
7
|
+
/**
|
|
8
|
+
* Two named states of plain data, which a grader renders a widget with (R7).
|
|
9
|
+
*
|
|
10
|
+
* It carries no behaviour, so reading it does not mean running the author's code — that is
|
|
11
|
+
* why an optional `sample` does not turn a plugin into a program.
|
|
12
|
+
*/
|
|
13
|
+
export interface WidgetSample {
|
|
14
|
+
running: unknown;
|
|
15
|
+
done: unknown;
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* How a plugin draws one kind of prompt.
|
|
19
|
+
*
|
|
20
|
+
* `static(spec)` is what a pipe, an agent and a screen reader get; it is what `projection()`
|
|
21
|
+
* already returns for the six built-ins, so a plugin widget slots into the same surface
|
|
22
|
+
* rather than beside it. `frame` is optional and drives `caique/raw`.
|
|
23
|
+
*/
|
|
24
|
+
export interface Widget {
|
|
25
|
+
static: (spec: PromptSpec) => string;
|
|
26
|
+
frame?: (t: number, spec: PromptSpec) => string;
|
|
27
|
+
sample?: WidgetSample;
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* The keys caique reads. Declared structurally: any object with these fields is a plugin
|
|
31
|
+
* here, whatever else it carries.
|
|
32
|
+
*/
|
|
33
|
+
export interface Plugin {
|
|
34
|
+
name: string;
|
|
35
|
+
contract?: number;
|
|
36
|
+
widgets?: Record<string, Widget>;
|
|
37
|
+
}
|
|
38
|
+
export type PluginErrorCode = 'E_PLUGIN_SCHEMA' | 'E_PLUGIN_CONTRACT' | 'E_NO_STATIC_PROJECTION' | 'E_UNKNOWN_KIND';
|
|
39
|
+
/** A refused plugin says what is wrong and what to do about it — the family's one vocabulary. */
|
|
40
|
+
export declare class PluginError extends Error {
|
|
41
|
+
readonly code: PluginErrorCode;
|
|
42
|
+
readonly fix: string;
|
|
43
|
+
constructor(code: PluginErrorCode, message: string, fix: string);
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* Refuse a plugin that cannot contribute a widget, at the door.
|
|
47
|
+
*
|
|
48
|
+
* Every refusal here is about the `widgets` key or the plugin's own identity. A key another
|
|
49
|
+
* layer owns is not inspected and not rejected (R1) — caique has no opinion about a spinner.
|
|
50
|
+
*/
|
|
51
|
+
export declare function validate(plugin: unknown): asserts plugin is Plugin;
|
|
52
|
+
/** Which plugin last contributed each kind — the shadowing a `plugin check` prints. */
|
|
53
|
+
export interface Contribution {
|
|
54
|
+
kind: string;
|
|
55
|
+
from: string;
|
|
56
|
+
/** Plugins that contributed this kind earlier and were overridden, in order. */
|
|
57
|
+
shadowed: string[];
|
|
58
|
+
}
|
|
59
|
+
/**
|
|
60
|
+
* Register a plugin. Later wins, like ESLint flat config: the array is ordered, a caller
|
|
61
|
+
* reads it top to bottom, and the last word on a kind is the one nearest the program.
|
|
62
|
+
*/
|
|
63
|
+
export declare function register(plugin: unknown): void;
|
|
64
|
+
/** Forget every registered plugin. For tests, and for a program that re-registers at runtime. */
|
|
65
|
+
export declare function reset(): void;
|
|
66
|
+
/** The plugins registered, in registration order. */
|
|
67
|
+
export declare function registered(): readonly Plugin[];
|
|
68
|
+
/** Every kind a plugin contributed, with who won it and who it shadowed. */
|
|
69
|
+
export declare function widgets(): Contribution[];
|
|
70
|
+
/** The widget that draws `kind`, or nothing when no plugin registered one. */
|
|
71
|
+
export declare function widgetFor(kind: string): Widget | undefined;
|
|
72
|
+
/** Every kind that can be drawn right now: the six, plus whatever is registered. */
|
|
73
|
+
export declare function kinds(): string[];
|
|
74
|
+
/**
|
|
75
|
+
* The static projection for *any* kind — the one surface a caller needs.
|
|
76
|
+
*
|
|
77
|
+
* The six are still drawn by caique; anything else is a registered widget's `static`. A
|
|
78
|
+
* kind that is neither is a refusal naming what *is* registered, so the reader can see the
|
|
79
|
+
* typo rather than a text prompt where their rating widget should have been.
|
|
80
|
+
*/
|
|
81
|
+
export declare function projectionOf(spec: PromptSpec): string;
|
package/dist/plugin.js
ADDED
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
import { projection } from './ask.js';
|
|
2
|
+
import { BUILT_IN_KINDS } from './spec.js';
|
|
3
|
+
export const CONTRACT = 1;
|
|
4
|
+
export class PluginError extends Error {
|
|
5
|
+
code;
|
|
6
|
+
fix;
|
|
7
|
+
constructor(code, message, fix) {
|
|
8
|
+
super(message);
|
|
9
|
+
this.code = code;
|
|
10
|
+
this.fix = fix;
|
|
11
|
+
this.name = 'PluginError';
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
const isRecord = (v) => typeof v === 'object' && v !== null && !Array.isArray(v);
|
|
15
|
+
const builtIns = () => [...BUILT_IN_KINDS].join(', ');
|
|
16
|
+
export function validate(plugin) {
|
|
17
|
+
if (!isRecord(plugin))
|
|
18
|
+
throw new PluginError('E_PLUGIN_SCHEMA', 'a plugin is a plain object', 'export an object, not a function or an array');
|
|
19
|
+
if (typeof plugin['name'] !== 'string' || plugin['name'] === '') {
|
|
20
|
+
throw new PluginError('E_PLUGIN_SCHEMA', 'a plugin needs a name', 'add `name: "…"` — it is how a shadowed widget is reported');
|
|
21
|
+
}
|
|
22
|
+
const contract = plugin['contract'];
|
|
23
|
+
if (contract !== undefined && (!Number.isInteger(contract) || contract > CONTRACT)) {
|
|
24
|
+
throw new PluginError('E_PLUGIN_CONTRACT', `plugin "${plugin['name']}" declares contract ${String(contract)}; this caique knows ${CONTRACT}`, 'upgrade caique, or lower the plugin’s contract');
|
|
25
|
+
}
|
|
26
|
+
validateWidgets(plugin['widgets'], plugin['name']);
|
|
27
|
+
}
|
|
28
|
+
function validateWidgets(widgetMap, name) {
|
|
29
|
+
if (widgetMap === undefined)
|
|
30
|
+
return;
|
|
31
|
+
if (!isRecord(widgetMap))
|
|
32
|
+
throw new PluginError('E_PLUGIN_SCHEMA', `plugin "${name}": widgets must be an object`, 'map a prompt kind to a widget: `{ static, frame?, sample? }`');
|
|
33
|
+
for (const [kind, widget] of Object.entries(widgetMap)) {
|
|
34
|
+
if (kind === '')
|
|
35
|
+
throw new PluginError('E_PLUGIN_SCHEMA', `plugin "${name}": a widget’s kind is empty`, 'name the kind — it is what a `PromptSpec` sets as `kind`');
|
|
36
|
+
if (BUILT_IN_KINDS.has(kind)) {
|
|
37
|
+
throw new PluginError('E_PLUGIN_SCHEMA', `plugin "${name}": "${kind}" is a built-in prompt kind`, `caique draws the six itself (${builtIns()}); name a kind of your own`);
|
|
38
|
+
}
|
|
39
|
+
if (!isRecord(widget))
|
|
40
|
+
throw new PluginError('E_PLUGIN_SCHEMA', `plugin "${name}": widget "${kind}" is not an object`, 'a widget is `{ static, frame?, sample? }`');
|
|
41
|
+
if (typeof widget['static'] !== 'function') {
|
|
42
|
+
throw new PluginError('E_NO_STATIC_PROJECTION', `plugin "${name}": widget "${kind}" has no static projection`, 'add `static: (spec) => "…"` — it is what a pipe, an agent and a screen reader get');
|
|
43
|
+
}
|
|
44
|
+
if (widget['frame'] !== undefined && typeof widget['frame'] !== 'function') {
|
|
45
|
+
throw new PluginError('E_PLUGIN_SCHEMA', `plugin "${name}": widget "${kind}" has a \`frame\` that is not a function`, 'a frame is `(t, spec) => "…"`, or leave it out and the widget is line-mode only');
|
|
46
|
+
}
|
|
47
|
+
validateSample(widget['sample'], name, kind);
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
function validateSample(sample, name, kind) {
|
|
51
|
+
if (sample === undefined)
|
|
52
|
+
return;
|
|
53
|
+
if (!isRecord(sample) || !('running' in sample) || !('done' in sample)) {
|
|
54
|
+
throw new PluginError('E_PLUGIN_SCHEMA', `plugin "${name}": widget "${kind}" has a malformed \`sample\``, 'a sample is `{ running, done }` — two named states of plain data, which a grader renders the widget with');
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
const order = [];
|
|
58
|
+
export function register(plugin) {
|
|
59
|
+
validate(plugin);
|
|
60
|
+
order.push(plugin);
|
|
61
|
+
}
|
|
62
|
+
export function reset() {
|
|
63
|
+
order.length = 0;
|
|
64
|
+
}
|
|
65
|
+
export function registered() {
|
|
66
|
+
return order;
|
|
67
|
+
}
|
|
68
|
+
export function widgets() {
|
|
69
|
+
const by = new Map();
|
|
70
|
+
for (const plugin of order) {
|
|
71
|
+
for (const kind of Object.keys(plugin.widgets ?? {})) {
|
|
72
|
+
const existing = by.get(kind);
|
|
73
|
+
by.set(kind, { kind, from: plugin.name, shadowed: existing === undefined ? [] : [...existing.shadowed, existing.from] });
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
return [...by.values()];
|
|
77
|
+
}
|
|
78
|
+
export function widgetFor(kind) {
|
|
79
|
+
for (let i = order.length - 1; i >= 0; i -= 1) {
|
|
80
|
+
const widget = order[i].widgets?.[kind];
|
|
81
|
+
if (widget !== undefined)
|
|
82
|
+
return widget;
|
|
83
|
+
}
|
|
84
|
+
return undefined;
|
|
85
|
+
}
|
|
86
|
+
export function kinds() {
|
|
87
|
+
return [...BUILT_IN_KINDS, ...widgets().map((c) => c.kind)];
|
|
88
|
+
}
|
|
89
|
+
export function projectionOf(spec) {
|
|
90
|
+
if (BUILT_IN_KINDS.has(spec.kind))
|
|
91
|
+
return projection(spec);
|
|
92
|
+
const widget = widgetFor(spec.kind);
|
|
93
|
+
if (widget === undefined) {
|
|
94
|
+
const known = widgets().map((c) => c.kind);
|
|
95
|
+
const named = known.length === 0 ? 'no plugin has registered a widget' : `registered kinds: ${known.join(', ')}`;
|
|
96
|
+
throw new PluginError('E_UNKNOWN_KIND', `no widget draws prompt kind ${JSON.stringify(spec.kind)} — ${named}`, `register a plugin whose \`widgets\` defines ${JSON.stringify(spec.kind)}, or use a built-in kind: ${builtIns()}`);
|
|
97
|
+
}
|
|
98
|
+
return widget.static(spec).trimEnd();
|
|
99
|
+
}
|
package/dist/raw.d.ts
CHANGED
|
@@ -1,20 +1,3 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* The raw-mode renderer: arrow keys and a moving highlight for `select` and `multiselect`,
|
|
3
|
-
* on a terminal that can take them.
|
|
4
|
-
*
|
|
5
|
-
* **It answers the same questions `ask()` does, and returns the same answers.** That is the
|
|
6
|
-
* whole arrangement: line mode is the floor (R5), this sits on top, and a caller chooses
|
|
7
|
-
* between them by asking whether the terminal is one. Anything this can do that line mode
|
|
8
|
-
* cannot is decoration; anything line mode can do that this cannot would be a bug.
|
|
9
|
-
*
|
|
10
|
-
* **On the dependency the design named.** It said "spinner from flagstaff", and this does
|
|
11
|
-
* not import flagstaff. A prompt has no spinner — it is waiting for a person, not for work
|
|
12
|
-
* — and the repaint it needs is three escape sequences, written here. Importing flagstaff
|
|
13
|
-
* to get them would make the one package that talks to a human the only one in the family
|
|
14
|
-
* that requires a sibling, which is the rule `caique`'s own README states. If a prompt ever
|
|
15
|
-
* needs to show progress *while* it waits, that is a caller composing `hoist()` around
|
|
16
|
-
* `ask()`, not this file reaching for it.
|
|
17
|
-
*/
|
|
18
1
|
import { type Asked, type Io } from './ask.js';
|
|
19
2
|
import { type Choice, type PromptSpec } from './spec.js';
|
|
20
3
|
/** A stream that can be put into raw mode and read a key at a time. */
|
|
@@ -50,6 +33,9 @@ export declare function renderList(spec: PromptSpec, choices: Choice[], state: L
|
|
|
50
33
|
* Returns the same `Asked` shape `ask()` does, so a caller can swap the two without
|
|
51
34
|
* knowing which ran — and cancels the same way, because `Ctrl-C` in raw mode is a byte and
|
|
52
35
|
* not a signal, and a person who presses it means to leave.
|
|
36
|
+
*
|
|
37
|
+
* The cursor is hidden through `closeout`: the byte path resolves and `restore()` runs in
|
|
38
|
+
* the `finally`; every path that never reaches the `finally` is the exit hook's.
|
|
53
39
|
*/
|
|
54
40
|
export declare function askList(spec: PromptSpec, io: RawIo, multi?: boolean): Promise<Asked>;
|
|
55
41
|
export {};
|
package/dist/raw.js
CHANGED
|
@@ -1,33 +1,8 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
* on a terminal that can take them.
|
|
4
|
-
*
|
|
5
|
-
* **It answers the same questions `ask()` does, and returns the same answers.** That is the
|
|
6
|
-
* whole arrangement: line mode is the floor (R5), this sits on top, and a caller chooses
|
|
7
|
-
* between them by asking whether the terminal is one. Anything this can do that line mode
|
|
8
|
-
* cannot is decoration; anything line mode can do that this cannot would be a bug.
|
|
9
|
-
*
|
|
10
|
-
* **On the dependency the design named.** It said "spinner from flagstaff", and this does
|
|
11
|
-
* not import flagstaff. A prompt has no spinner — it is waiting for a person, not for work
|
|
12
|
-
* — and the repaint it needs is three escape sequences, written here. Importing flagstaff
|
|
13
|
-
* to get them would make the one package that talks to a human the only one in the family
|
|
14
|
-
* that requires a sibling, which is the rule `caique`'s own README states. If a prompt ever
|
|
15
|
-
* needs to show progress *while* it waits, that is a caller composing `hoist()` around
|
|
16
|
-
* `ask()`, not this file reaching for it.
|
|
17
|
-
*/
|
|
18
|
-
import {} from './ask.js';
|
|
19
|
-
import {} from './spec.js';
|
|
1
|
+
import { hideCursor } from 'closeout/cursor';
|
|
2
|
+
import exitHook from 'closeout/exit-hook';
|
|
20
3
|
const ESC = '\u001B';
|
|
21
4
|
const CSI = `${ESC}[`;
|
|
22
|
-
const HIDE_CURSOR = `${CSI}?25l`;
|
|
23
|
-
const SHOW_CURSOR = `${CSI}?25h`;
|
|
24
|
-
/** Column 1, up `n` lines, clear to the end of the screen — the only repaint this needs. */
|
|
25
5
|
const erase = (lines) => `${CSI}1G${lines > 1 ? `${CSI}${lines - 1}A` : ''}${CSI}0J`;
|
|
26
|
-
/**
|
|
27
|
-
* What a keypress means. Only the six that drive a list — everything else is `other`, and
|
|
28
|
-
* a widget that does not know what to do with a key does nothing, which is what a person
|
|
29
|
-
* expects from a key they pressed by accident.
|
|
30
|
-
*/
|
|
31
6
|
export function keyOf(data) {
|
|
32
7
|
if (data === `${CSI}A` || data === 'k')
|
|
33
8
|
return 'up';
|
|
@@ -37,26 +12,21 @@ export function keyOf(data) {
|
|
|
37
12
|
return 'space';
|
|
38
13
|
if (data === '\r' || data === '\n')
|
|
39
14
|
return 'enter';
|
|
40
|
-
// Ctrl-C and Ctrl-D. In raw mode the terminal delivers these as bytes rather than
|
|
41
|
-
// signals, so a widget that did not read them would leave a person unable to leave.
|
|
42
15
|
if (data === '\u0003' || data === '\u0004' || data === ESC)
|
|
43
16
|
return 'cancel';
|
|
44
17
|
return 'other';
|
|
45
18
|
}
|
|
46
|
-
/** Whether this runtime can drive the raw renderer at all. */
|
|
47
19
|
export function canRender(keys) {
|
|
48
20
|
return keys.isTTY === true && typeof keys.setRawMode === 'function';
|
|
49
21
|
}
|
|
50
22
|
const MARK = { on: '◉', off: '◯' };
|
|
51
23
|
const POINTER = '❯';
|
|
52
24
|
const CANCELLED = { ok: false, reason: 'cancelled' };
|
|
53
|
-
/** The `◉`/`◯` column, which only a multiselect has. Empty for a single select. */
|
|
54
25
|
function markFor(state, index, multi) {
|
|
55
26
|
if (!multi)
|
|
56
27
|
return '';
|
|
57
28
|
return `${state.selected.has(index) ? MARK.on : MARK.off} `;
|
|
58
29
|
}
|
|
59
|
-
/** One frame of the list. Exported so a test asserts the drawing rather than a screenshot. */
|
|
60
30
|
export function renderList(spec, choices, state, multi) {
|
|
61
31
|
const rows = choices.map((choice, index) => {
|
|
62
32
|
const pointer = index === state.cursor ? POINTER : ' ';
|
|
@@ -66,11 +36,6 @@ export function renderList(spec, choices, state, multi) {
|
|
|
66
36
|
return [spec.message, ...rows].join('\n');
|
|
67
37
|
}
|
|
68
38
|
const clamp = (index, length) => (index + length) % length;
|
|
69
|
-
/**
|
|
70
|
-
* Apply a navigation key, and say whether anything changed — a key with no meaning here
|
|
71
|
-
* changes nothing and repaints nothing, which is what a person expects from a key they
|
|
72
|
-
* pressed by accident.
|
|
73
|
-
*/
|
|
74
39
|
function moved(key, state, length, multi) {
|
|
75
40
|
if (key === 'up') {
|
|
76
41
|
state.cursor = clamp(state.cursor - 1, length);
|
|
@@ -88,30 +53,24 @@ function moved(key, state, length, multi) {
|
|
|
88
53
|
state.selected.add(state.cursor);
|
|
89
54
|
return true;
|
|
90
55
|
}
|
|
91
|
-
/** What enter answers with. List order, not press order: a set of choices has no sequence. */
|
|
92
56
|
function chosen(choices, state, multi) {
|
|
93
57
|
if (!multi)
|
|
94
58
|
return choices[state.cursor]?.value ?? '';
|
|
95
59
|
return [...state.selected].sort((a, b) => a - b).map((index) => choices[index]?.value ?? '');
|
|
96
60
|
}
|
|
97
|
-
|
|
98
|
-
* Drive a list prompt with the arrow keys, repainting in place.
|
|
99
|
-
*
|
|
100
|
-
* Returns the same `Asked` shape `ask()` does, so a caller can swap the two without
|
|
101
|
-
* knowing which ran — and cancels the same way, because `Ctrl-C` in raw mode is a byte and
|
|
102
|
-
* not a signal, and a person who presses it means to leave.
|
|
103
|
-
*/
|
|
61
|
+
const asTerminal = (writer) => ({ write: (text) => writer.write(text), isTTY: true });
|
|
104
62
|
export async function askList(spec, io, multi = false) {
|
|
105
63
|
const choices = spec.choices ?? [];
|
|
106
64
|
const state = { cursor: 0, selected: new Set() };
|
|
107
65
|
let painted = 0;
|
|
108
66
|
const paint = () => {
|
|
109
67
|
const frame = renderList(spec, choices, state, multi);
|
|
110
|
-
io.writer.write((painted === 0 ?
|
|
68
|
+
io.writer.write((painted === 0 ? '' : erase(painted)) + frame);
|
|
111
69
|
painted = frame.split('\n').length;
|
|
112
70
|
};
|
|
113
71
|
io.keys.setRawMode?.(true);
|
|
114
72
|
io.keys.resume?.();
|
|
73
|
+
const restore = hideCursor(asTerminal(io.writer), exitHook);
|
|
115
74
|
paint();
|
|
116
75
|
try {
|
|
117
76
|
return await new Promise((resolve) => {
|
|
@@ -130,17 +89,15 @@ export async function askList(spec, io, multi = false) {
|
|
|
130
89
|
};
|
|
131
90
|
const done = (answer) => {
|
|
132
91
|
io.keys.off('data', onData);
|
|
133
|
-
|
|
134
|
-
// was found: a prompt that exits in raw mode leaves the shell unusable.
|
|
135
|
-
io.writer.write(`${erase(painted)}${renderList(spec, choices, state, multi)}\n${SHOW_CURSOR}`);
|
|
92
|
+
io.writer.write(`${erase(painted)}${renderList(spec, choices, state, multi)}\n`);
|
|
136
93
|
resolve(answer);
|
|
137
94
|
};
|
|
138
95
|
io.keys.on('data', onData);
|
|
139
96
|
});
|
|
140
97
|
}
|
|
141
98
|
finally {
|
|
99
|
+
restore();
|
|
142
100
|
io.keys.setRawMode?.(false);
|
|
143
101
|
io.keys.pause?.();
|
|
144
102
|
}
|
|
145
103
|
}
|
|
146
|
-
//# sourceMappingURL=raw.js.map
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The slice of the world caique needs, and the one file here that names `process` (Y9) —
|
|
3
|
+
* the same seam `paratext/src/runtime.ts` declares. `decide()` already took the fields it
|
|
4
|
+
* reads and `createIo()` already took its streams; this is the other half, so a program can
|
|
5
|
+
* get a real one without writing `process.stdin` itself.
|
|
6
|
+
*
|
|
7
|
+
* A **function**, for paratext's reason: a runtime built at import freezes the environment
|
|
8
|
+
* as it was when the module graph loaded, which is before a test can say what it wants.
|
|
9
|
+
* `runtime.test.ts` asks twice across a change, so a captured constant cannot pass.
|
|
10
|
+
*/
|
|
11
|
+
/**
|
|
12
|
+
* Declared structurally, not imported: burgee's `Runtime` satisfies it, so does a literal in
|
|
13
|
+
* a test. `decide.ts` names the narrower pair it reads for the same reason; this satisfies it.
|
|
14
|
+
*/
|
|
15
|
+
export interface Runtime {
|
|
16
|
+
/** `decide()` reads `CI`: set means nobody is there to type (R6). */
|
|
17
|
+
env: Record<string, string | undefined>;
|
|
18
|
+
stdin: NodeJS.ReadableStream & {
|
|
19
|
+
isTTY?: boolean;
|
|
20
|
+
};
|
|
21
|
+
stdout: NodeJS.WritableStream & {
|
|
22
|
+
isTTY?: boolean;
|
|
23
|
+
};
|
|
24
|
+
/** `stdin` decides whether a person can be asked at all; `stdout`, whether anything is drawn. */
|
|
25
|
+
isTTY: {
|
|
26
|
+
stdin: boolean;
|
|
27
|
+
stdout: boolean;
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
/** What a real process looks like. Callers that have not got one pass their own. */
|
|
31
|
+
export declare const processRuntime: () => Runtime;
|
package/dist/runtime.js
ADDED
package/dist/schema.json
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"$schema":"https://json-schema.org/draft/2020-12/schema","$id":"https://github.com/ofri-peretz/burgee/blob/main/packages/flagstaff/src/schema.json","title":"flagstaff plugin","description":"A plugin is one plain object. Everything in it is data that can be read without running it; the only functions allowed are a component's `static` (required) and `frame` (optional). A spinner or component without a static projection is refused at register().","type":"object","required":["name"],"additionalProperties":true,"properties":{"name":{"type":"string","minLength":1,"description":"The plugin's name; also the prefix a host may use when two plugins contribute the same key."},"contract":{"type":"integer","minimum":1,"description":"The plugin contract this object follows. A host refuses a newer contract than it knows."},"tokens":{"type":"object","description":"A roundel theme: semantic token name to a hex colour, contrast-checked when flown.","additionalProperties":{"type":"string","pattern":"^#[0-9a-fA-F]{6}$"},"propertyNames":{"enum":["error","warn","ok","hint","muted","command","flag","value","heading","ground"],"description":"roundel's ten semantic tokens, and nothing else — `roundel`'s own validate() refuses any other name."}},"glyphs":{"type":"object","description":"Symbols by meaning: `ok`, `fail`, `warn`, `info`, `running`. A plugin that ships glyphs changes every built-in that draws one.","additionalProperties":{"type":"string","minLength":1}},"spinners":{"type":"object","description":"Spinner styles by name, in cli-spinners' shape plus the static projection.","additionalProperties":{"$ref":"#/$defs/spinner"}},"borders":{"type":"object","description":"Border styles a box can be drawn with, by name.","additionalProperties":{"$ref":"#/$defs/border"}},"components":{"type":"object","description":"Components by name: `static(state)` returns the text a pipe, an agent or a screen reader gets; `frame(t, state)` is the optional animated form.","additionalProperties":{"$ref":"#/$defs/component"}},"capabilities":{"$ref":"#/$defs/capabilities"}},"$defs":{"spinner":{"type":"object","required":["frames","interval","static"],"properties":{"frames":{"type":"array","items":{"type":"string"},"minItems":1},"interval":{"type":"integer","minimum":1,"description":"Milliseconds between frames on a terminal."},"static":{"type":"string","description":"What a pipe prints instead of the animation."}}},"component":{"type":"object","required":["static"],"properties":{"static":{"description":"(state) => string. Required: the projection every non-terminal mode prints."},"frame":{"description":"(t, state) => string. Optional: the frame at t milliseconds since hoisting."},"sample":{"type":"object","required":["running","done"],"description":"Two states to *show* this component with: `flagstaff check` and the docs gallery render `running` then `done`. Omitted, they assume `{ phase: 'running' }` and `{ phase: 'done' }` and say so in the output. The loop never reads it — a running program's state comes from the program.","properties":{"running":{"description":"The state to open with."},"done":{"description":"The state to close with."}}},"interval":{"type":"integer","minimum":1,"description":"Milliseconds between repaints when `frame` is given; 80 when omitted."}}},"border":{"type":"object","required":["topLeft","top","topRight","left","right","bottomLeft","bottom","bottomRight"],"description":"cli-boxes' shape exactly, so that corpus imports unchanged.","properties":{"topLeft":{"type":"string"},"top":{"type":"string"},"topRight":{"type":"string"},"left":{"type":"string"},"right":{"type":"string"},"bottomLeft":{"type":"string"},"bottom":{"type":"string"},"bottomRight":{"type":"string"}}},"capability":{"type":"object","required":["name","osc","when","encode","fallback"],"additionalProperties":false,"description":"One paratext capability: what it says to the terminal, when the terminal is believed to understand it, and what prints when it does not. No functions, so it travels through JSON. `encode` and `fallback` are templates: `{field}` is the field's value, `{field|base64}` is it base64-encoded, and `[ … ]` is emitted only when every field inside it has a value.","properties":{"name":{"type":"string","minLength":1,"description":"How callers name it. Registering an existing name replaces it — how a caller corrects a guess we got wrong."},"osc":{"description":"The OSC code this speaks, or 'BEL' for the bell and for protocols that are not OSC at all, such as Kitty's.","oneOf":[{"type":"integer","minimum":0},{"const":"BEL"}]},"when":{"type":"object","description":"When the terminal is believed to understand it. Every clause must hold; `termProgram` and `envAny` are ORs within themselves. Guesses — no terminal answers 'do you do OSC 1337' — and therefore data a caller can replace.","additionalProperties":false,"properties":{"tty":{"type":"boolean","description":"Refuse a pipe. Almost always true: a file that receives OSC gets control bytes in it."},"termProgram":{"type":"array","items":{"type":"string"},"description":"Any one of these TERM_PROGRAM values."},"envAny":{"type":"array","items":{"type":"string"},"description":"Any one of these environment variables merely being set, as VTE announces itself."},"term":{"type":"string","description":"An exact TERM — Kitty is xterm-kitty."}}},"encode":{"type":"string","minLength":1,"description":"The bytes, as a template, for a terminal that does understand."},"fallback":{"type":"string","description":"What to print when it does not — PRINCIPLES rule 6. '' is a legitimate answer, a window title having nothing to say in a log; absence is not, which is why this is required and may be ''."}},"examples":[{"name":"kitty-image","osc":"BEL","when":{"tty":true,"term":"xterm-kitty"},"encode":"\u001b_Ga=T,f=100;{base64}\u001b\\","fallback":"{caption}"}]},"capabilities":{"type":"object","description":"paratext capabilities by name — the OSC section of a plugin, read the way flagstaff reads `spinners`.","additionalProperties":{"$ref":"#/$defs/capability"}},"capabilityDocument":{"description":"What paratext's `check()` accepts. Two shapes, and only one of them survives 1.0.","oneOf":[{"description":"The family shape: a plugin carrying its capabilities under `capabilities`.","type":"object","required":["name","capabilities"],"properties":{"name":{"type":"string","minLength":1},"contract":{"type":"integer","minimum":1},"capabilities":{"$ref":"#/$defs/capabilities"}}},{"deprecated":true,"description":"Deprecated: one capability as the whole document, the shape paratext's schema had before this one absorbed it. Accepted for one minor release, removed at 1.0 — `check()` validates it and says so.","$ref":"#/$defs/capability"}]}}}
|
package/dist/spec.d.ts
CHANGED
|
@@ -9,8 +9,29 @@
|
|
|
9
9
|
*
|
|
10
10
|
* Nothing here imports a runtime, a stream or a terminal. This file is data.
|
|
11
11
|
*/
|
|
12
|
-
/**
|
|
13
|
-
|
|
12
|
+
/**
|
|
13
|
+
* The six kinds a prompt can be — plus whatever a plugin adds.
|
|
14
|
+
*
|
|
15
|
+
* **Why `(string & {})` rather than a closed union** (`plugin-contract` R5, D5). A closed
|
|
16
|
+
* union makes a plugin's seventh kind a type error, so hosting `widgets` at all would be a
|
|
17
|
+
* breaking change written as an additive one: every caller of `caique/plugin` would need
|
|
18
|
+
* this file edited before it could name its own kind. The intersection keeps all six
|
|
19
|
+
* literals in an editor's completion list — which a bare `string` would throw away — while
|
|
20
|
+
* admitting the kinds `caique/plugin` renders.
|
|
21
|
+
*
|
|
22
|
+
* Anything more than a *kind* is still a wizard, which is out of scope. Widening the type
|
|
23
|
+
* does not widen the model: a prompt is one question with one answer, whoever draws it.
|
|
24
|
+
*/
|
|
25
|
+
export type PromptKind = 'text' | 'confirm' | 'select' | 'multiselect' | 'password' | 'path' | (string & {});
|
|
26
|
+
/**
|
|
27
|
+
* The six caique draws itself, as data rather than as a `switch` nobody can read back.
|
|
28
|
+
*
|
|
29
|
+
* `caique/plugin` needs this set to answer two questions a plugin makes askable for the
|
|
30
|
+
* first time: whether a kind is already spoken for, and which kinds a refusal should name.
|
|
31
|
+
* Deriving it from the widget dispatch in `ask.ts` would mean two lists that agree only by
|
|
32
|
+
* inspection, which is the drift the family's locks exist to prevent.
|
|
33
|
+
*/
|
|
34
|
+
export declare const BUILT_IN_KINDS: ReadonlySet<string>;
|
|
14
35
|
/** One choice in a `select` or `multiselect`. `value` is what the option receives. */
|
|
15
36
|
export interface Choice {
|
|
16
37
|
value: string;
|