@orkestrel/terminal 0.0.13 → 0.0.15
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 +11 -16
- package/dist/src/core/index.cjs +458 -281
- package/dist/src/core/index.cjs.map +1 -1
- package/dist/src/core/index.d.cts +728 -369
- package/dist/src/core/index.d.ts +728 -369
- package/dist/src/core/index.js +443 -264
- package/dist/src/core/index.js.map +1 -1
- package/dist/src/server/index.cjs +268 -258
- package/dist/src/server/index.cjs.map +1 -1
- package/dist/src/server/index.d.cts +217 -201
- package/dist/src/server/index.d.ts +217 -201
- package/dist/src/server/index.js +257 -242
- package/dist/src/server/index.js.map +1 -1
- package/package.json +18 -20
package/dist/src/core/index.js
CHANGED
|
@@ -1,48 +1,45 @@
|
|
|
1
|
-
import { STATUS_ICONS, createStyler, freezeStyle, strip, stripControls } from "@orkestrel/console";
|
|
1
|
+
import { CSI, ESC, STATUS_ICONS, createStyler, freezeStyle, strip, stripControls } from "@orkestrel/console";
|
|
2
2
|
import { arrayOf, attempt, isArray, isBoolean, isNonEmptyString, isNumber, isRecord, isString, literalOf, parseJSON, rawShape, recordOf, stringShape } from "@orkestrel/contract";
|
|
3
3
|
import { Emitter } from "@orkestrel/emitter";
|
|
4
4
|
import { createForm, isFieldError, isFormError, parseForm, serializeForm } from "@orkestrel/form";
|
|
5
5
|
import { createSSEParser } from "@orkestrel/sse";
|
|
6
6
|
import { createDatabase, createMemoryDriver } from "@orkestrel/database";
|
|
7
7
|
//#region src/core/constants.ts
|
|
8
|
-
/**
|
|
8
|
+
/** Names the carriage return byte (`\r`, U+000D) — Enter on most terminals. */
|
|
9
9
|
var RETURN = String.fromCharCode(13);
|
|
10
|
-
/**
|
|
10
|
+
/** Names the line feed byte (`\n`, U+000A) — Enter on some terminals / pasted input. */
|
|
11
11
|
var NEWLINE = String.fromCharCode(10);
|
|
12
|
-
/**
|
|
12
|
+
/** Names the tab byte (`\t`, U+0009). */
|
|
13
13
|
var TAB = String.fromCharCode(9);
|
|
14
|
-
/**
|
|
15
|
-
var ESCAPE = String.fromCharCode(27);
|
|
16
|
-
/** Backspace (BS, U+0008) — Ctrl+H / some terminals' Backspace. */
|
|
14
|
+
/** Names the backspace byte (BS, U+0008) — Ctrl+H / some terminals' Backspace. */
|
|
17
15
|
var BACKSPACE = String.fromCharCode(8);
|
|
18
|
-
/**
|
|
16
|
+
/** Names the delete byte (DEL, U+007F) — the usual Backspace byte on a Unix TTY. */
|
|
19
17
|
var DELETE = String.fromCharCode(127);
|
|
20
|
-
/**
|
|
18
|
+
/** Names the space byte (U+0020). */
|
|
21
19
|
var SPACE = " ";
|
|
22
|
-
/** Ctrl+C (ETX, U+0003) — interrupt / cancel. */
|
|
20
|
+
/** Names the Ctrl+C byte (ETX, U+0003) — interrupt / cancel. */
|
|
23
21
|
var CTRL_C = String.fromCharCode(3);
|
|
24
|
-
/** Ctrl+D (EOT, U+0004) — end-of-transmission / finish (the editor's commit key). */
|
|
22
|
+
/** Names the Ctrl+D byte (EOT, U+0004) — end-of-transmission / finish (the editor's commit key). */
|
|
25
23
|
var CTRL_D = String.fromCharCode(4);
|
|
26
|
-
/** Ctrl+U (NAK, U+0015) — clear the current line. */
|
|
24
|
+
/** Names the Ctrl+U byte (NAK, U+0015) — clear the current line. */
|
|
27
25
|
var CTRL_U = String.fromCharCode(21);
|
|
28
|
-
/** Ctrl+A (SOH, U+0001) — move to start of line. */
|
|
26
|
+
/** Names the Ctrl+A byte (SOH, U+0001) — move to start of line. */
|
|
29
27
|
var CTRL_A = String.fromCharCode(1);
|
|
30
|
-
/** Ctrl+E (ENQ, U+0005) — move to end of line. */
|
|
28
|
+
/** Names the Ctrl+E byte (ENQ, U+0005) — move to end of line. */
|
|
31
29
|
var CTRL_E = String.fromCharCode(5);
|
|
32
30
|
/**
|
|
33
|
-
*
|
|
34
|
-
*
|
|
35
|
-
*
|
|
36
|
-
*
|
|
31
|
+
* Names the Single Shift Three lead (`ESCO`) — the alternate arrow-key prefix some terminals emit
|
|
32
|
+
* (`ESC O A`). Built from the console module's own {@link import('@orkestrel/console').ESC}; the
|
|
33
|
+
* navigation keys' CSI lead is that module's {@link import('@orkestrel/console').CSI}, which this
|
|
34
|
+
* package reuses rather than redeclaring.
|
|
37
35
|
*/
|
|
38
|
-
var
|
|
39
|
-
/** The Single Shift Three lead (`ESCO`) — the alternate arrow-key prefix some terminals emit (`ESC O A`). */
|
|
40
|
-
var KEY_SS3 = `O`;
|
|
36
|
+
var KEY_SS3 = `${ESC}O`;
|
|
41
37
|
/**
|
|
42
|
-
*
|
|
43
|
-
* consults for the navigation
|
|
44
|
-
* form (`ESCOA`…) of the
|
|
45
|
-
* their numeric-tilde variants
|
|
38
|
+
* Holds the exact escape sequence to canonical key name table
|
|
39
|
+
* {@link import('./helpers.js').parseKey} consults for the navigation and editing keys. Covers the
|
|
40
|
+
* CSI form (`ESC[A`…) and the SS3 form (`ESCOA`…) of the arrows, plus the `home` / `end` / `delete`
|
|
41
|
+
* CSI sequences with their numeric-tilde variants. The source of truth for the multi-byte key
|
|
42
|
+
* decode; frozen.
|
|
46
43
|
*
|
|
47
44
|
* @remarks
|
|
48
45
|
* Terminals disagree on these: a cursor key is `ESC[A` (normal) or `ESCOA` (application mode),
|
|
@@ -51,32 +48,32 @@ var KEY_SS3 = `O`;
|
|
|
51
48
|
* sees the wire encoding.
|
|
52
49
|
*/
|
|
53
50
|
var SEQUENCE_NAMES = Object.freeze({
|
|
54
|
-
[`${
|
|
55
|
-
[`${
|
|
56
|
-
[`${
|
|
57
|
-
[`${
|
|
51
|
+
[`${CSI}A`]: "up",
|
|
52
|
+
[`${CSI}B`]: "down",
|
|
53
|
+
[`${CSI}C`]: "right",
|
|
54
|
+
[`${CSI}D`]: "left",
|
|
58
55
|
[`${KEY_SS3}A`]: "up",
|
|
59
56
|
[`${KEY_SS3}B`]: "down",
|
|
60
57
|
[`${KEY_SS3}C`]: "right",
|
|
61
58
|
[`${KEY_SS3}D`]: "left",
|
|
62
|
-
[`${
|
|
63
|
-
[`${
|
|
64
|
-
[`${
|
|
65
|
-
[`${
|
|
66
|
-
[`${
|
|
67
|
-
[`${
|
|
68
|
-
[`${
|
|
59
|
+
[`${CSI}H`]: "home",
|
|
60
|
+
[`${CSI}F`]: "end",
|
|
61
|
+
[`${CSI}1~`]: "home",
|
|
62
|
+
[`${CSI}4~`]: "end",
|
|
63
|
+
[`${CSI}3~`]: "delete",
|
|
64
|
+
[`${CSI}7~`]: "home",
|
|
65
|
+
[`${CSI}8~`]: "end"
|
|
69
66
|
});
|
|
70
67
|
/**
|
|
71
|
-
*
|
|
72
|
-
* consults for the one-byte keys and the two-byte CRLF
|
|
73
|
-
* canonical `name` and whether it is a `ctrl` combination. The
|
|
74
|
-
* frozen.
|
|
68
|
+
* Holds the control byte (or CRLF pair) to key descriptor table
|
|
69
|
+
* {@link import('./helpers.js').parseKey} consults for the one-byte keys and the two-byte CRLF
|
|
70
|
+
* Enter chunk. Each entry carries the canonical `name` and whether it is a `ctrl` combination. The
|
|
71
|
+
* source of truth for that decode; frozen.
|
|
75
72
|
*
|
|
76
73
|
* @remarks
|
|
77
74
|
* `return` / `newline` / `return + newline` all map to `return` (one canonical Enter name) — a
|
|
78
75
|
* terminal or a paste can deliver Enter as `\r`, `\n`, or the `\r\n` pair in one chunk;
|
|
79
|
-
* `delete` / `backspace` both map to `backspace` (the
|
|
76
|
+
* `delete` / `backspace` both map to `backspace` (the Backspace bytes); the Ctrl combos
|
|
80
77
|
* (`c` / `d` / `u` / `a` / `e`) carry `ctrl: true` so a reducer can match
|
|
81
78
|
* `key.ctrl && key.name === 'c'`. `escape` / `tab` / `space` are plain named keys.
|
|
82
79
|
*/
|
|
@@ -98,7 +95,7 @@ var CONTROL_NAMES = Object.freeze({
|
|
|
98
95
|
name: "tab",
|
|
99
96
|
ctrl: false
|
|
100
97
|
}),
|
|
101
|
-
[
|
|
98
|
+
[ESC]: Object.freeze({
|
|
102
99
|
name: "escape",
|
|
103
100
|
ctrl: false
|
|
104
101
|
}),
|
|
@@ -135,11 +132,12 @@ var CONTROL_NAMES = Object.freeze({
|
|
|
135
132
|
ctrl: true
|
|
136
133
|
})
|
|
137
134
|
});
|
|
138
|
-
/**
|
|
135
|
+
/** Names the default mask glyph {@link import('./helpers.js').createPasswordState} uses — `*`. */
|
|
139
136
|
var DEFAULT_MASK = "*";
|
|
140
137
|
/**
|
|
141
|
-
*
|
|
142
|
-
* the
|
|
138
|
+
* Holds the terminal-owned glyphs {@link DEFAULT_PROMPT_THEME} assembles its `icons` from, beside
|
|
139
|
+
* the console module's own success and error marks. Read only when the default theme is assembled;
|
|
140
|
+
* a view reads its resolved theme and never this constant. Frozen.
|
|
143
141
|
*
|
|
144
142
|
* @remarks
|
|
145
143
|
* - `question` — the leading mark on a prompt's message line.
|
|
@@ -156,7 +154,7 @@ var PROMPT_ICONS = Object.freeze({
|
|
|
156
154
|
unchecked: "☐"
|
|
157
155
|
});
|
|
158
156
|
/**
|
|
159
|
-
*
|
|
157
|
+
* Holds every {@link import('./types.js').PromptRole}, in one frozen list — the role axis's source of
|
|
160
158
|
* truth. {@link import('./helpers.js').createPromptTheme} walks it to merge a partial theme, and
|
|
161
159
|
* a consumer building a complete role map reads it rather than retyping every name.
|
|
162
160
|
*/
|
|
@@ -174,7 +172,7 @@ var PROMPT_ROLES = Object.freeze([
|
|
|
174
172
|
"description"
|
|
175
173
|
]);
|
|
176
174
|
/**
|
|
177
|
-
*
|
|
175
|
+
* Holds the {@link import('./types.js').PromptTheme} every prompt renders with unless its options supply
|
|
178
176
|
* another — the glyph set assembled from {@link PROMPT_ICONS} plus the console
|
|
179
177
|
* {@link import('@orkestrel/console').STATUS_ICONS} `success` / `error` marks, and the console
|
|
180
178
|
* {@link import('@orkestrel/console').Style} each role is painted with. Deeply frozen through the
|
|
@@ -228,30 +226,34 @@ var DEFAULT_PROMPT_THEME = Object.freeze({
|
|
|
228
226
|
description: freezeStyle({ attributes: ["dim"] })
|
|
229
227
|
})
|
|
230
228
|
});
|
|
231
|
-
/**
|
|
229
|
+
/** Holds how long (ms) the {@link import('./types.js').PromptInterface} broker parks an unanswered form before it expires — 5 minutes. */
|
|
232
230
|
var DEFAULT_PROMPT_TIMEOUT_MS = 3e5;
|
|
233
|
-
/**
|
|
231
|
+
/** Holds how long (ms) the {@link import('./types.js').PromptClientInterface} waits before each reconnect attempt — 2 seconds. */
|
|
234
232
|
var DEFAULT_RECONNECT_DELAY_MS = 2e3;
|
|
235
233
|
/**
|
|
236
|
-
*
|
|
237
|
-
*
|
|
234
|
+
* Holds the SSE `event:` names the broker emits and the
|
|
235
|
+
* {@link import('./types.js').PromptClientInterface} dispatches on — `pending`, `expire`, and
|
|
236
|
+
* `destroy`. Frozen; the source of truth for the wire event vocabulary.
|
|
238
237
|
*
|
|
239
238
|
* @remarks
|
|
240
239
|
* - `pending` — a serialized {@link import('./types.js').PendingForm} to dispatch and answer.
|
|
241
|
-
* - `expire` — an `{ id }` payload: the broker expired or released a parked
|
|
242
|
-
* - `
|
|
240
|
+
* - `expire` — an `{ id }` payload: the broker expired or released a parked form (the client drops it).
|
|
241
|
+
* - `destroy` — the broker is going away; the client disconnects (no auto-reconnect) but stays reusable.
|
|
243
242
|
*/
|
|
244
243
|
var SSE_EVENTS = Object.freeze({
|
|
245
244
|
pending: "pending",
|
|
246
245
|
expire: "expire",
|
|
247
|
-
|
|
246
|
+
destroy: "destroy"
|
|
248
247
|
});
|
|
249
|
-
/**
|
|
248
|
+
/**
|
|
249
|
+
* Names the auth-token request header the {@link import('./types.js').PromptClientInterface} sends
|
|
250
|
+
* when a `token` is configured — `x-orkestrel-token`.
|
|
251
|
+
*/
|
|
250
252
|
var HEADER_TOKEN = "x-orkestrel-token";
|
|
251
|
-
/**
|
|
253
|
+
/** Names the `Accept` header value that opens the broker's SSE stream — `text/event-stream`. */
|
|
252
254
|
var ACCEPT_EVENT_STREAM = "text/event-stream";
|
|
253
255
|
/**
|
|
254
|
-
*
|
|
256
|
+
* Sets the maximum number of characters the {@link import('./types.js').PromptClientInterface} lets its
|
|
255
257
|
* SSE parser buffer before treating the stream as hostile — 1 MiB, comfortably above any
|
|
256
258
|
* legitimate prompt payload. Passed as the `limit` to `createSSEParser` so an unterminated
|
|
257
259
|
* or oversized `data:` field cannot grow the buffer without bound (a memory-exhaustion guard).
|
|
@@ -260,7 +262,7 @@ var SSE_BUFFER_LIMIT = 1048576;
|
|
|
260
262
|
//#endregion
|
|
261
263
|
//#region src/core/errors.ts
|
|
262
264
|
/**
|
|
263
|
-
*
|
|
265
|
+
* Represents the error the terminal surfaces for its own refusals: parking on a destroyed or full broker, an
|
|
264
266
|
* unusable driver stream, a manager routing fault, or a ctrl-c cancellation. A parked form's own
|
|
265
267
|
* lifecycle failures reject through the form's `answer` with the form package's error, never with
|
|
266
268
|
* this one.
|
|
@@ -271,10 +273,17 @@ var SSE_BUFFER_LIMIT = 1048576;
|
|
|
271
273
|
* a caught value with {@link isTerminalError} and branch on `error.code`.
|
|
272
274
|
*/
|
|
273
275
|
var TerminalError = class extends Error {
|
|
274
|
-
/**
|
|
276
|
+
/** Holds the machine-readable condition — see {@link TerminalErrorCode}. */
|
|
275
277
|
code;
|
|
276
|
-
/**
|
|
278
|
+
/** Holds an optional context bag naming the offending values — see the class {@link TerminalError remarks}. */
|
|
277
279
|
context;
|
|
280
|
+
/**
|
|
281
|
+
* Builds one terminal refusal.
|
|
282
|
+
*
|
|
283
|
+
* @param code - The machine-readable {@link TerminalErrorCode} a caller branches on
|
|
284
|
+
* @param message - The human-readable reason
|
|
285
|
+
* @param context - The optional bag naming the offending values — see the class {@link TerminalError remarks}
|
|
286
|
+
*/
|
|
278
287
|
constructor(code, message, context) {
|
|
279
288
|
super(message);
|
|
280
289
|
this.name = "TerminalError";
|
|
@@ -283,10 +292,10 @@ var TerminalError = class extends Error {
|
|
|
283
292
|
}
|
|
284
293
|
};
|
|
285
294
|
/**
|
|
286
|
-
*
|
|
295
|
+
* Narrows an unknown caught value to a {@link TerminalError}, so a caller can branch on its `code`.
|
|
287
296
|
*
|
|
288
297
|
* @param value - The value to test (typically a `catch` binding or a rejected prompt call)
|
|
289
|
-
* @returns
|
|
298
|
+
* @returns True if `value` is a {@link TerminalError}; false otherwise
|
|
290
299
|
*
|
|
291
300
|
* @example
|
|
292
301
|
* ```ts
|
|
@@ -304,17 +313,23 @@ function isTerminalError(value) {
|
|
|
304
313
|
}
|
|
305
314
|
//#endregion
|
|
306
315
|
//#region src/core/validators.ts
|
|
307
|
-
/**
|
|
316
|
+
/**
|
|
317
|
+
* Narrows an unknown value to a {@link PendingFormStatus}.
|
|
318
|
+
*
|
|
319
|
+
* @param value - The candidate ticket status
|
|
320
|
+
* @returns True if the value is one of the declared ticket statuses; false otherwise
|
|
321
|
+
*/
|
|
308
322
|
var isPendingFormStatus = literalOf("pending", "answered", "expired");
|
|
309
323
|
/**
|
|
310
|
-
*
|
|
324
|
+
* Narrows an unknown wire value to a {@link PendingForm} envelope — the envelope alone, because the
|
|
325
|
+
* form package's `parseForm` owns the schema payload.
|
|
311
326
|
*
|
|
312
327
|
* @remarks
|
|
313
328
|
* This guard checks the transport record and proves only that `schema` is a record. The Form
|
|
314
329
|
* package's `parseForm` owns the schema payload and its semantic audit.
|
|
315
330
|
*
|
|
316
331
|
* @param value - The decoded wire value to inspect
|
|
317
|
-
* @returns
|
|
332
|
+
* @returns True if the value is a complete pending-form envelope; false otherwise
|
|
318
333
|
*/
|
|
319
334
|
function isPendingForm(value) {
|
|
320
335
|
return recordOf({
|
|
@@ -327,17 +342,24 @@ function isPendingForm(value) {
|
|
|
327
342
|
}, ["from", "to"])(value);
|
|
328
343
|
}
|
|
329
344
|
/**
|
|
330
|
-
*
|
|
345
|
+
* Narrows an unknown value to a transport-neutral {@link WireEvent} — the guard a consumer's own
|
|
346
|
+
* transport applies to an inbound frame.
|
|
331
347
|
*
|
|
332
348
|
* @param value - The candidate wire event
|
|
333
|
-
* @returns
|
|
349
|
+
* @returns True if the value carries an event name, serialized data, and an optional id; false otherwise
|
|
334
350
|
*/
|
|
335
351
|
var isWireEvent = recordOf({
|
|
336
352
|
event: isString,
|
|
337
353
|
data: isString,
|
|
338
354
|
id: isString
|
|
339
355
|
}, ["id"]);
|
|
340
|
-
/**
|
|
356
|
+
/**
|
|
357
|
+
* Narrows an unknown value to a {@link TerminalSnapshot} — a non-empty `id` and an optional numeric
|
|
358
|
+
* `timeout`, the read boundary a store applies to an untrusted persisted row.
|
|
359
|
+
*
|
|
360
|
+
* @param value - The candidate snapshot read back from storage
|
|
361
|
+
* @returns True if the value carries a non-empty `id` and an optional numeric `timeout`; false otherwise
|
|
362
|
+
*/
|
|
341
363
|
var isTerminalSnapshot = recordOf({
|
|
342
364
|
id: isNonEmptyString,
|
|
343
365
|
timeout: isNumber
|
|
@@ -345,24 +367,24 @@ var isTerminalSnapshot = recordOf({
|
|
|
345
367
|
//#endregion
|
|
346
368
|
//#region src/core/helpers.ts
|
|
347
369
|
/**
|
|
348
|
-
*
|
|
349
|
-
* read as UTF-8; the resulting string is matched against the known control bytes and the CRLF
|
|
350
|
-
*
|
|
351
|
-
*
|
|
370
|
+
* Decodes one keypress's bytes into a {@link KeyEvent} — total, never throws. A `Uint8Array` is
|
|
371
|
+
* read as UTF-8; the resulting string is matched against the known control bytes and the CRLF pair
|
|
372
|
+
* ({@link CONTROL_NAMES}) and escape sequences ({@link SEQUENCE_NAMES}), falling back to a single
|
|
373
|
+
* printable character. An unrecognized sequence carries no `name`, with the raw `sequence`
|
|
352
374
|
* preserved.
|
|
353
375
|
*
|
|
354
376
|
* @remarks
|
|
355
377
|
* - **Single control byte.** A one-character control input (`return` / `backspace` / `tab` /
|
|
356
378
|
* `escape` / `space`, or a Ctrl combo `c` / `d` / `u` / `a` / `e`), or the two-byte `\r\n`
|
|
357
379
|
* CRLF pair, is looked up in {@link CONTROL_NAMES}, carrying its `ctrl` flag.
|
|
358
|
-
* - **Escape sequence.** A multi-byte ESC sequence (`up` / `down` / `left` / `right` in
|
|
380
|
+
* - **Escape sequence.** A multi-byte ESC sequence (`up` / `down` / `left` / `right` in both the
|
|
359
381
|
* `ESC[A` and `ESCOA` forms, plus `home` / `end` / `delete`) is looked up in
|
|
360
382
|
* {@link SEQUENCE_NAMES} and flagged `meta`.
|
|
361
383
|
* - **Printable character.** A single printable character becomes `name` = that character, with
|
|
362
384
|
* `shift` set when it is an uppercase letter. A multi-code-point printable (an emoji, a pasted
|
|
363
385
|
* run) keeps its first code point as the name and the whole input as `sequence`.
|
|
364
|
-
* - **Unknown.** Anything else (an unrecognized escape, an empty input) yields
|
|
365
|
-
* total, so the driver never crashes on a stray byte.
|
|
386
|
+
* - **Unknown.** Anything else (an unrecognized escape, an empty input) yields an event with NO
|
|
387
|
+
* `name` — absence, never an empty string — total, so the driver never crashes on a stray byte.
|
|
366
388
|
*
|
|
367
389
|
* @param input - The raw keypress bytes, as a string or `Uint8Array`
|
|
368
390
|
* @returns The decoded {@link KeyEvent}
|
|
@@ -403,14 +425,19 @@ function parseKey(input) {
|
|
|
403
425
|
shift: first !== first.toLowerCase()
|
|
404
426
|
};
|
|
405
427
|
return {
|
|
406
|
-
name: "",
|
|
407
428
|
sequence,
|
|
408
429
|
ctrl: false,
|
|
409
430
|
meta: false,
|
|
410
431
|
shift: false
|
|
411
432
|
};
|
|
412
433
|
}
|
|
413
|
-
/**
|
|
434
|
+
/**
|
|
435
|
+
* Checks whether a single character is printable — the fallback test {@link parseKey} applies after
|
|
436
|
+
* the control bytes and the escape sequences, so the C0 controls and DEL are excluded.
|
|
437
|
+
*
|
|
438
|
+
* @param character - The single character to test
|
|
439
|
+
* @returns True if the character is at or above space and is not DEL; false otherwise
|
|
440
|
+
*/
|
|
414
441
|
function isPrintable(character) {
|
|
415
442
|
if (character.length === 0) return false;
|
|
416
443
|
const code = character.codePointAt(0);
|
|
@@ -418,7 +445,7 @@ function isPrintable(character) {
|
|
|
418
445
|
return code >= 32 && code !== 127;
|
|
419
446
|
}
|
|
420
447
|
/**
|
|
421
|
-
*
|
|
448
|
+
* Builds a complete {@link PromptTheme} by merging a partial one over
|
|
422
449
|
* {@link DEFAULT_PROMPT_THEME}, leaf by leaf — each supplied icon replaces that glyph, each
|
|
423
450
|
* supplied role replaces that {@link Style}, and everything else keeps its default. Each supplied
|
|
424
451
|
* style is snapshotted through the console module's own
|
|
@@ -453,7 +480,7 @@ function createPromptTheme(options) {
|
|
|
453
480
|
});
|
|
454
481
|
}
|
|
455
482
|
/**
|
|
456
|
-
*
|
|
483
|
+
* Sanitizes text for one single-line display slot. Composes console's ANSI {@link strip} and C0
|
|
457
484
|
* {@link stripControls} passes with removal of tab, line feed, and carriage return.
|
|
458
485
|
*
|
|
459
486
|
* @param text - The text to sanitize for a glyph or hint slot
|
|
@@ -468,7 +495,8 @@ function sanitizeDisplayText(text) {
|
|
|
468
495
|
return stripControls(strip(text)).replaceAll(" ", "").replaceAll("\n", "").replaceAll("\r", "");
|
|
469
496
|
}
|
|
470
497
|
/**
|
|
471
|
-
*
|
|
498
|
+
* Sanitizes every terminal-readable string in a parsed form schema, keeping every identity and
|
|
499
|
+
* answer string verbatim and dropping field metadata.
|
|
472
500
|
*
|
|
473
501
|
* @remarks
|
|
474
502
|
* Display strings pass through {@link sanitizeDisplayText}: labels, help, placeholders, masks,
|
|
@@ -510,6 +538,7 @@ function sanitizeSchema(schema) {
|
|
|
510
538
|
switch (field.control) {
|
|
511
539
|
case "text":
|
|
512
540
|
case "editor":
|
|
541
|
+
case "number":
|
|
513
542
|
fields.push({
|
|
514
543
|
...field,
|
|
515
544
|
...shared,
|
|
@@ -523,22 +552,10 @@ function sanitizeSchema(schema) {
|
|
|
523
552
|
...field.mask !== void 0 ? { mask: sanitizeDisplayText(field.mask) } : {}
|
|
524
553
|
});
|
|
525
554
|
break;
|
|
526
|
-
case "number":
|
|
527
|
-
fields.push({
|
|
528
|
-
...field,
|
|
529
|
-
...shared,
|
|
530
|
-
...field.placeholder !== void 0 ? { placeholder: sanitizeDisplayText(field.placeholder) } : {}
|
|
531
|
-
});
|
|
532
|
-
break;
|
|
533
555
|
case "date":
|
|
534
556
|
case "time":
|
|
535
557
|
case "datetime":
|
|
536
558
|
case "color":
|
|
537
|
-
fields.push({
|
|
538
|
-
...field,
|
|
539
|
-
...shared
|
|
540
|
-
});
|
|
541
|
-
break;
|
|
542
559
|
case "confirm":
|
|
543
560
|
fields.push({
|
|
544
561
|
...field,
|
|
@@ -546,17 +563,6 @@ function sanitizeSchema(schema) {
|
|
|
546
563
|
});
|
|
547
564
|
break;
|
|
548
565
|
case "select":
|
|
549
|
-
fields.push({
|
|
550
|
-
...field,
|
|
551
|
-
...shared,
|
|
552
|
-
choices: field.choices.map((choice) => ({
|
|
553
|
-
...choice,
|
|
554
|
-
value: choice.value,
|
|
555
|
-
label: sanitizeDisplayText(choice.label),
|
|
556
|
-
...choice.help !== void 0 ? { help: sanitizeDisplayText(choice.help) } : {}
|
|
557
|
-
}))
|
|
558
|
-
});
|
|
559
|
-
break;
|
|
560
566
|
case "checkbox":
|
|
561
567
|
fields.push({
|
|
562
568
|
...field,
|
|
@@ -585,7 +591,7 @@ function sanitizeSchema(schema) {
|
|
|
585
591
|
};
|
|
586
592
|
}
|
|
587
593
|
/**
|
|
588
|
-
*
|
|
594
|
+
* Sanitizes every glyph a wire-supplied {@link PromptThemeOptions} carries for a single-line display
|
|
589
595
|
* slot. Only the icons need it: a role is guard-narrowed to a console {@link Style}, whose colors
|
|
590
596
|
* and attributes are fixed name sets, so no role can carry a byte a terminal would act on.
|
|
591
597
|
*
|
|
@@ -602,13 +608,21 @@ function sanitizeThemeIcons(theme) {
|
|
|
602
608
|
icons: sanitized
|
|
603
609
|
};
|
|
604
610
|
}
|
|
605
|
-
/**
|
|
606
|
-
|
|
611
|
+
/**
|
|
612
|
+
* Renders the styled question header (`? message`) — the leading line every active prompt view
|
|
613
|
+
* shares, themed by the `question` + `message` roles.
|
|
614
|
+
*
|
|
615
|
+
* @param styler - The console styler that renders each role
|
|
616
|
+
* @param theme - The resolved prompt theme
|
|
617
|
+
* @param message - The prompt's question text
|
|
618
|
+
* @returns The rendered question header
|
|
619
|
+
*/
|
|
620
|
+
function renderPromptHeader(styler, theme, message) {
|
|
607
621
|
return `${styler.render(theme.roles.question, theme.icons.question)} ${styler.render(theme.roles.message, message)}`;
|
|
608
622
|
}
|
|
609
623
|
/**
|
|
610
|
-
*
|
|
611
|
-
* when no hint is supplied.
|
|
624
|
+
* Renders a question header followed by a key hint painted with the `hint` role, or the header
|
|
625
|
+
* alone when no hint is supplied.
|
|
612
626
|
*
|
|
613
627
|
* @param styler - The console styler that renders each role
|
|
614
628
|
* @param theme - The resolved prompt theme
|
|
@@ -616,20 +630,37 @@ function promptHeader(styler, theme, message) {
|
|
|
616
630
|
* @param hint - The optional key hint to append
|
|
617
631
|
* @returns The rendered header with the optional hint
|
|
618
632
|
*/
|
|
619
|
-
function
|
|
620
|
-
const head =
|
|
633
|
+
function renderHintedHeader(styler, theme, message, hint) {
|
|
634
|
+
const head = renderPromptHeader(styler, theme, message);
|
|
621
635
|
return hint === void 0 ? head : `${head} ${styler.render(theme.roles.hint, hint)}`;
|
|
622
636
|
}
|
|
623
|
-
/**
|
|
624
|
-
|
|
637
|
+
/**
|
|
638
|
+
* Renders the styled submit line (`✔ message`) — the committed header an interactive prompt shows
|
|
639
|
+
* after it resolves, themed by the `success` + `message` roles.
|
|
640
|
+
*
|
|
641
|
+
* @param styler - The console styler that renders each role
|
|
642
|
+
* @param theme - The resolved prompt theme
|
|
643
|
+
* @param message - The settled prompt's question text
|
|
644
|
+
* @returns The rendered committed header
|
|
645
|
+
*/
|
|
646
|
+
function renderSubmitHeader(styler, theme, message) {
|
|
625
647
|
return `${styler.render(theme.roles.success, theme.icons.success)} ${styler.render(theme.roles.message, message)}`;
|
|
626
648
|
}
|
|
627
|
-
/**
|
|
628
|
-
|
|
649
|
+
/**
|
|
650
|
+
* Renders the styled failure line (`✖ message`) a form driver writes for each refused field before
|
|
651
|
+
* it asks that field again.
|
|
652
|
+
*
|
|
653
|
+
* @param styler - The console styler that renders each role
|
|
654
|
+
* @param theme - The resolved prompt theme
|
|
655
|
+
* @param message - The failure text, normally the field's label and the reason
|
|
656
|
+
* @returns The rendered failure line
|
|
657
|
+
*/
|
|
658
|
+
function renderErrorLine(styler, theme, message) {
|
|
629
659
|
return `${styler.render(theme.roles.error, theme.icons.error)} ${styler.render(theme.roles.error, message)}`;
|
|
630
660
|
}
|
|
631
661
|
/**
|
|
632
|
-
*
|
|
662
|
+
* Builds the initial text-field reducer state — the sanitized label, the declared default, the
|
|
663
|
+
* styler, and the resolved theme.
|
|
633
664
|
*
|
|
634
665
|
* @param field - The text field to render
|
|
635
666
|
* @param styler - The styler used to render the view
|
|
@@ -645,22 +676,32 @@ function createInputState(field, styler = createStyler(), theme) {
|
|
|
645
676
|
value: ""
|
|
646
677
|
};
|
|
647
678
|
}
|
|
648
|
-
/**
|
|
649
|
-
|
|
679
|
+
/**
|
|
680
|
+
* Renders a text-field reducer state as a styled view — the header, the pointer, and the typed
|
|
681
|
+
* value, or the default shown as a hint while nothing is typed.
|
|
682
|
+
*
|
|
683
|
+
* @param state - The text field's current reducer state
|
|
684
|
+
* @returns The rendered single-line view — header, pointer, and the typed value or the default
|
|
685
|
+
*/
|
|
686
|
+
function renderInputView(state) {
|
|
650
687
|
const content = state.value.length > 0 ? state.value : state.default;
|
|
651
688
|
const role = state.value.length > 0 ? state.theme.roles.content : state.theme.roles.hint;
|
|
652
689
|
const shown = state.styler.render(role, sanitizeDisplayText(content));
|
|
653
|
-
return `${
|
|
690
|
+
return `${renderPromptHeader(state.styler, state.theme, state.message)} ${state.styler.render(state.theme.roles.pointer, state.theme.icons.pointer)} ${shown}`;
|
|
654
691
|
}
|
|
655
692
|
/**
|
|
656
|
-
*
|
|
693
|
+
* Advances an input prompt by one {@link KeyEvent} — the pure `(state, key) → PromptStep<string>`
|
|
657
694
|
* reducer. Printable characters extend the value; backspace shrinks it; ctrl-u clears it; ctrl-c
|
|
658
695
|
* cancels; return produces the candidate value, with an empty line falling back to the default.
|
|
696
|
+
*
|
|
697
|
+
* @param state - The text field's current reducer state
|
|
698
|
+
* @param key - The decoded keypress to apply
|
|
699
|
+
* @returns The next step — the new state, the rendered view, the status, and the value on submit
|
|
659
700
|
*/
|
|
660
|
-
function
|
|
701
|
+
function reduceInput(state, key) {
|
|
661
702
|
if (key.ctrl && key.name === "c") return {
|
|
662
703
|
state,
|
|
663
|
-
view:
|
|
704
|
+
view: renderInputView(state),
|
|
664
705
|
status: "cancel"
|
|
665
706
|
};
|
|
666
707
|
if (key.name === "return") {
|
|
@@ -670,7 +711,7 @@ function inputReduce(state, key) {
|
|
|
670
711
|
...state,
|
|
671
712
|
value: answer
|
|
672
713
|
},
|
|
673
|
-
view: `${
|
|
714
|
+
view: `${renderSubmitHeader(state.styler, state.theme, state.message)} ${state.styler.render(state.theme.roles.hint, sanitizeDisplayText(answer))}`,
|
|
674
715
|
status: "submit",
|
|
675
716
|
value: answer
|
|
676
717
|
};
|
|
@@ -678,7 +719,7 @@ function inputReduce(state, key) {
|
|
|
678
719
|
const value = editLine(state.value, key);
|
|
679
720
|
if (value === void 0) return {
|
|
680
721
|
state,
|
|
681
|
-
view:
|
|
722
|
+
view: renderInputView(state),
|
|
682
723
|
status: "active"
|
|
683
724
|
};
|
|
684
725
|
const next = {
|
|
@@ -687,12 +728,13 @@ function inputReduce(state, key) {
|
|
|
687
728
|
};
|
|
688
729
|
return {
|
|
689
730
|
state: next,
|
|
690
|
-
view:
|
|
731
|
+
view: renderInputView(next),
|
|
691
732
|
status: "active"
|
|
692
733
|
};
|
|
693
734
|
}
|
|
694
735
|
/**
|
|
695
|
-
*
|
|
736
|
+
* Builds the initial password-field reducer state — the text-field state, plus the mask glyph each
|
|
737
|
+
* typed character renders as.
|
|
696
738
|
*
|
|
697
739
|
* @param field - The password field to render
|
|
698
740
|
* @param styler - The styler used to render the view
|
|
@@ -708,32 +750,42 @@ function createPasswordState(field, styler = createStyler(), theme) {
|
|
|
708
750
|
value: ""
|
|
709
751
|
};
|
|
710
752
|
}
|
|
711
|
-
/**
|
|
712
|
-
|
|
753
|
+
/**
|
|
754
|
+
* Renders a password-field reducer state as a styled view, with the value replaced by the mask
|
|
755
|
+
* repeated so the secret is never echoed.
|
|
756
|
+
*
|
|
757
|
+
* @param state - The password field's current reducer state
|
|
758
|
+
* @returns The rendered view, with the mask repeated in place of the typed value
|
|
759
|
+
*/
|
|
760
|
+
function renderPasswordView(state) {
|
|
713
761
|
const masked = state.styler.render(state.theme.roles.content, state.mask.repeat(state.value.length));
|
|
714
|
-
return `${
|
|
762
|
+
return `${renderPromptHeader(state.styler, state.theme, state.message)} ${state.styler.render(state.theme.roles.pointer, state.theme.icons.pointer)} ${masked}`;
|
|
715
763
|
}
|
|
716
764
|
/**
|
|
717
|
-
*
|
|
718
|
-
* reducer. Identical line-editing to {@link
|
|
765
|
+
* Advances a password prompt by one {@link KeyEvent} — the pure `(state, key) → PromptStep<string>`
|
|
766
|
+
* reducer. Identical line-editing to {@link reduceInput} (printable extends, backspace shrinks,
|
|
719
767
|
* ctrl-u clears, ctrl-c cancels) but the view masks the value. Return produces the candidate value.
|
|
768
|
+
*
|
|
769
|
+
* @param state - The password field's current reducer state
|
|
770
|
+
* @param key - The decoded keypress to apply
|
|
771
|
+
* @returns The next step — the new state, the masked view, the status, and the value on submit
|
|
720
772
|
*/
|
|
721
|
-
function
|
|
773
|
+
function reducePassword(state, key) {
|
|
722
774
|
if (key.ctrl && key.name === "c") return {
|
|
723
775
|
state,
|
|
724
|
-
view:
|
|
776
|
+
view: renderPasswordView(state),
|
|
725
777
|
status: "cancel"
|
|
726
778
|
};
|
|
727
779
|
if (key.name === "return") return {
|
|
728
780
|
state,
|
|
729
|
-
view: `${
|
|
781
|
+
view: `${renderSubmitHeader(state.styler, state.theme, state.message)} ${state.styler.render(state.theme.roles.hint, state.mask.repeat(state.value.length))}`,
|
|
730
782
|
status: "submit",
|
|
731
783
|
value: state.value
|
|
732
784
|
};
|
|
733
785
|
const value = editLine(state.value, key);
|
|
734
786
|
if (value === void 0) return {
|
|
735
787
|
state,
|
|
736
|
-
view:
|
|
788
|
+
view: renderPasswordView(state),
|
|
737
789
|
status: "active"
|
|
738
790
|
};
|
|
739
791
|
const next = {
|
|
@@ -742,12 +794,13 @@ function passwordReduce(state, key) {
|
|
|
742
794
|
};
|
|
743
795
|
return {
|
|
744
796
|
state: next,
|
|
745
|
-
view:
|
|
797
|
+
view: renderPasswordView(next),
|
|
746
798
|
status: "active"
|
|
747
799
|
};
|
|
748
800
|
}
|
|
749
801
|
/**
|
|
750
|
-
*
|
|
802
|
+
* Builds the initial confirm-field reducer state — the sanitized label and the declared default
|
|
803
|
+
* answer.
|
|
751
804
|
*
|
|
752
805
|
* @param field - The confirm field to render
|
|
753
806
|
* @param styler - The styler used to render the view
|
|
@@ -763,43 +816,52 @@ function createConfirmState(field, styler = createStyler(), theme) {
|
|
|
763
816
|
};
|
|
764
817
|
}
|
|
765
818
|
/**
|
|
766
|
-
*
|
|
819
|
+
* Renders a confirm-field reducer state as a styled view — the header and the yes/no group, with
|
|
820
|
+
* the default letter capitalized and painted by the `selected` role.
|
|
821
|
+
*
|
|
822
|
+
* @param state - The confirm field's current reducer state
|
|
823
|
+
* @returns The rendered view — the header and the yes/no group with the default capitalized
|
|
767
824
|
*/
|
|
768
|
-
function
|
|
769
|
-
const head =
|
|
825
|
+
function renderConfirmView(state) {
|
|
826
|
+
const head = renderPromptHeader(state.styler, state.theme, state.message);
|
|
770
827
|
const answer = state.default ? `${state.styler.render(state.theme.roles.selected, "Y")}${state.styler.render(state.theme.roles.hint, "/n")}` : `${state.styler.render(state.theme.roles.hint, "y/")}${state.styler.render(state.theme.roles.selected, "N")}`;
|
|
771
828
|
return `${head} ${state.styler.render(state.theme.roles.hint, "(")}${answer}${state.styler.render(state.theme.roles.hint, ")")}`;
|
|
772
829
|
}
|
|
773
830
|
/**
|
|
774
|
-
*
|
|
831
|
+
* Advances a confirm prompt by one {@link KeyEvent} — the pure `(state, key) → PromptStep<boolean>`
|
|
775
832
|
* reducer. `y` / `Y` submits `true`, `n` / `N` submits `false`, return on an empty line submits
|
|
776
833
|
* the `default`, ctrl-c cancels; any other key is ignored (stays active).
|
|
834
|
+
*
|
|
835
|
+
* @param state - The confirm field's current reducer state
|
|
836
|
+
* @param key - The decoded keypress to apply
|
|
837
|
+
* @returns The next step — the state, the rendered view, the status, and the answer on submit
|
|
777
838
|
*/
|
|
778
|
-
function
|
|
839
|
+
function reduceConfirm(state, key) {
|
|
779
840
|
if (key.ctrl && key.name === "c") return {
|
|
780
841
|
state,
|
|
781
|
-
view:
|
|
842
|
+
view: renderConfirmView(state),
|
|
782
843
|
status: "cancel"
|
|
783
844
|
};
|
|
784
845
|
let answer;
|
|
785
|
-
const choice = key.name
|
|
846
|
+
const choice = key.name?.toLowerCase();
|
|
786
847
|
if (key.name === "return") answer = state.default;
|
|
787
848
|
else if (choice === "y") answer = true;
|
|
788
849
|
else if (choice === "n") answer = false;
|
|
789
850
|
if (answer === void 0) return {
|
|
790
851
|
state,
|
|
791
|
-
view:
|
|
852
|
+
view: renderConfirmView(state),
|
|
792
853
|
status: "active"
|
|
793
854
|
};
|
|
794
855
|
return {
|
|
795
856
|
state,
|
|
796
|
-
view: `${
|
|
857
|
+
view: `${renderSubmitHeader(state.styler, state.theme, state.message)} ${state.styler.render(state.theme.roles.hint, answer ? "yes" : "no")}`,
|
|
797
858
|
status: "submit",
|
|
798
859
|
value: answer
|
|
799
860
|
};
|
|
800
861
|
}
|
|
801
862
|
/**
|
|
802
|
-
*
|
|
863
|
+
* Builds the initial select-field reducer state — the offered choices, with the focus pre-placed on
|
|
864
|
+
* the declared default.
|
|
803
865
|
*
|
|
804
866
|
* @param field - The select field to render
|
|
805
867
|
* @param styler - The styler used to render the view
|
|
@@ -817,30 +879,40 @@ function createSelectState(field, styler = createStyler(), theme) {
|
|
|
817
879
|
focused: index >= 0 ? index : 0
|
|
818
880
|
};
|
|
819
881
|
}
|
|
820
|
-
/**
|
|
821
|
-
|
|
882
|
+
/**
|
|
883
|
+
* Renders a select-field reducer state as a multi-line styled view — one row per choice, with the
|
|
884
|
+
* focused row marked and its help shown.
|
|
885
|
+
*
|
|
886
|
+
* @param state - The select field's current reducer state
|
|
887
|
+
* @returns The rendered view — the header, then one row per choice with the focused row marked
|
|
888
|
+
*/
|
|
889
|
+
function renderSelectView(state) {
|
|
822
890
|
const lines = state.choices.map((choice, index) => {
|
|
823
891
|
const active = index === state.focused;
|
|
824
892
|
return `${active ? state.styler.render(state.theme.roles.pointer, state.theme.icons.pointer) : " "} ${active ? state.styler.render(state.theme.roles.selected, state.theme.icons.selected) : state.styler.render(state.theme.roles.muted, state.theme.icons.dot)} ${active ? state.styler.render(state.theme.roles.focus, choice.label) : state.styler.render(state.theme.roles.content, choice.label)}${choice.help === void 0 ? "" : ` ${state.styler.render(state.theme.roles.description, choice.help)}`}`;
|
|
825
893
|
});
|
|
826
|
-
return [
|
|
894
|
+
return [renderPromptHeader(state.styler, state.theme, state.message), ...lines].join("\n");
|
|
827
895
|
}
|
|
828
896
|
/**
|
|
829
|
-
*
|
|
830
|
-
* reducer. `up` / `down` (and `k` / `j`) move the focus,
|
|
897
|
+
* Advances a select prompt by one {@link KeyEvent} — the pure `(state, key) → PromptStep<string>`
|
|
898
|
+
* reducer. `up` / `down` (and `k` / `j`) move the focus, wrapping at the ends; return submits the
|
|
831
899
|
* focused choice's `value`; ctrl-c cancels. An empty choice list can never submit (a higher layer
|
|
832
900
|
* guards against it); any other key is ignored.
|
|
901
|
+
*
|
|
902
|
+
* @param state - The select field's current reducer state
|
|
903
|
+
* @param key - The decoded keypress to apply
|
|
904
|
+
* @returns The next step — the state, the rendered view, the status, and the chosen value on submit
|
|
833
905
|
*/
|
|
834
|
-
function
|
|
906
|
+
function reduceSelect(state, key) {
|
|
835
907
|
if (key.ctrl && key.name === "c") return {
|
|
836
908
|
state,
|
|
837
|
-
view:
|
|
909
|
+
view: renderSelectView(state),
|
|
838
910
|
status: "cancel"
|
|
839
911
|
};
|
|
840
912
|
const count = state.choices.length;
|
|
841
913
|
if (count === 0) return {
|
|
842
914
|
state,
|
|
843
|
-
view:
|
|
915
|
+
view: renderSelectView(state),
|
|
844
916
|
status: "active"
|
|
845
917
|
};
|
|
846
918
|
if (key.name === "up" || key.name === "k") {
|
|
@@ -850,7 +922,7 @@ function selectReduce(state, key) {
|
|
|
850
922
|
};
|
|
851
923
|
return {
|
|
852
924
|
state: next,
|
|
853
|
-
view:
|
|
925
|
+
view: renderSelectView(next),
|
|
854
926
|
status: "active"
|
|
855
927
|
};
|
|
856
928
|
}
|
|
@@ -861,7 +933,7 @@ function selectReduce(state, key) {
|
|
|
861
933
|
};
|
|
862
934
|
return {
|
|
863
935
|
state: next,
|
|
864
|
-
view:
|
|
936
|
+
view: renderSelectView(next),
|
|
865
937
|
status: "active"
|
|
866
938
|
};
|
|
867
939
|
}
|
|
@@ -870,19 +942,20 @@ function selectReduce(state, key) {
|
|
|
870
942
|
const value = choice?.value ?? "";
|
|
871
943
|
return {
|
|
872
944
|
state,
|
|
873
|
-
view: `${
|
|
945
|
+
view: `${renderSubmitHeader(state.styler, state.theme, state.message)} ${state.styler.render(state.theme.roles.hint, choice?.label ?? "")}`,
|
|
874
946
|
status: "submit",
|
|
875
947
|
value
|
|
876
948
|
};
|
|
877
949
|
}
|
|
878
950
|
return {
|
|
879
951
|
state,
|
|
880
|
-
view:
|
|
952
|
+
view: renderSelectView(state),
|
|
881
953
|
status: "active"
|
|
882
954
|
};
|
|
883
955
|
}
|
|
884
956
|
/**
|
|
885
|
-
*
|
|
957
|
+
* Builds the initial checkbox-field reducer state — the offered choices, with every value in the
|
|
958
|
+
* field's `default` list pre-checked.
|
|
886
959
|
*
|
|
887
960
|
* @param field - The checkbox field to render
|
|
888
961
|
* @param styler - The styler used to render the view
|
|
@@ -904,8 +977,14 @@ function createCheckboxState(field, styler = createStyler(), theme) {
|
|
|
904
977
|
checked
|
|
905
978
|
};
|
|
906
979
|
}
|
|
907
|
-
/**
|
|
908
|
-
|
|
980
|
+
/**
|
|
981
|
+
* Renders a checkbox-field reducer state as a multi-line styled view — one box per choice, and the
|
|
982
|
+
* selected count beneath them.
|
|
983
|
+
*
|
|
984
|
+
* @param state - The checkbox field's current reducer state
|
|
985
|
+
* @returns The rendered view — the header, one box per choice, and the selected count
|
|
986
|
+
*/
|
|
987
|
+
function renderCheckboxView(state) {
|
|
909
988
|
const lines = state.choices.map((choice, index) => {
|
|
910
989
|
const active = index === state.focused;
|
|
911
990
|
const ticked = state.checked.includes(index);
|
|
@@ -913,21 +992,25 @@ function checkboxView(state) {
|
|
|
913
992
|
});
|
|
914
993
|
const summary = state.styler.render(state.theme.roles.hint, `${state.checked.length} selected`);
|
|
915
994
|
return [
|
|
916
|
-
|
|
995
|
+
renderPromptHeader(state.styler, state.theme, state.message),
|
|
917
996
|
...lines,
|
|
918
997
|
summary
|
|
919
998
|
].join("\n");
|
|
920
999
|
}
|
|
921
1000
|
/**
|
|
922
|
-
*
|
|
1001
|
+
* Advances a checkbox prompt by one {@link KeyEvent} — the pure
|
|
923
1002
|
* `(state, key) → PromptStep<readonly string[]>` reducer. `up` / `down` (and `k` / `j`) move the
|
|
924
1003
|
* focus (wrapping); `space` toggles the focused index in the checked set; return submits the
|
|
925
1004
|
* checked values in choice order; ctrl-c cancels. The form applies selection-count rules.
|
|
1005
|
+
*
|
|
1006
|
+
* @param state - The checkbox field's current reducer state
|
|
1007
|
+
* @param key - The decoded keypress to apply
|
|
1008
|
+
* @returns The next step — the state, the rendered view, the status, and the ticked values on submit
|
|
926
1009
|
*/
|
|
927
|
-
function
|
|
1010
|
+
function reduceCheckbox(state, key) {
|
|
928
1011
|
if (key.ctrl && key.name === "c") return {
|
|
929
1012
|
state,
|
|
930
|
-
view:
|
|
1013
|
+
view: renderCheckboxView(state),
|
|
931
1014
|
status: "cancel"
|
|
932
1015
|
};
|
|
933
1016
|
const count = state.choices.length;
|
|
@@ -938,7 +1021,7 @@ function checkboxReduce(state, key) {
|
|
|
938
1021
|
};
|
|
939
1022
|
return {
|
|
940
1023
|
state: next,
|
|
941
|
-
view:
|
|
1024
|
+
view: renderCheckboxView(next),
|
|
942
1025
|
status: "active"
|
|
943
1026
|
};
|
|
944
1027
|
}
|
|
@@ -949,7 +1032,7 @@ function checkboxReduce(state, key) {
|
|
|
949
1032
|
};
|
|
950
1033
|
return {
|
|
951
1034
|
state: next,
|
|
952
|
-
view:
|
|
1035
|
+
view: renderCheckboxView(next),
|
|
953
1036
|
status: "active"
|
|
954
1037
|
};
|
|
955
1038
|
}
|
|
@@ -961,7 +1044,7 @@ function checkboxReduce(state, key) {
|
|
|
961
1044
|
};
|
|
962
1045
|
return {
|
|
963
1046
|
state: next,
|
|
964
|
-
view:
|
|
1047
|
+
view: renderCheckboxView(next),
|
|
965
1048
|
status: "active"
|
|
966
1049
|
};
|
|
967
1050
|
}
|
|
@@ -971,23 +1054,31 @@ function checkboxReduce(state, key) {
|
|
|
971
1054
|
const summary = ordered.map((index) => state.choices[index]?.label).filter((name) => name !== void 0).join(", ");
|
|
972
1055
|
return {
|
|
973
1056
|
state,
|
|
974
|
-
view: `${
|
|
1057
|
+
view: `${renderSubmitHeader(state.styler, state.theme, state.message)} ${state.styler.render(state.theme.roles.hint, summary)}`,
|
|
975
1058
|
status: "submit",
|
|
976
1059
|
value: values
|
|
977
1060
|
};
|
|
978
1061
|
}
|
|
979
1062
|
return {
|
|
980
1063
|
state,
|
|
981
|
-
view:
|
|
1064
|
+
view: renderCheckboxView(state),
|
|
982
1065
|
status: "active"
|
|
983
1066
|
};
|
|
984
1067
|
}
|
|
985
|
-
/**
|
|
1068
|
+
/**
|
|
1069
|
+
* Toggles `index` in a readonly index list — copy-on-write, returning the new sorted-by-insertion
|
|
1070
|
+
* list; the primitive {@link reduceCheckbox} calls.
|
|
1071
|
+
*
|
|
1072
|
+
* @param indices - The ticked indices, in tick order
|
|
1073
|
+
* @param index - The index to add when absent, or drop when present
|
|
1074
|
+
* @returns A new list carrying the toggled membership; the input is never mutated
|
|
1075
|
+
*/
|
|
986
1076
|
function toggleIndex(indices, index) {
|
|
987
1077
|
return indices.includes(index) ? indices.filter((i) => i !== index) : [...indices, index];
|
|
988
1078
|
}
|
|
989
1079
|
/**
|
|
990
|
-
*
|
|
1080
|
+
* Builds the initial editor-field reducer state — the committed lines empty, and the declared
|
|
1081
|
+
* default held for a finish with nothing typed.
|
|
991
1082
|
*
|
|
992
1083
|
* @param field - The editor field to render
|
|
993
1084
|
* @param styler - The styler used to render the view
|
|
@@ -1005,23 +1096,31 @@ function createEditorState(field, styler = createStyler(), theme) {
|
|
|
1005
1096
|
};
|
|
1006
1097
|
}
|
|
1007
1098
|
/**
|
|
1008
|
-
*
|
|
1099
|
+
* Renders an editor-field reducer state as a multi-line styled view — the finish hint, the
|
|
1100
|
+
* committed lines, and the line in progress.
|
|
1101
|
+
*
|
|
1102
|
+
* @param state - The editor field's current reducer state
|
|
1103
|
+
* @returns The rendered view — the hinted header, the committed lines, and the line in progress
|
|
1009
1104
|
*/
|
|
1010
|
-
function
|
|
1011
|
-
const head =
|
|
1105
|
+
function renderEditorView(state) {
|
|
1106
|
+
const head = renderHintedHeader(state.styler, state.theme, state.message, "(Ctrl+D to finish)");
|
|
1012
1107
|
const pointer = state.styler.render(state.theme.roles.pointer, state.theme.icons.pointer);
|
|
1013
1108
|
return [head, ...[...state.lines.map((line) => state.styler.render(state.theme.roles.content, line)), `${pointer} ${state.styler.render(state.theme.roles.content, state.current)}`]].join("\n");
|
|
1014
1109
|
}
|
|
1015
1110
|
/**
|
|
1016
|
-
*
|
|
1111
|
+
* Advances an editor prompt by one {@link KeyEvent} — the pure `(state, key) → PromptStep<string>`
|
|
1017
1112
|
* reducer. Printable characters extend the current line; backspace shrinks it; return commits the
|
|
1018
|
-
* current line and starts a fresh one; ctrl-d
|
|
1019
|
-
* default when empty
|
|
1113
|
+
* current line and starts a fresh one; ctrl-d finishes, joining every line and falling back to the
|
|
1114
|
+
* default when empty; ctrl-c cancels. The form validates the candidate after the driver fills it.
|
|
1115
|
+
*
|
|
1116
|
+
* @param state - The editor field's current reducer state
|
|
1117
|
+
* @param key - The decoded keypress to apply
|
|
1118
|
+
* @returns The next step — the state, the rendered view, the status, and the joined text on submit
|
|
1020
1119
|
*/
|
|
1021
|
-
function
|
|
1120
|
+
function reduceEditor(state, key) {
|
|
1022
1121
|
if (key.ctrl && key.name === "c") return {
|
|
1023
1122
|
state,
|
|
1024
|
-
view:
|
|
1123
|
+
view: renderEditorView(state),
|
|
1025
1124
|
status: "cancel"
|
|
1026
1125
|
};
|
|
1027
1126
|
if (key.ctrl && key.name === "d") {
|
|
@@ -1030,7 +1129,7 @@ function editorReduce(state, key) {
|
|
|
1030
1129
|
const answer = joined.length > 0 ? joined : state.default;
|
|
1031
1130
|
return {
|
|
1032
1131
|
state,
|
|
1033
|
-
view: `${
|
|
1132
|
+
view: `${renderSubmitHeader(state.styler, state.theme, state.message)} ${state.styler.render(state.theme.roles.hint, `${String(lines.length)} line${lines.length === 1 ? "" : "s"}`)}`,
|
|
1034
1133
|
status: "submit",
|
|
1035
1134
|
value: answer
|
|
1036
1135
|
};
|
|
@@ -1043,14 +1142,14 @@ function editorReduce(state, key) {
|
|
|
1043
1142
|
};
|
|
1044
1143
|
return {
|
|
1045
1144
|
state: next,
|
|
1046
|
-
view:
|
|
1145
|
+
view: renderEditorView(next),
|
|
1047
1146
|
status: "active"
|
|
1048
1147
|
};
|
|
1049
1148
|
}
|
|
1050
1149
|
const current = editLine(state.current, key);
|
|
1051
1150
|
if (current === void 0) return {
|
|
1052
1151
|
state,
|
|
1053
|
-
view:
|
|
1152
|
+
view: renderEditorView(state),
|
|
1054
1153
|
status: "active"
|
|
1055
1154
|
};
|
|
1056
1155
|
const next = {
|
|
@@ -1059,48 +1158,69 @@ function editorReduce(state, key) {
|
|
|
1059
1158
|
};
|
|
1060
1159
|
return {
|
|
1061
1160
|
state: next,
|
|
1062
|
-
view:
|
|
1161
|
+
view: renderEditorView(next),
|
|
1063
1162
|
status: "active"
|
|
1064
1163
|
};
|
|
1065
1164
|
}
|
|
1066
1165
|
/**
|
|
1067
|
-
*
|
|
1068
|
-
* password
|
|
1069
|
-
* `space` appends a space; ctrl-u clears the line
|
|
1070
|
-
*
|
|
1166
|
+
* Applies a single line-editing {@link KeyEvent} to a text buffer — the editing shared by input,
|
|
1167
|
+
* password, and editor. A printable key appends its character; `backspace` drops the last
|
|
1168
|
+
* character; `space` appends a space; ctrl-u clears the line; a key that edits nothing returns
|
|
1169
|
+
* `undefined`.
|
|
1170
|
+
*
|
|
1171
|
+
* @param value - The buffer the field holds so far
|
|
1172
|
+
* @param key - The decoded keypress to apply
|
|
1173
|
+
* @returns The new buffer, or `undefined` when the key does not edit the line, so the caller can
|
|
1174
|
+
* leave the state untouched
|
|
1071
1175
|
*/
|
|
1072
1176
|
function editLine(value, key) {
|
|
1073
1177
|
if (key.ctrl && key.name === "u") return "";
|
|
1074
1178
|
if (key.name === "backspace") return value.slice(0, -1);
|
|
1075
1179
|
if (key.name === "space") return `${value} `;
|
|
1076
|
-
if (!key.ctrl && !key.meta && [...key.name].length === 1 && isPrintable(key.name)) return `${value}${key.sequence}`;
|
|
1180
|
+
if (!key.ctrl && !key.meta && key.name !== void 0 && [...key.name].length === 1 && isPrintable(key.name)) return `${value}${key.sequence}`;
|
|
1077
1181
|
}
|
|
1078
1182
|
/**
|
|
1079
|
-
*
|
|
1080
|
-
* wrapper that arms `callback` after `ms` and returns a {@link
|
|
1183
|
+
* Implements the default {@link import('./types.js').TimerHandler} — a thin host `setTimeout` / `clearTimeout`
|
|
1184
|
+
* wrapper that arms `callback` after `ms` and returns a {@link TimerCancelFunction}. The deadline seam
|
|
1081
1185
|
* behind both the {@link import('./Prompt.js').Prompt} broker (its expiry) and the
|
|
1082
1186
|
* {@link import('./PromptClient.js').PromptClient} (its reconnect backoff); a test injects a
|
|
1083
1187
|
* deterministic timer instead, so neither entity touches real time.
|
|
1188
|
+
*
|
|
1189
|
+
* @param callback - The deadline callback to arm
|
|
1190
|
+
* @param ms - How long to wait before firing it, in milliseconds
|
|
1191
|
+
* @returns The {@link TimerCancelFunction} that clears the armed deadline
|
|
1084
1192
|
*/
|
|
1085
1193
|
function defaultTimer(callback, ms) {
|
|
1086
1194
|
const handle = setTimeout(callback, ms);
|
|
1087
1195
|
return () => clearTimeout(handle);
|
|
1088
1196
|
}
|
|
1089
|
-
/**
|
|
1197
|
+
/**
|
|
1198
|
+
* Implements the default {@link import('./types.js').FetchHandler} — the global `fetch`, adapted to
|
|
1199
|
+
* the minimal injected shape the {@link import('./PromptClient.js').PromptClient} uses.
|
|
1200
|
+
*
|
|
1201
|
+
* @param input - The request URL
|
|
1202
|
+
* @param init - The request init the client sets — method, headers, body, and abort signal
|
|
1203
|
+
* @returns The host `fetch` promise for that request
|
|
1204
|
+
*/
|
|
1090
1205
|
function globalFetch(input, init) {
|
|
1091
1206
|
return fetch(input, init);
|
|
1092
1207
|
}
|
|
1093
1208
|
/**
|
|
1094
|
-
*
|
|
1209
|
+
* Checks whether a caught value is an `AbortError` — the {@link import('./PromptClient.js').PromptClient}
|
|
1095
1210
|
* distinguishes a deliberate `disconnect` / teardown (an aborted `fetch`) from a real fault, so it
|
|
1096
1211
|
* exits its connect loop quietly instead of emitting `error` / reconnecting.
|
|
1212
|
+
*
|
|
1213
|
+
* @param error - The caught value to test
|
|
1214
|
+
* @returns True if the value is a host `Error` or `DOMException` named `AbortError`; false otherwise
|
|
1097
1215
|
*/
|
|
1098
1216
|
function isAbortError(error) {
|
|
1099
1217
|
return (error instanceof DOMException || error instanceof Error) && error.name === "AbortError";
|
|
1100
1218
|
}
|
|
1101
1219
|
/**
|
|
1102
|
-
*
|
|
1103
|
-
* loopback address. Pure string parsing (no `URL` global), so it stays total on malformed input
|
|
1220
|
+
* Checks whether `url` is an insecure remote endpoint — a plain `http://` URL whose host is not a
|
|
1221
|
+
* loopback address. Pure string parsing (no `URL` global), so it stays total on malformed input;
|
|
1222
|
+
* the {@link import('./PromptClient.js').PromptClient} warns once when a `token` would cross such
|
|
1223
|
+
* an endpoint in cleartext.
|
|
1104
1224
|
*
|
|
1105
1225
|
* @remarks
|
|
1106
1226
|
* A loopback host (`localhost`, `127.0.0.1`, `[::1]`) over `http://` is exempt (local
|
|
@@ -1108,7 +1228,7 @@ function isAbortError(error) {
|
|
|
1108
1228
|
* An `https://` URL (or any non-`http://` scheme) is never flagged.
|
|
1109
1229
|
*
|
|
1110
1230
|
* @param url - The candidate endpoint URL
|
|
1111
|
-
* @returns
|
|
1231
|
+
* @returns True if `url` is a non-loopback `http://` endpoint; false otherwise
|
|
1112
1232
|
*
|
|
1113
1233
|
* @example
|
|
1114
1234
|
* ```ts
|
|
@@ -1126,7 +1246,13 @@ function isInsecureRemote(url) {
|
|
|
1126
1246
|
const hostname = host.startsWith("[") ? host.slice(0, host.indexOf("]") + 1) : host.split(":")[0] ?? "";
|
|
1127
1247
|
return hostname !== "localhost" && hostname !== "127.0.0.1" && hostname !== "[::1]";
|
|
1128
1248
|
}
|
|
1129
|
-
/**
|
|
1249
|
+
/**
|
|
1250
|
+
* Serializes a parked {@link PendingForm} into a `pending` {@link WireEvent}, whose frame `id` is
|
|
1251
|
+
* the form's own id.
|
|
1252
|
+
*
|
|
1253
|
+
* @param form - The parked form's wire-safe record
|
|
1254
|
+
* @returns The `pending` frame — the JSON-stringified record as `data`, and the form's own `id`
|
|
1255
|
+
*/
|
|
1130
1256
|
function serializePending(form) {
|
|
1131
1257
|
return {
|
|
1132
1258
|
event: "pending",
|
|
@@ -1134,25 +1260,37 @@ function serializePending(form) {
|
|
|
1134
1260
|
id: form.id
|
|
1135
1261
|
};
|
|
1136
1262
|
}
|
|
1137
|
-
/**
|
|
1263
|
+
/**
|
|
1264
|
+
* Serializes a parked form's expiry or release into an `expire` {@link WireEvent}, whose `data` is
|
|
1265
|
+
* the JSON `{ id }` payload.
|
|
1266
|
+
*
|
|
1267
|
+
* @param id - The id of the parked form that expired or was released
|
|
1268
|
+
* @returns The `expire` frame, carrying the JSON-stringified `{ id }` payload as `data`
|
|
1269
|
+
*/
|
|
1138
1270
|
function serializeExpire(id) {
|
|
1139
1271
|
return {
|
|
1140
1272
|
event: "expire",
|
|
1141
1273
|
data: JSON.stringify({ id })
|
|
1142
1274
|
};
|
|
1143
1275
|
}
|
|
1144
|
-
/**
|
|
1145
|
-
|
|
1276
|
+
/**
|
|
1277
|
+
* Serializes the `destroy` {@link WireEvent} a broker or manager sends when it is going away, which
|
|
1278
|
+
* carries no payload.
|
|
1279
|
+
*
|
|
1280
|
+
* @returns The `destroy` frame, whose `data` is empty because the signal carries no payload
|
|
1281
|
+
*/
|
|
1282
|
+
function serializeDestroy() {
|
|
1146
1283
|
return {
|
|
1147
|
-
event: "
|
|
1284
|
+
event: "destroy",
|
|
1148
1285
|
data: ""
|
|
1149
1286
|
};
|
|
1150
1287
|
}
|
|
1151
1288
|
//#endregion
|
|
1152
1289
|
//#region src/core/Prompt.ts
|
|
1153
1290
|
/**
|
|
1154
|
-
*
|
|
1155
|
-
* remote answers to the authoritative form
|
|
1291
|
+
* Implements the headless form broker. It parks live forms, exposes their serialized schemas,
|
|
1292
|
+
* applies remote answers to the authoritative form, and abandons a parked form on timeout, release,
|
|
1293
|
+
* or teardown.
|
|
1156
1294
|
*
|
|
1157
1295
|
* @remarks
|
|
1158
1296
|
* A parked record carries one call to `serializeForm`. A failed fill or submit leaves the record
|
|
@@ -1342,8 +1480,9 @@ var Prompt = class {
|
|
|
1342
1480
|
//#endregion
|
|
1343
1481
|
//#region src/core/PromptClient.ts
|
|
1344
1482
|
/**
|
|
1345
|
-
*
|
|
1346
|
-
* local terminal,
|
|
1483
|
+
* Implements the SSE form bridge. It ingests serialized forms from a remote broker without waiting
|
|
1484
|
+
* on a render, drives one form at a time through a local terminal, posts each answer back, and asks
|
|
1485
|
+
* again when the authoritative form refuses one.
|
|
1347
1486
|
*
|
|
1348
1487
|
* @remarks
|
|
1349
1488
|
* - **Connect + reconnect.** {@link connect} opens the SSE stream and reconnects after a transport
|
|
@@ -1351,12 +1490,13 @@ var Prompt = class {
|
|
|
1351
1490
|
* {@link disconnect} deliberately stopped it.
|
|
1352
1491
|
* - **Ingest + render.** Each `pending` envelope passes through `isPendingForm`, Form's `parseForm`,
|
|
1353
1492
|
* and terminal's `sanitizeSchema`, then enters a serial render queue. The SSE reader never awaits
|
|
1354
|
-
* that queue, so `expire` and `
|
|
1493
|
+
* that queue, so `expire` and `destroy` remain live while a person is answering.
|
|
1355
1494
|
* - **Safe local form.** The rendering copy omits every wire `pattern`, because Form compiles a
|
|
1356
1495
|
* pattern during local evaluation. The broker's parked form retains it and remains authoritative.
|
|
1357
1496
|
* - **Refusal retry.** A structured `rejected` response seeds a new rendering form with the values
|
|
1358
|
-
*
|
|
1359
|
-
* counter truncates the loop; acceptance, expiry, and
|
|
1497
|
+
* the refused attempt submitted, applies every {@link FieldError} through `invalidate`, and asks
|
|
1498
|
+
* again. No retry counter truncates the loop; acceptance, expiry, and the broker's own teardown
|
|
1499
|
+
* are its bounds.
|
|
1360
1500
|
* - **Replay safety.** A replayed id is skipped while it is queued, rendering, or posting. Once an
|
|
1361
1501
|
* attempt ends, a later delivery of that id may be rendered again.
|
|
1362
1502
|
*
|
|
@@ -1494,7 +1634,7 @@ var PromptClient = class {
|
|
|
1494
1634
|
if (isRecord(parsed) && isString(parsed.id)) this.#expire(parsed.id);
|
|
1495
1635
|
return;
|
|
1496
1636
|
}
|
|
1497
|
-
if (event.event === SSE_EVENTS.
|
|
1637
|
+
if (event.event === SSE_EVENTS.destroy) {
|
|
1498
1638
|
this.disconnect();
|
|
1499
1639
|
this.#interrupt();
|
|
1500
1640
|
}
|
|
@@ -1647,25 +1787,24 @@ var PromptClient = class {
|
|
|
1647
1787
|
//#endregion
|
|
1648
1788
|
//#region src/core/TerminalManager.ts
|
|
1649
1789
|
/**
|
|
1650
|
-
*
|
|
1651
|
-
*
|
|
1652
|
-
*
|
|
1653
|
-
* asks.
|
|
1790
|
+
* Registers named {@link PromptInterface} brokers, one per endpoint, so several parties can `ask`
|
|
1791
|
+
* forms of each other by name with a `from` → `to` attribution edge on every parked form, and
|
|
1792
|
+
* refuses `DEADLOCK` on a transitive cycle across every in-flight ask.
|
|
1654
1793
|
*
|
|
1655
1794
|
* @remarks
|
|
1656
1795
|
* - **Registry.** `add(name, options?)` mints (or, if `name` is already mounted, returns the
|
|
1657
|
-
*
|
|
1796
|
+
* existing broker unchanged — idempotent, never clobbers a live/parked endpoint). Every mounted
|
|
1658
1797
|
* broker's `pending` / `answer` / `expire` events are re-emitted on the manager, attributed by
|
|
1659
1798
|
* `name`.
|
|
1660
|
-
* - **`ask`.** The target must already be mounted
|
|
1799
|
+
* - **`ask`.** The target must already be mounted through {@link add} — `ask` never auto-adds it;
|
|
1661
1800
|
* rejects `TARGET` for an unknown `to` (listing the known names). Rejects `DEADLOCK` when parking
|
|
1662
|
-
* `from → to` would close a cycle over the
|
|
1801
|
+
* `from → to` would close a cycle over the current in-flight edge set (walked transitively);
|
|
1663
1802
|
* otherwise parks the caller's live form through the target's broker and returns that form's own
|
|
1664
1803
|
* `answer` promise. Edge cleanup never alters the value or rejection the caller observes.
|
|
1665
1804
|
* - **Durable open / save.** `open(name)` restores an EMPTY broker from the `store` (parked
|
|
1666
1805
|
* Promises are process-bound and never resurrected); `save(name)` persists the endpoint's
|
|
1667
1806
|
* configured `timeout`.
|
|
1668
|
-
* - **Removal.** `remove` drops one endpoint, a batch (
|
|
1807
|
+
* - **Removal.** `remove` drops one endpoint, a batch (the array overload declared first), or every
|
|
1669
1808
|
* endpoint when called without an argument. It destroys each broker, which expires every form
|
|
1670
1809
|
* still parked on it. `destroy` is idempotent.
|
|
1671
1810
|
*
|
|
@@ -1710,7 +1849,7 @@ var TerminalManager = class {
|
|
|
1710
1849
|
return this.#terminals.get(name);
|
|
1711
1850
|
}
|
|
1712
1851
|
terminals() {
|
|
1713
|
-
return [...this.#terminals.
|
|
1852
|
+
return [...this.#terminals.values()];
|
|
1714
1853
|
}
|
|
1715
1854
|
add(name, options) {
|
|
1716
1855
|
if (this.#destroyed) throw new TerminalError("DESTROYED", "manager destroyed");
|
|
@@ -1742,7 +1881,7 @@ var TerminalManager = class {
|
|
|
1742
1881
|
ask(from, to, form) {
|
|
1743
1882
|
const broker = this.#terminals.get(to);
|
|
1744
1883
|
if (broker === void 0) {
|
|
1745
|
-
const known = this.
|
|
1884
|
+
const known = [...this.#terminals.keys()];
|
|
1746
1885
|
return Promise.reject(new TerminalError("TARGET", `unknown terminal '${to}' (known: ${known.length > 0 ? known.join(", ") : "none"})`, {
|
|
1747
1886
|
to,
|
|
1748
1887
|
known
|
|
@@ -1780,7 +1919,7 @@ var TerminalManager = class {
|
|
|
1780
1919
|
const broker = this.#terminals.get(to);
|
|
1781
1920
|
if (broker === void 0) return {
|
|
1782
1921
|
success: false,
|
|
1783
|
-
error: { reason: "
|
|
1922
|
+
error: { reason: "target" }
|
|
1784
1923
|
};
|
|
1785
1924
|
const result = broker.answer(id, values);
|
|
1786
1925
|
if (result.success) this.#edges.delete(id);
|
|
@@ -1813,8 +1952,8 @@ var TerminalManager = class {
|
|
|
1813
1952
|
return;
|
|
1814
1953
|
}
|
|
1815
1954
|
if (isArray(names)) {
|
|
1816
|
-
let removed =
|
|
1817
|
-
for (const name of names) if (this.#removeOne(name)) removed =
|
|
1955
|
+
let removed = true;
|
|
1956
|
+
for (const name of names) if (!this.#removeOne(name)) removed = false;
|
|
1818
1957
|
return removed;
|
|
1819
1958
|
}
|
|
1820
1959
|
return this.#removeOne(names);
|
|
@@ -1889,16 +2028,17 @@ var TerminalManager = class {
|
|
|
1889
2028
|
}
|
|
1890
2029
|
};
|
|
1891
2030
|
//#endregion
|
|
1892
|
-
//#region src/core/MemoryTerminalStore.ts
|
|
2031
|
+
//#region src/core/stores/MemoryTerminalStore.ts
|
|
1893
2032
|
/**
|
|
1894
|
-
*
|
|
1895
|
-
* {@link TerminalSnapshot}
|
|
1896
|
-
* {@link import('
|
|
1897
|
-
* {@link import('./DatabaseTerminalStore.js').DatabaseTerminalStore}.
|
|
2033
|
+
* Implements the in-memory {@link TerminalStoreInterface} — a process-lifetime `Map` of
|
|
2034
|
+
* {@link TerminalSnapshot} records keyed by endpoint id, the default store
|
|
2035
|
+
* {@link import('../factories.js').createMemoryTerminalStore} builds and the exact twin of
|
|
2036
|
+
* {@link import('./DatabaseTerminalStore.js').DatabaseTerminalStore}. It carries no idle expiry and
|
|
2037
|
+
* no eviction.
|
|
1898
2038
|
*
|
|
1899
2039
|
* @remarks
|
|
1900
|
-
* A plain `Map<string, TerminalSnapshot>`
|
|
1901
|
-
*
|
|
2040
|
+
* A plain `Map<string, TerminalSnapshot>` — the snapshot is already pure, self-contained CONFIG-only
|
|
2041
|
+
* JSON, so the memory tier needs no encoding. There is NO idle-TTL and NO
|
|
1902
2042
|
* eviction: a persisted config lives until an explicit `delete`. A durable backend (JSON / SQLite /
|
|
1903
2043
|
* IndexedDB) swaps in through the SAME interface without touching the manager — its
|
|
1904
2044
|
* driver-pluggable twin is {@link import('./DatabaseTerminalStore.js').DatabaseTerminalStore} (the
|
|
@@ -1908,13 +2048,13 @@ var TerminalManager = class {
|
|
|
1908
2048
|
* - **`set` inserts / replaces under the snapshot's OWN `id`** (no separate id param).
|
|
1909
2049
|
* - **`delete` drops a snapshot by id**; an absent id is a no-op (no throw).
|
|
1910
2050
|
*
|
|
1911
|
-
* The public surface is EXACTLY `get` / `set` / `delete` — no extra members
|
|
1912
|
-
*
|
|
2051
|
+
* The public surface is EXACTLY `get` / `set` / `delete` — no extra members, so the class and
|
|
2052
|
+
* {@link TerminalStoreInterface} carry the same methods. Hydration is a caller concern: `open` always
|
|
1913
2053
|
* restores an EMPTY broker — parked Promises are process-bound and never resurrected.
|
|
1914
2054
|
*
|
|
1915
2055
|
* @example
|
|
1916
2056
|
* ```ts
|
|
1917
|
-
* import { createMemoryTerminalStore } from '@
|
|
2057
|
+
* import { createMemoryTerminalStore } from '@orkestrel/terminal'
|
|
1918
2058
|
*
|
|
1919
2059
|
* const store = createMemoryTerminalStore()
|
|
1920
2060
|
* await store.set({ id: 'shell', timeout: 5000 }) // persist a config
|
|
@@ -1924,32 +2064,51 @@ var TerminalManager = class {
|
|
|
1924
2064
|
*/
|
|
1925
2065
|
var MemoryTerminalStore = class {
|
|
1926
2066
|
#snapshots = /* @__PURE__ */ new Map();
|
|
2067
|
+
/**
|
|
2068
|
+
* Resolves the persisted snapshot for `id`.
|
|
2069
|
+
*
|
|
2070
|
+
* @param id - The endpoint name the snapshot is keyed by
|
|
2071
|
+
* @returns The stored snapshot, or `undefined` when none is held
|
|
2072
|
+
*/
|
|
1927
2073
|
get(id) {
|
|
1928
2074
|
return Promise.resolve(this.#snapshots.get(id));
|
|
1929
2075
|
}
|
|
2076
|
+
/**
|
|
2077
|
+
* Inserts or replaces under the snapshot's OWN `id` (no separate id param).
|
|
2078
|
+
*
|
|
2079
|
+
* @param snapshot - The config snapshot to persist, carrying its own `id`
|
|
2080
|
+
* @returns A promise that settles once the snapshot is held
|
|
2081
|
+
*/
|
|
1930
2082
|
set(snapshot) {
|
|
1931
2083
|
this.#snapshots.set(snapshot.id, snapshot);
|
|
1932
2084
|
return Promise.resolve();
|
|
1933
2085
|
}
|
|
2086
|
+
/**
|
|
2087
|
+
* Drops a snapshot by id; an absent id is a no-op (no throw).
|
|
2088
|
+
*
|
|
2089
|
+
* @param id - The endpoint name to drop
|
|
2090
|
+
* @returns A promise that settles once the snapshot is gone
|
|
2091
|
+
*/
|
|
1934
2092
|
delete(id) {
|
|
1935
2093
|
this.#snapshots.delete(id);
|
|
1936
2094
|
return Promise.resolve();
|
|
1937
2095
|
}
|
|
1938
2096
|
};
|
|
1939
2097
|
//#endregion
|
|
1940
|
-
//#region src/core/DatabaseTerminalStore.ts
|
|
2098
|
+
//#region src/core/stores/DatabaseTerminalStore.ts
|
|
1941
2099
|
/**
|
|
1942
|
-
*
|
|
1943
|
-
* durable
|
|
1944
|
-
* `delete`) over a {@link TableInterface}, the driver-pluggable twin of the plain-`Map`
|
|
1945
|
-
* {@link import('./MemoryTerminalStore.js').MemoryTerminalStore}.
|
|
2100
|
+
* Implements a {@link TerminalStoreInterface} backed by one table of the `databases` layer — an
|
|
2101
|
+
* endpoint's durable config state is a row, so persistence reduces to keyed point-access (`get` /
|
|
2102
|
+
* `set` / `delete`) over a {@link TableInterface}, the driver-pluggable twin of the plain-`Map`
|
|
2103
|
+
* {@link import('./MemoryTerminalStore.js').MemoryTerminalStore}. A stored `snapshot` is narrowed
|
|
2104
|
+
* with {@link import('../validators.js').isTerminalSnapshot} on read.
|
|
1946
2105
|
*
|
|
1947
2106
|
* @remarks
|
|
1948
2107
|
* The store is driver-agnostic: it holds a single {@link TableInterface} whose backend (memory,
|
|
1949
2108
|
* JSON, SQLite, IndexedDB) is chosen by whoever builds it (the factories), so a JSON / SQLite /
|
|
1950
2109
|
* IndexedDB backend swaps in WITHOUT touching the manager — the same seam as
|
|
1951
2110
|
* {@link import('./MemoryTerminalStore.js').MemoryTerminalStore}. The driver defaults to memory
|
|
1952
|
-
* ({@link import('
|
|
2111
|
+
* ({@link import('../factories.js').createDatabaseTerminalStore} passes `createMemoryDriver()`), so
|
|
1953
2112
|
* it ALSO works in memory out of the box; you opt into the durable plumbing by passing a JSON /
|
|
1954
2113
|
* SQLite / IndexedDB driver.
|
|
1955
2114
|
*
|
|
@@ -1961,18 +2120,19 @@ var MemoryTerminalStore = class {
|
|
|
1961
2120
|
* - **`set(snapshot)` upserts under the snapshot's OWN `id`** (no separate id param) — it writes
|
|
1962
2121
|
* the row `{ id: snapshot.id, snapshot }`.
|
|
1963
2122
|
* - **`get(id)` resolves the stored snapshot for an id**, narrowing the opaque JSON column back to
|
|
1964
|
-
* a {@link TerminalSnapshot} ({@link import('
|
|
1965
|
-
*
|
|
2123
|
+
* a {@link TerminalSnapshot} ({@link import('../validators.js').isTerminalSnapshot} — the total
|
|
2124
|
+
* guard that narrows an untrusted storage read), or `undefined` if none is stored.
|
|
1966
2125
|
* - **`delete(id)` drops a snapshot by id**; an absent id is a no-op (no throw).
|
|
1967
2126
|
*
|
|
1968
2127
|
* There is NO idle-TTL / eviction — a persisted config lives until an explicit `delete`. The public
|
|
1969
|
-
* surface is EXACTLY `get` / `set` / `delete` — no extra members
|
|
1970
|
-
* {@link TerminalStoreInterface}
|
|
2128
|
+
* surface is EXACTLY `get` / `set` / `delete` — no extra members, so the class and
|
|
2129
|
+
* {@link TerminalStoreInterface} carry the same methods. Hydration stays a caller concern: `open` always restores an EMPTY
|
|
1971
2130
|
* broker — parked Promises are process-bound and never resurrected.
|
|
1972
2131
|
*
|
|
1973
2132
|
* @example
|
|
1974
2133
|
* ```ts
|
|
1975
|
-
* import { createDatabaseTerminalStore
|
|
2134
|
+
* import { createDatabaseTerminalStore } from '@orkestrel/terminal'
|
|
2135
|
+
* import { createMemoryDriver } from '@orkestrel/database'
|
|
1976
2136
|
*
|
|
1977
2137
|
* const store = createDatabaseTerminalStore(createMemoryDriver()) // a durable driver swaps in here
|
|
1978
2138
|
* await store.set({ id: 'shell', timeout: 5000 }) // persist the config (one JSON column)
|
|
@@ -1983,7 +2143,7 @@ var MemoryTerminalStore = class {
|
|
|
1983
2143
|
var DatabaseTerminalStore = class {
|
|
1984
2144
|
#table;
|
|
1985
2145
|
/**
|
|
1986
|
-
*
|
|
2146
|
+
* Wraps a table as a terminal store.
|
|
1987
2147
|
*
|
|
1988
2148
|
* @param table - The {@link TableInterface} holding the snapshots — its row is the
|
|
1989
2149
|
* {@link TerminalSnapshotRow} `{ id; snapshot }` shape (the snapshot one opaque JSON column)
|
|
@@ -1991,20 +2151,37 @@ var DatabaseTerminalStore = class {
|
|
|
1991
2151
|
constructor(table) {
|
|
1992
2152
|
this.#table = table;
|
|
1993
2153
|
}
|
|
1994
|
-
/**
|
|
2154
|
+
/**
|
|
2155
|
+
* Resolves the persisted snapshot for `id`, narrowing the opaque JSON column back to a
|
|
2156
|
+
* `TerminalSnapshot`.
|
|
2157
|
+
*
|
|
2158
|
+
* @param id - The endpoint name the row is keyed by
|
|
2159
|
+
* @returns The stored snapshot, or `undefined` when none is held or the column is off-shape
|
|
2160
|
+
*/
|
|
1995
2161
|
async get(id) {
|
|
1996
2162
|
const row = await this.#table.get(id);
|
|
1997
2163
|
if (row === void 0) return void 0;
|
|
1998
2164
|
return isTerminalSnapshot(row.snapshot) ? row.snapshot : void 0;
|
|
1999
2165
|
}
|
|
2000
|
-
/**
|
|
2166
|
+
/**
|
|
2167
|
+
* Inserts or replaces under the snapshot's OWN `id` (no separate id param) — the row is
|
|
2168
|
+
* `{ id, snapshot }`.
|
|
2169
|
+
*
|
|
2170
|
+
* @param snapshot - The config snapshot to persist, carrying its own `id`
|
|
2171
|
+
* @returns A promise that settles once the row is written
|
|
2172
|
+
*/
|
|
2001
2173
|
async set(snapshot) {
|
|
2002
2174
|
await this.#table.set({
|
|
2003
2175
|
id: snapshot.id,
|
|
2004
2176
|
snapshot
|
|
2005
2177
|
});
|
|
2006
2178
|
}
|
|
2007
|
-
/**
|
|
2179
|
+
/**
|
|
2180
|
+
* Drops a snapshot by id; an absent id is a no-op (no throw).
|
|
2181
|
+
*
|
|
2182
|
+
* @param id - The endpoint name to drop
|
|
2183
|
+
* @returns A promise that settles once the row is gone
|
|
2184
|
+
*/
|
|
2008
2185
|
async delete(id) {
|
|
2009
2186
|
await this.#table.remove(id);
|
|
2010
2187
|
}
|
|
@@ -2012,7 +2189,7 @@ var DatabaseTerminalStore = class {
|
|
|
2012
2189
|
//#endregion
|
|
2013
2190
|
//#region src/core/factories.ts
|
|
2014
2191
|
/**
|
|
2015
|
-
*
|
|
2192
|
+
* Creates the headless {@link PromptInterface} broker. It parks live forms and applies remote
|
|
2016
2193
|
* answers to the authoritative instances.
|
|
2017
2194
|
*
|
|
2018
2195
|
* @param options - See {@link PromptOptions}
|
|
@@ -2025,7 +2202,7 @@ var DatabaseTerminalStore = class {
|
|
|
2025
2202
|
*
|
|
2026
2203
|
* @example
|
|
2027
2204
|
* ```ts
|
|
2028
|
-
* import { createPrompt } from '@
|
|
2205
|
+
* import { createPrompt } from '@orkestrel/terminal'
|
|
2029
2206
|
* import { createForm } from '@orkestrel/form'
|
|
2030
2207
|
*
|
|
2031
2208
|
* const prompt = createPrompt()
|
|
@@ -2038,9 +2215,10 @@ function createPrompt(options) {
|
|
|
2038
2215
|
return new Prompt(options);
|
|
2039
2216
|
}
|
|
2040
2217
|
/**
|
|
2041
|
-
*
|
|
2042
|
-
* endpoint, dispatches each received form to a local
|
|
2043
|
-
* and POSTs the answer back. Universal — `fetch`
|
|
2218
|
+
* Creates the SSE prompt {@link PromptClientInterface} bridge — it connects to a remote broker's
|
|
2219
|
+
* SSE endpoint, dispatches each received form to a local
|
|
2220
|
+
* {@link import('./types.js').TerminalInterface}, and POSTs the answer back. Universal — `fetch`
|
|
2221
|
+
* and SSE are web standards.
|
|
2044
2222
|
*
|
|
2045
2223
|
* @param options - See {@link PromptClientOptions} (`url` + `terminal` required)
|
|
2046
2224
|
* @returns A {@link PromptClientInterface}
|
|
@@ -2050,11 +2228,11 @@ function createPrompt(options) {
|
|
|
2050
2228
|
* ends; it reconnects with the `delay` backoff unless `reconnect` is `false` / the client was
|
|
2051
2229
|
* `destroy`ed. Inject `options.fetch` (a scripted `fetch`) and `options.timer` to drive it
|
|
2052
2230
|
* deterministically in tests — no real network.
|
|
2053
|
-
* -
|
|
2231
|
+
* - **Wire narrowing.** Every decoded prompt is guard-narrowed before dispatch (never an `as`).
|
|
2054
2232
|
*
|
|
2055
2233
|
* @example
|
|
2056
2234
|
* ```ts
|
|
2057
|
-
* import { createPromptClient } from '@
|
|
2235
|
+
* import { createPromptClient } from '@orkestrel/terminal'
|
|
2058
2236
|
*
|
|
2059
2237
|
* const client = createPromptClient({ url: 'http://host/prompts', terminal })
|
|
2060
2238
|
* await client.connect()
|
|
@@ -2064,16 +2242,16 @@ function createPromptClient(options) {
|
|
|
2064
2242
|
return new PromptClient(options);
|
|
2065
2243
|
}
|
|
2066
2244
|
/**
|
|
2067
|
-
*
|
|
2068
|
-
*
|
|
2069
|
-
*
|
|
2245
|
+
* Creates the multi-endpoint {@link TerminalManager} — a named registry of {@link PromptInterface}
|
|
2246
|
+
* brokers so several parties can `ask` forms of each other by name, with a transitive cycle check
|
|
2247
|
+
* that refuses `DEADLOCK` across every in-flight ask.
|
|
2070
2248
|
*
|
|
2071
2249
|
* @param options - See {@link TerminalManagerOptions}
|
|
2072
2250
|
* @returns A {@link TerminalManager}
|
|
2073
2251
|
*
|
|
2074
2252
|
* @example
|
|
2075
2253
|
* ```ts
|
|
2076
|
-
* import { createTerminalManager } from '@
|
|
2254
|
+
* import { createTerminalManager } from '@orkestrel/terminal'
|
|
2077
2255
|
*
|
|
2078
2256
|
* const manager = createTerminalManager()
|
|
2079
2257
|
* manager.add('agent')
|
|
@@ -2083,14 +2261,14 @@ function createTerminalManager(options) {
|
|
|
2083
2261
|
return new TerminalManager(options);
|
|
2084
2262
|
}
|
|
2085
2263
|
/**
|
|
2086
|
-
*
|
|
2264
|
+
* Creates the in-memory {@link TerminalStoreInterface} — a process-lifetime `Map` of endpoint
|
|
2087
2265
|
* config snapshots, the default store backing a {@link TerminalManagerInterface}'s `open` / `save`.
|
|
2088
2266
|
*
|
|
2089
2267
|
* @returns A {@link TerminalStoreInterface}
|
|
2090
2268
|
*
|
|
2091
2269
|
* @example
|
|
2092
2270
|
* ```ts
|
|
2093
|
-
* import { createMemoryTerminalStore } from '@
|
|
2271
|
+
* import { createMemoryTerminalStore } from '@orkestrel/terminal'
|
|
2094
2272
|
*
|
|
2095
2273
|
* const store = createMemoryTerminalStore()
|
|
2096
2274
|
* ```
|
|
@@ -2099,16 +2277,17 @@ function createMemoryTerminalStore() {
|
|
|
2099
2277
|
return new MemoryTerminalStore();
|
|
2100
2278
|
}
|
|
2101
2279
|
/**
|
|
2102
|
-
*
|
|
2280
|
+
* Creates a {@link TerminalStoreInterface} backed by one table of the `databases` layer — the
|
|
2103
2281
|
* driver-pluggable twin of {@link createMemoryTerminalStore}, storing each endpoint's config
|
|
2104
|
-
* snapshot as one opaque JSON column.
|
|
2282
|
+
* snapshot as one opaque JSON column. The default driver is an in-memory `@orkestrel/database`
|
|
2283
|
+
* driver.
|
|
2105
2284
|
*
|
|
2106
2285
|
* @param driver - The {@link DriverInterface} backing the table (default an in-memory driver)
|
|
2107
2286
|
* @returns A {@link TerminalStoreInterface}
|
|
2108
2287
|
*
|
|
2109
2288
|
* @example
|
|
2110
2289
|
* ```ts
|
|
2111
|
-
* import { createDatabaseTerminalStore } from '@
|
|
2290
|
+
* import { createDatabaseTerminalStore } from '@orkestrel/terminal'
|
|
2112
2291
|
*
|
|
2113
2292
|
* const store = createDatabaseTerminalStore() // in-memory by default
|
|
2114
2293
|
* ```
|
|
@@ -2124,6 +2303,6 @@ function createDatabaseTerminalStore(driver = createMemoryDriver()) {
|
|
|
2124
2303
|
}).table("terminals"));
|
|
2125
2304
|
}
|
|
2126
2305
|
//#endregion
|
|
2127
|
-
export { ACCEPT_EVENT_STREAM, BACKSPACE, CONTROL_NAMES, CTRL_A, CTRL_C, CTRL_D, CTRL_E, CTRL_U, DEFAULT_MASK, DEFAULT_PROMPT_THEME, DEFAULT_PROMPT_TIMEOUT_MS, DEFAULT_RECONNECT_DELAY_MS, DELETE, DatabaseTerminalStore,
|
|
2306
|
+
export { ACCEPT_EVENT_STREAM, BACKSPACE, CONTROL_NAMES, CTRL_A, CTRL_C, CTRL_D, CTRL_E, CTRL_U, DEFAULT_MASK, DEFAULT_PROMPT_THEME, DEFAULT_PROMPT_TIMEOUT_MS, DEFAULT_RECONNECT_DELAY_MS, DELETE, DatabaseTerminalStore, HEADER_TOKEN, KEY_SS3, MemoryTerminalStore, NEWLINE, PROMPT_ICONS, PROMPT_ROLES, Prompt, PromptClient, RETURN, SEQUENCE_NAMES, SPACE, SSE_BUFFER_LIMIT, SSE_EVENTS, TAB, TerminalError, TerminalManager, createCheckboxState, createConfirmState, createDatabaseTerminalStore, createEditorState, createInputState, createMemoryTerminalStore, createPasswordState, createPrompt, createPromptClient, createPromptTheme, createSelectState, createTerminalManager, defaultTimer, editLine, globalFetch, isAbortError, isInsecureRemote, isPendingForm, isPendingFormStatus, isPrintable, isTerminalError, isTerminalSnapshot, isWireEvent, parseKey, reduceCheckbox, reduceConfirm, reduceEditor, reduceInput, reducePassword, reduceSelect, renderCheckboxView, renderConfirmView, renderEditorView, renderErrorLine, renderHintedHeader, renderInputView, renderPasswordView, renderPromptHeader, renderSelectView, renderSubmitHeader, sanitizeDisplayText, sanitizeSchema, sanitizeThemeIcons, serializeDestroy, serializeExpire, serializePending, toggleIndex };
|
|
2128
2307
|
|
|
2129
2308
|
//# sourceMappingURL=index.js.map
|