@orkestrel/terminal 0.0.13 → 0.0.14
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +3 -7
- package/dist/src/core/index.cjs +391 -249
- package/dist/src/core/index.cjs.map +1 -1
- package/dist/src/core/index.d.cts +532 -288
- package/dist/src/core/index.d.ts +532 -288
- package/dist/src/core/index.js +376 -232
- package/dist/src/core/index.js.map +1 -1
- package/dist/src/server/index.cjs +187 -201
- package/dist/src/server/index.cjs.map +1 -1
- package/dist/src/server/index.d.cts +129 -143
- package/dist/src/server/index.d.ts +129 -143
- package/dist/src/server/index.js +176 -185
- package/dist/src/server/index.js.map +1 -1
- package/package.json +15 -16
package/dist/src/core/index.js
CHANGED
|
@@ -1,47 +1,43 @@
|
|
|
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
|
-
*
|
|
38
|
+
* Holds the exact escape SEQUENCE → canonical key NAME table {@link import('./helpers.js').parseKey}
|
|
43
39
|
* consults for the navigation / editing keys. Covers BOTH the CSI form (`ESC[A`…) and the SS3
|
|
44
|
-
* form (`ESCOA`…) of the
|
|
40
|
+
* form (`ESCOA`…) of the arrows, plus the `home` / `end` / `delete` CSI sequences (with
|
|
45
41
|
* their numeric-tilde variants). The source of truth for the multi-byte key decode; frozen.
|
|
46
42
|
*
|
|
47
43
|
* @remarks
|
|
@@ -51,24 +47,24 @@ var KEY_SS3 = `O`;
|
|
|
51
47
|
* sees the wire encoding.
|
|
52
48
|
*/
|
|
53
49
|
var SEQUENCE_NAMES = Object.freeze({
|
|
54
|
-
[`${
|
|
55
|
-
[`${
|
|
56
|
-
[`${
|
|
57
|
-
[`${
|
|
50
|
+
[`${CSI}A`]: "up",
|
|
51
|
+
[`${CSI}B`]: "down",
|
|
52
|
+
[`${CSI}C`]: "right",
|
|
53
|
+
[`${CSI}D`]: "left",
|
|
58
54
|
[`${KEY_SS3}A`]: "up",
|
|
59
55
|
[`${KEY_SS3}B`]: "down",
|
|
60
56
|
[`${KEY_SS3}C`]: "right",
|
|
61
57
|
[`${KEY_SS3}D`]: "left",
|
|
62
|
-
[`${
|
|
63
|
-
[`${
|
|
64
|
-
[`${
|
|
65
|
-
[`${
|
|
66
|
-
[`${
|
|
67
|
-
[`${
|
|
68
|
-
[`${
|
|
58
|
+
[`${CSI}H`]: "home",
|
|
59
|
+
[`${CSI}F`]: "end",
|
|
60
|
+
[`${CSI}1~`]: "home",
|
|
61
|
+
[`${CSI}4~`]: "end",
|
|
62
|
+
[`${CSI}3~`]: "delete",
|
|
63
|
+
[`${CSI}7~`]: "home",
|
|
64
|
+
[`${CSI}8~`]: "end"
|
|
69
65
|
});
|
|
70
66
|
/**
|
|
71
|
-
*
|
|
67
|
+
* Holds the control BYTE (or CRLF pair) → key descriptor table {@link import('./helpers.js').parseKey}
|
|
72
68
|
* consults for the one-byte keys and the two-byte CRLF Enter chunk. Each entry carries the
|
|
73
69
|
* canonical `name` and whether it is a `ctrl` combination. The source of truth for that decode;
|
|
74
70
|
* frozen.
|
|
@@ -76,7 +72,7 @@ var SEQUENCE_NAMES = Object.freeze({
|
|
|
76
72
|
* @remarks
|
|
77
73
|
* `return` / `newline` / `return + newline` all map to `return` (one canonical Enter name) — a
|
|
78
74
|
* 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
|
|
75
|
+
* `delete` / `backspace` both map to `backspace` (the Backspace bytes); the Ctrl combos
|
|
80
76
|
* (`c` / `d` / `u` / `a` / `e`) carry `ctrl: true` so a reducer can match
|
|
81
77
|
* `key.ctrl && key.name === 'c'`. `escape` / `tab` / `space` are plain named keys.
|
|
82
78
|
*/
|
|
@@ -98,7 +94,7 @@ var CONTROL_NAMES = Object.freeze({
|
|
|
98
94
|
name: "tab",
|
|
99
95
|
ctrl: false
|
|
100
96
|
}),
|
|
101
|
-
[
|
|
97
|
+
[ESC]: Object.freeze({
|
|
102
98
|
name: "escape",
|
|
103
99
|
ctrl: false
|
|
104
100
|
}),
|
|
@@ -135,10 +131,10 @@ var CONTROL_NAMES = Object.freeze({
|
|
|
135
131
|
ctrl: true
|
|
136
132
|
})
|
|
137
133
|
});
|
|
138
|
-
/**
|
|
134
|
+
/** Names the default mask glyph {@link import('./helpers.js').createPasswordState} uses — `*`. */
|
|
139
135
|
var DEFAULT_MASK = "*";
|
|
140
136
|
/**
|
|
141
|
-
*
|
|
137
|
+
* Holds the default glyphs {@link DEFAULT_PROMPT_THEME} assembles its `icons` from. Read only when
|
|
142
138
|
* the default theme is assembled; a view reads its resolved theme and never this constant. Frozen.
|
|
143
139
|
*
|
|
144
140
|
* @remarks
|
|
@@ -156,7 +152,7 @@ var PROMPT_ICONS = Object.freeze({
|
|
|
156
152
|
unchecked: "☐"
|
|
157
153
|
});
|
|
158
154
|
/**
|
|
159
|
-
*
|
|
155
|
+
* Holds every {@link import('./types.js').PromptRole}, in one frozen list — the role axis's source of
|
|
160
156
|
* truth. {@link import('./helpers.js').createPromptTheme} walks it to merge a partial theme, and
|
|
161
157
|
* a consumer building a complete role map reads it rather than retyping every name.
|
|
162
158
|
*/
|
|
@@ -174,7 +170,7 @@ var PROMPT_ROLES = Object.freeze([
|
|
|
174
170
|
"description"
|
|
175
171
|
]);
|
|
176
172
|
/**
|
|
177
|
-
*
|
|
173
|
+
* Holds the {@link import('./types.js').PromptTheme} every prompt renders with unless its options supply
|
|
178
174
|
* another — the glyph set assembled from {@link PROMPT_ICONS} plus the console
|
|
179
175
|
* {@link import('@orkestrel/console').STATUS_ICONS} `success` / `error` marks, and the console
|
|
180
176
|
* {@link import('@orkestrel/console').Style} each role is painted with. Deeply frozen through the
|
|
@@ -228,30 +224,30 @@ var DEFAULT_PROMPT_THEME = Object.freeze({
|
|
|
228
224
|
description: freezeStyle({ attributes: ["dim"] })
|
|
229
225
|
})
|
|
230
226
|
});
|
|
231
|
-
/**
|
|
227
|
+
/** Holds how long (ms) the {@link import('./types.js').PromptInterface} broker parks an unanswered prompt before it expires — 5 minutes. */
|
|
232
228
|
var DEFAULT_PROMPT_TIMEOUT_MS = 3e5;
|
|
233
|
-
/**
|
|
229
|
+
/** Holds how long (ms) the {@link import('./types.js').PromptClientInterface} waits before each reconnect attempt — 2 seconds. */
|
|
234
230
|
var DEFAULT_RECONNECT_DELAY_MS = 2e3;
|
|
235
231
|
/**
|
|
236
|
-
*
|
|
232
|
+
* Holds the SSE `event:` names the broker emits and the {@link import('./types.js').PromptClientInterface}
|
|
237
233
|
* dispatches on. Frozen; the source of truth for the wire event vocabulary.
|
|
238
234
|
*
|
|
239
235
|
* @remarks
|
|
240
236
|
* - `pending` — a serialized {@link import('./types.js').PendingForm} to dispatch and answer.
|
|
241
237
|
* - `expire` — an `{ id }` payload: the broker expired or released a parked prompt (the client drops it).
|
|
242
|
-
* - `
|
|
238
|
+
* - `destroy` — the broker is going away; the client disconnects (no auto-reconnect) but stays reusable.
|
|
243
239
|
*/
|
|
244
240
|
var SSE_EVENTS = Object.freeze({
|
|
245
241
|
pending: "pending",
|
|
246
242
|
expire: "expire",
|
|
247
|
-
|
|
243
|
+
destroy: "destroy"
|
|
248
244
|
});
|
|
249
|
-
/**
|
|
245
|
+
/** Names the auth-token request header the {@link import('./types.js').PromptClientInterface} sends. */
|
|
250
246
|
var HEADER_TOKEN = "x-orkestrel-token";
|
|
251
|
-
/**
|
|
247
|
+
/** Names the `Accept` header value that opens the broker's SSE stream. */
|
|
252
248
|
var ACCEPT_EVENT_STREAM = "text/event-stream";
|
|
253
249
|
/**
|
|
254
|
-
*
|
|
250
|
+
* Sets the maximum number of characters the {@link import('./types.js').PromptClientInterface} lets its
|
|
255
251
|
* SSE parser buffer before treating the stream as hostile — 1 MiB, comfortably above any
|
|
256
252
|
* legitimate prompt payload. Passed as the `limit` to `createSSEParser` so an unterminated
|
|
257
253
|
* or oversized `data:` field cannot grow the buffer without bound (a memory-exhaustion guard).
|
|
@@ -260,7 +256,7 @@ var SSE_BUFFER_LIMIT = 1048576;
|
|
|
260
256
|
//#endregion
|
|
261
257
|
//#region src/core/errors.ts
|
|
262
258
|
/**
|
|
263
|
-
*
|
|
259
|
+
* Represents the error the terminal surfaces for its own refusals: parking on a destroyed or full broker, an
|
|
264
260
|
* unusable driver stream, a manager routing fault, or a ctrl-c cancellation. A parked form's own
|
|
265
261
|
* lifecycle failures reject through the form's `answer` with the form package's error, never with
|
|
266
262
|
* this one.
|
|
@@ -271,10 +267,17 @@ var SSE_BUFFER_LIMIT = 1048576;
|
|
|
271
267
|
* a caught value with {@link isTerminalError} and branch on `error.code`.
|
|
272
268
|
*/
|
|
273
269
|
var TerminalError = class extends Error {
|
|
274
|
-
/**
|
|
270
|
+
/** Holds the machine-readable condition — see {@link TerminalErrorCode}. */
|
|
275
271
|
code;
|
|
276
|
-
/**
|
|
272
|
+
/** Holds an optional context bag naming the offending values — see the class {@link TerminalError remarks}. */
|
|
277
273
|
context;
|
|
274
|
+
/**
|
|
275
|
+
* Builds one terminal refusal.
|
|
276
|
+
*
|
|
277
|
+
* @param code - The machine-readable {@link TerminalErrorCode} a caller branches on
|
|
278
|
+
* @param message - The human-readable reason
|
|
279
|
+
* @param context - The optional bag naming the offending values — see the class {@link TerminalError remarks}
|
|
280
|
+
*/
|
|
278
281
|
constructor(code, message, context) {
|
|
279
282
|
super(message);
|
|
280
283
|
this.name = "TerminalError";
|
|
@@ -283,10 +286,10 @@ var TerminalError = class extends Error {
|
|
|
283
286
|
}
|
|
284
287
|
};
|
|
285
288
|
/**
|
|
286
|
-
*
|
|
289
|
+
* Narrows an unknown caught value to a {@link TerminalError}.
|
|
287
290
|
*
|
|
288
291
|
* @param value - The value to test (typically a `catch` binding or a rejected prompt call)
|
|
289
|
-
* @returns
|
|
292
|
+
* @returns True if `value` is a {@link TerminalError}; false otherwise
|
|
290
293
|
*
|
|
291
294
|
* @example
|
|
292
295
|
* ```ts
|
|
@@ -304,17 +307,22 @@ function isTerminalError(value) {
|
|
|
304
307
|
}
|
|
305
308
|
//#endregion
|
|
306
309
|
//#region src/core/validators.ts
|
|
307
|
-
/**
|
|
310
|
+
/**
|
|
311
|
+
* Narrows an unknown value to a {@link PendingFormStatus}.
|
|
312
|
+
*
|
|
313
|
+
* @param value - The candidate ticket status
|
|
314
|
+
* @returns True if the value is one of the declared ticket statuses; false otherwise
|
|
315
|
+
*/
|
|
308
316
|
var isPendingFormStatus = literalOf("pending", "answered", "expired");
|
|
309
317
|
/**
|
|
310
|
-
*
|
|
318
|
+
* Narrows an unknown wire value to a {@link PendingForm} envelope.
|
|
311
319
|
*
|
|
312
320
|
* @remarks
|
|
313
321
|
* This guard checks the transport record and proves only that `schema` is a record. The Form
|
|
314
322
|
* package's `parseForm` owns the schema payload and its semantic audit.
|
|
315
323
|
*
|
|
316
324
|
* @param value - The decoded wire value to inspect
|
|
317
|
-
* @returns
|
|
325
|
+
* @returns True if the value is a complete pending-form envelope; false otherwise
|
|
318
326
|
*/
|
|
319
327
|
function isPendingForm(value) {
|
|
320
328
|
return recordOf({
|
|
@@ -327,17 +335,23 @@ function isPendingForm(value) {
|
|
|
327
335
|
}, ["from", "to"])(value);
|
|
328
336
|
}
|
|
329
337
|
/**
|
|
330
|
-
*
|
|
338
|
+
* Narrows an unknown value to a transport-neutral {@link WireEvent}.
|
|
331
339
|
*
|
|
332
340
|
* @param value - The candidate wire event
|
|
333
|
-
* @returns
|
|
341
|
+
* @returns True if the value carries an event name, serialized data, and an optional id; false otherwise
|
|
334
342
|
*/
|
|
335
343
|
var isWireEvent = recordOf({
|
|
336
344
|
event: isString,
|
|
337
345
|
data: isString,
|
|
338
346
|
id: isString
|
|
339
347
|
}, ["id"]);
|
|
340
|
-
/**
|
|
348
|
+
/**
|
|
349
|
+
* Narrows an unknown value to a {@link TerminalSnapshot} — the read boundary a store applies to an
|
|
350
|
+
* untrusted persisted row.
|
|
351
|
+
*
|
|
352
|
+
* @param value - The candidate snapshot read back from storage
|
|
353
|
+
* @returns True if the value carries a non-empty `id` and an optional numeric `timeout`; false otherwise
|
|
354
|
+
*/
|
|
341
355
|
var isTerminalSnapshot = recordOf({
|
|
342
356
|
id: isNonEmptyString,
|
|
343
357
|
timeout: isNumber
|
|
@@ -345,10 +359,10 @@ var isTerminalSnapshot = recordOf({
|
|
|
345
359
|
//#endregion
|
|
346
360
|
//#region src/core/helpers.ts
|
|
347
361
|
/**
|
|
348
|
-
*
|
|
362
|
+
* Decodes one keypress's bytes into a {@link KeyEvent} — total, never throws. A `Uint8Array` is
|
|
349
363
|
* read as UTF-8; the resulting string is matched against the known control bytes and the CRLF
|
|
350
364
|
* pair ({@link CONTROL_NAMES}) and escape sequences ({@link SEQUENCE_NAMES}), falling back to a
|
|
351
|
-
* single printable character. An unrecognized sequence
|
|
365
|
+
* single printable character. An unrecognized sequence carries NO `name`, with the raw `sequence`
|
|
352
366
|
* preserved.
|
|
353
367
|
*
|
|
354
368
|
* @remarks
|
|
@@ -361,8 +375,8 @@ var isTerminalSnapshot = recordOf({
|
|
|
361
375
|
* - **Printable character.** A single printable character becomes `name` = that character, with
|
|
362
376
|
* `shift` set when it is an uppercase letter. A multi-code-point printable (an emoji, a pasted
|
|
363
377
|
* 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.
|
|
378
|
+
* - **Unknown.** Anything else (an unrecognized escape, an empty input) yields an event with NO
|
|
379
|
+
* `name` — absence, never an empty string — total, so the driver never crashes on a stray byte.
|
|
366
380
|
*
|
|
367
381
|
* @param input - The raw keypress bytes, as a string or `Uint8Array`
|
|
368
382
|
* @returns The decoded {@link KeyEvent}
|
|
@@ -403,14 +417,19 @@ function parseKey(input) {
|
|
|
403
417
|
shift: first !== first.toLowerCase()
|
|
404
418
|
};
|
|
405
419
|
return {
|
|
406
|
-
name: "",
|
|
407
420
|
sequence,
|
|
408
421
|
ctrl: false,
|
|
409
422
|
meta: false,
|
|
410
423
|
shift: false
|
|
411
424
|
};
|
|
412
425
|
}
|
|
413
|
-
/**
|
|
426
|
+
/**
|
|
427
|
+
* Checks whether a single character is a printable (non-control) character — used by
|
|
428
|
+
* {@link parseKey}'s char fallback.
|
|
429
|
+
*
|
|
430
|
+
* @param character - The single character to test
|
|
431
|
+
* @returns True if the character is at or above space and is not DEL; false otherwise
|
|
432
|
+
*/
|
|
414
433
|
function isPrintable(character) {
|
|
415
434
|
if (character.length === 0) return false;
|
|
416
435
|
const code = character.codePointAt(0);
|
|
@@ -418,7 +437,7 @@ function isPrintable(character) {
|
|
|
418
437
|
return code >= 32 && code !== 127;
|
|
419
438
|
}
|
|
420
439
|
/**
|
|
421
|
-
*
|
|
440
|
+
* Builds a complete {@link PromptTheme} by merging a partial one over
|
|
422
441
|
* {@link DEFAULT_PROMPT_THEME}, leaf by leaf — each supplied icon replaces that glyph, each
|
|
423
442
|
* supplied role replaces that {@link Style}, and everything else keeps its default. Each supplied
|
|
424
443
|
* style is snapshotted through the console module's own
|
|
@@ -453,7 +472,7 @@ function createPromptTheme(options) {
|
|
|
453
472
|
});
|
|
454
473
|
}
|
|
455
474
|
/**
|
|
456
|
-
*
|
|
475
|
+
* Sanitizes text for one single-line display slot. Composes console's ANSI {@link strip} and C0
|
|
457
476
|
* {@link stripControls} passes with removal of tab, line feed, and carriage return.
|
|
458
477
|
*
|
|
459
478
|
* @param text - The text to sanitize for a glyph or hint slot
|
|
@@ -468,7 +487,7 @@ function sanitizeDisplayText(text) {
|
|
|
468
487
|
return stripControls(strip(text)).replaceAll(" ", "").replaceAll("\n", "").replaceAll("\r", "");
|
|
469
488
|
}
|
|
470
489
|
/**
|
|
471
|
-
*
|
|
490
|
+
* Sanitizes every terminal-readable string in a parsed form schema.
|
|
472
491
|
*
|
|
473
492
|
* @remarks
|
|
474
493
|
* Display strings pass through {@link sanitizeDisplayText}: labels, help, placeholders, masks,
|
|
@@ -510,6 +529,7 @@ function sanitizeSchema(schema) {
|
|
|
510
529
|
switch (field.control) {
|
|
511
530
|
case "text":
|
|
512
531
|
case "editor":
|
|
532
|
+
case "number":
|
|
513
533
|
fields.push({
|
|
514
534
|
...field,
|
|
515
535
|
...shared,
|
|
@@ -523,22 +543,10 @@ function sanitizeSchema(schema) {
|
|
|
523
543
|
...field.mask !== void 0 ? { mask: sanitizeDisplayText(field.mask) } : {}
|
|
524
544
|
});
|
|
525
545
|
break;
|
|
526
|
-
case "number":
|
|
527
|
-
fields.push({
|
|
528
|
-
...field,
|
|
529
|
-
...shared,
|
|
530
|
-
...field.placeholder !== void 0 ? { placeholder: sanitizeDisplayText(field.placeholder) } : {}
|
|
531
|
-
});
|
|
532
|
-
break;
|
|
533
546
|
case "date":
|
|
534
547
|
case "time":
|
|
535
548
|
case "datetime":
|
|
536
549
|
case "color":
|
|
537
|
-
fields.push({
|
|
538
|
-
...field,
|
|
539
|
-
...shared
|
|
540
|
-
});
|
|
541
|
-
break;
|
|
542
550
|
case "confirm":
|
|
543
551
|
fields.push({
|
|
544
552
|
...field,
|
|
@@ -546,17 +554,6 @@ function sanitizeSchema(schema) {
|
|
|
546
554
|
});
|
|
547
555
|
break;
|
|
548
556
|
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
557
|
case "checkbox":
|
|
561
558
|
fields.push({
|
|
562
559
|
...field,
|
|
@@ -585,7 +582,7 @@ function sanitizeSchema(schema) {
|
|
|
585
582
|
};
|
|
586
583
|
}
|
|
587
584
|
/**
|
|
588
|
-
*
|
|
585
|
+
* Sanitizes every glyph a wire-supplied {@link PromptThemeOptions} carries for a single-line display
|
|
589
586
|
* slot. Only the icons need it: a role is guard-narrowed to a console {@link Style}, whose colors
|
|
590
587
|
* and attributes are fixed name sets, so no role can carry a byte a terminal would act on.
|
|
591
588
|
*
|
|
@@ -602,13 +599,21 @@ function sanitizeThemeIcons(theme) {
|
|
|
602
599
|
icons: sanitized
|
|
603
600
|
};
|
|
604
601
|
}
|
|
605
|
-
/**
|
|
606
|
-
|
|
602
|
+
/**
|
|
603
|
+
* Renders the styled question header (`? message`) — the leading line every active prompt view
|
|
604
|
+
* shares, themed by the `question` + `message` roles.
|
|
605
|
+
*
|
|
606
|
+
* @param styler - The console styler that renders each role
|
|
607
|
+
* @param theme - The resolved prompt theme
|
|
608
|
+
* @param message - The prompt's question text
|
|
609
|
+
* @returns The rendered question header
|
|
610
|
+
*/
|
|
611
|
+
function renderPromptHeader(styler, theme, message) {
|
|
607
612
|
return `${styler.render(theme.roles.question, theme.icons.question)} ${styler.render(theme.roles.message, message)}`;
|
|
608
613
|
}
|
|
609
614
|
/**
|
|
610
|
-
*
|
|
611
|
-
* when no hint is supplied.
|
|
615
|
+
* Renders a question header followed by a key hint painted with the `hint` role, or the header
|
|
616
|
+
* alone when no hint is supplied.
|
|
612
617
|
*
|
|
613
618
|
* @param styler - The console styler that renders each role
|
|
614
619
|
* @param theme - The resolved prompt theme
|
|
@@ -616,20 +621,35 @@ function promptHeader(styler, theme, message) {
|
|
|
616
621
|
* @param hint - The optional key hint to append
|
|
617
622
|
* @returns The rendered header with the optional hint
|
|
618
623
|
*/
|
|
619
|
-
function
|
|
620
|
-
const head =
|
|
624
|
+
function renderHintedHeader(styler, theme, message, hint) {
|
|
625
|
+
const head = renderPromptHeader(styler, theme, message);
|
|
621
626
|
return hint === void 0 ? head : `${head} ${styler.render(theme.roles.hint, hint)}`;
|
|
622
627
|
}
|
|
623
|
-
/**
|
|
624
|
-
|
|
628
|
+
/**
|
|
629
|
+
* Renders the styled submit line (`✔ message`) — the committed header an interactive prompt shows
|
|
630
|
+
* after it resolves, themed by the `success` + `message` roles.
|
|
631
|
+
*
|
|
632
|
+
* @param styler - The console styler that renders each role
|
|
633
|
+
* @param theme - The resolved prompt theme
|
|
634
|
+
* @param message - The settled prompt's question text
|
|
635
|
+
* @returns The rendered committed header
|
|
636
|
+
*/
|
|
637
|
+
function renderSubmitHeader(styler, theme, message) {
|
|
625
638
|
return `${styler.render(theme.roles.success, theme.icons.success)} ${styler.render(theme.roles.message, message)}`;
|
|
626
639
|
}
|
|
627
|
-
/**
|
|
628
|
-
|
|
640
|
+
/**
|
|
641
|
+
* Renders the styled failure line (`✖ message`) a form driver appends for a refused field.
|
|
642
|
+
*
|
|
643
|
+
* @param styler - The console styler that renders each role
|
|
644
|
+
* @param theme - The resolved prompt theme
|
|
645
|
+
* @param message - The failure text, normally the field's label and the reason
|
|
646
|
+
* @returns The rendered failure line
|
|
647
|
+
*/
|
|
648
|
+
function renderErrorLine(styler, theme, message) {
|
|
629
649
|
return `${styler.render(theme.roles.error, theme.icons.error)} ${styler.render(theme.roles.error, message)}`;
|
|
630
650
|
}
|
|
631
651
|
/**
|
|
632
|
-
*
|
|
652
|
+
* Builds the initial text-field key state.
|
|
633
653
|
*
|
|
634
654
|
* @param field - The text field to render
|
|
635
655
|
* @param styler - The styler used to render the view
|
|
@@ -645,22 +665,31 @@ function createInputState(field, styler = createStyler(), theme) {
|
|
|
645
665
|
value: ""
|
|
646
666
|
};
|
|
647
667
|
}
|
|
648
|
-
/**
|
|
649
|
-
|
|
668
|
+
/**
|
|
669
|
+
* Renders a text-field key state as a styled view.
|
|
670
|
+
*
|
|
671
|
+
* @param state - The text field's current reducer state
|
|
672
|
+
* @returns The rendered single-line view — header, pointer, and the typed value or the default
|
|
673
|
+
*/
|
|
674
|
+
function renderInputView(state) {
|
|
650
675
|
const content = state.value.length > 0 ? state.value : state.default;
|
|
651
676
|
const role = state.value.length > 0 ? state.theme.roles.content : state.theme.roles.hint;
|
|
652
677
|
const shown = state.styler.render(role, sanitizeDisplayText(content));
|
|
653
|
-
return `${
|
|
678
|
+
return `${renderPromptHeader(state.styler, state.theme, state.message)} ${state.styler.render(state.theme.roles.pointer, state.theme.icons.pointer)} ${shown}`;
|
|
654
679
|
}
|
|
655
680
|
/**
|
|
656
|
-
*
|
|
681
|
+
* Advances an input prompt by one {@link KeyEvent} — the pure `(state, key) → PromptStep<string>`
|
|
657
682
|
* reducer. Printable characters extend the value; backspace shrinks it; ctrl-u clears it; ctrl-c
|
|
658
683
|
* cancels; return produces the candidate value, with an empty line falling back to the default.
|
|
684
|
+
*
|
|
685
|
+
* @param state - The text field's current reducer state
|
|
686
|
+
* @param key - The decoded keypress to apply
|
|
687
|
+
* @returns The next step — the new state, the rendered view, the status, and the value on submit
|
|
659
688
|
*/
|
|
660
|
-
function
|
|
689
|
+
function reduceInput(state, key) {
|
|
661
690
|
if (key.ctrl && key.name === "c") return {
|
|
662
691
|
state,
|
|
663
|
-
view:
|
|
692
|
+
view: renderInputView(state),
|
|
664
693
|
status: "cancel"
|
|
665
694
|
};
|
|
666
695
|
if (key.name === "return") {
|
|
@@ -670,7 +699,7 @@ function inputReduce(state, key) {
|
|
|
670
699
|
...state,
|
|
671
700
|
value: answer
|
|
672
701
|
},
|
|
673
|
-
view: `${
|
|
702
|
+
view: `${renderSubmitHeader(state.styler, state.theme, state.message)} ${state.styler.render(state.theme.roles.hint, sanitizeDisplayText(answer))}`,
|
|
674
703
|
status: "submit",
|
|
675
704
|
value: answer
|
|
676
705
|
};
|
|
@@ -678,7 +707,7 @@ function inputReduce(state, key) {
|
|
|
678
707
|
const value = editLine(state.value, key);
|
|
679
708
|
if (value === void 0) return {
|
|
680
709
|
state,
|
|
681
|
-
view:
|
|
710
|
+
view: renderInputView(state),
|
|
682
711
|
status: "active"
|
|
683
712
|
};
|
|
684
713
|
const next = {
|
|
@@ -687,12 +716,12 @@ function inputReduce(state, key) {
|
|
|
687
716
|
};
|
|
688
717
|
return {
|
|
689
718
|
state: next,
|
|
690
|
-
view:
|
|
719
|
+
view: renderInputView(next),
|
|
691
720
|
status: "active"
|
|
692
721
|
};
|
|
693
722
|
}
|
|
694
723
|
/**
|
|
695
|
-
*
|
|
724
|
+
* Builds the initial password-field key state.
|
|
696
725
|
*
|
|
697
726
|
* @param field - The password field to render
|
|
698
727
|
* @param styler - The styler used to render the view
|
|
@@ -708,32 +737,41 @@ function createPasswordState(field, styler = createStyler(), theme) {
|
|
|
708
737
|
value: ""
|
|
709
738
|
};
|
|
710
739
|
}
|
|
711
|
-
/**
|
|
712
|
-
|
|
740
|
+
/**
|
|
741
|
+
* Renders a password-field key state as a styled view.
|
|
742
|
+
*
|
|
743
|
+
* @param state - The password field's current reducer state
|
|
744
|
+
* @returns The rendered view, with the mask repeated in place of the typed value
|
|
745
|
+
*/
|
|
746
|
+
function renderPasswordView(state) {
|
|
713
747
|
const masked = state.styler.render(state.theme.roles.content, state.mask.repeat(state.value.length));
|
|
714
|
-
return `${
|
|
748
|
+
return `${renderPromptHeader(state.styler, state.theme, state.message)} ${state.styler.render(state.theme.roles.pointer, state.theme.icons.pointer)} ${masked}`;
|
|
715
749
|
}
|
|
716
750
|
/**
|
|
717
|
-
*
|
|
718
|
-
* reducer. Identical line-editing to {@link
|
|
751
|
+
* Advances a password prompt by one {@link KeyEvent} — the pure `(state, key) → PromptStep<string>`
|
|
752
|
+
* reducer. Identical line-editing to {@link reduceInput} (printable extends, backspace shrinks,
|
|
719
753
|
* ctrl-u clears, ctrl-c cancels) but the view masks the value. Return produces the candidate value.
|
|
754
|
+
*
|
|
755
|
+
* @param state - The password field's current reducer state
|
|
756
|
+
* @param key - The decoded keypress to apply
|
|
757
|
+
* @returns The next step — the new state, the masked view, the status, and the value on submit
|
|
720
758
|
*/
|
|
721
|
-
function
|
|
759
|
+
function reducePassword(state, key) {
|
|
722
760
|
if (key.ctrl && key.name === "c") return {
|
|
723
761
|
state,
|
|
724
|
-
view:
|
|
762
|
+
view: renderPasswordView(state),
|
|
725
763
|
status: "cancel"
|
|
726
764
|
};
|
|
727
765
|
if (key.name === "return") return {
|
|
728
766
|
state,
|
|
729
|
-
view: `${
|
|
767
|
+
view: `${renderSubmitHeader(state.styler, state.theme, state.message)} ${state.styler.render(state.theme.roles.hint, state.mask.repeat(state.value.length))}`,
|
|
730
768
|
status: "submit",
|
|
731
769
|
value: state.value
|
|
732
770
|
};
|
|
733
771
|
const value = editLine(state.value, key);
|
|
734
772
|
if (value === void 0) return {
|
|
735
773
|
state,
|
|
736
|
-
view:
|
|
774
|
+
view: renderPasswordView(state),
|
|
737
775
|
status: "active"
|
|
738
776
|
};
|
|
739
777
|
const next = {
|
|
@@ -742,12 +780,12 @@ function passwordReduce(state, key) {
|
|
|
742
780
|
};
|
|
743
781
|
return {
|
|
744
782
|
state: next,
|
|
745
|
-
view:
|
|
783
|
+
view: renderPasswordView(next),
|
|
746
784
|
status: "active"
|
|
747
785
|
};
|
|
748
786
|
}
|
|
749
787
|
/**
|
|
750
|
-
*
|
|
788
|
+
* Builds the initial confirm-field key state.
|
|
751
789
|
*
|
|
752
790
|
* @param field - The confirm field to render
|
|
753
791
|
* @param styler - The styler used to render the view
|
|
@@ -763,43 +801,50 @@ function createConfirmState(field, styler = createStyler(), theme) {
|
|
|
763
801
|
};
|
|
764
802
|
}
|
|
765
803
|
/**
|
|
766
|
-
*
|
|
804
|
+
* Renders a confirm-field key state as a styled view. The selected role paints the default letter.
|
|
805
|
+
*
|
|
806
|
+
* @param state - The confirm field's current reducer state
|
|
807
|
+
* @returns The rendered view — the header and the yes/no group with the default capitalized
|
|
767
808
|
*/
|
|
768
|
-
function
|
|
769
|
-
const head =
|
|
809
|
+
function renderConfirmView(state) {
|
|
810
|
+
const head = renderPromptHeader(state.styler, state.theme, state.message);
|
|
770
811
|
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
812
|
return `${head} ${state.styler.render(state.theme.roles.hint, "(")}${answer}${state.styler.render(state.theme.roles.hint, ")")}`;
|
|
772
813
|
}
|
|
773
814
|
/**
|
|
774
|
-
*
|
|
815
|
+
* Advances a confirm prompt by one {@link KeyEvent} — the pure `(state, key) → PromptStep<boolean>`
|
|
775
816
|
* reducer. `y` / `Y` submits `true`, `n` / `N` submits `false`, return on an empty line submits
|
|
776
817
|
* the `default`, ctrl-c cancels; any other key is ignored (stays active).
|
|
818
|
+
*
|
|
819
|
+
* @param state - The confirm field's current reducer state
|
|
820
|
+
* @param key - The decoded keypress to apply
|
|
821
|
+
* @returns The next step — the state, the rendered view, the status, and the answer on submit
|
|
777
822
|
*/
|
|
778
|
-
function
|
|
823
|
+
function reduceConfirm(state, key) {
|
|
779
824
|
if (key.ctrl && key.name === "c") return {
|
|
780
825
|
state,
|
|
781
|
-
view:
|
|
826
|
+
view: renderConfirmView(state),
|
|
782
827
|
status: "cancel"
|
|
783
828
|
};
|
|
784
829
|
let answer;
|
|
785
|
-
const choice = key.name
|
|
830
|
+
const choice = key.name?.toLowerCase();
|
|
786
831
|
if (key.name === "return") answer = state.default;
|
|
787
832
|
else if (choice === "y") answer = true;
|
|
788
833
|
else if (choice === "n") answer = false;
|
|
789
834
|
if (answer === void 0) return {
|
|
790
835
|
state,
|
|
791
|
-
view:
|
|
836
|
+
view: renderConfirmView(state),
|
|
792
837
|
status: "active"
|
|
793
838
|
};
|
|
794
839
|
return {
|
|
795
840
|
state,
|
|
796
|
-
view: `${
|
|
841
|
+
view: `${renderSubmitHeader(state.styler, state.theme, state.message)} ${state.styler.render(state.theme.roles.hint, answer ? "yes" : "no")}`,
|
|
797
842
|
status: "submit",
|
|
798
843
|
value: answer
|
|
799
844
|
};
|
|
800
845
|
}
|
|
801
846
|
/**
|
|
802
|
-
*
|
|
847
|
+
* Builds the initial select-field key state.
|
|
803
848
|
*
|
|
804
849
|
* @param field - The select field to render
|
|
805
850
|
* @param styler - The styler used to render the view
|
|
@@ -817,30 +862,39 @@ function createSelectState(field, styler = createStyler(), theme) {
|
|
|
817
862
|
focused: index >= 0 ? index : 0
|
|
818
863
|
};
|
|
819
864
|
}
|
|
820
|
-
/**
|
|
821
|
-
|
|
865
|
+
/**
|
|
866
|
+
* Renders a select-field key state as a multi-line styled view.
|
|
867
|
+
*
|
|
868
|
+
* @param state - The select field's current reducer state
|
|
869
|
+
* @returns The rendered view — the header, then one row per choice with the focused row marked
|
|
870
|
+
*/
|
|
871
|
+
function renderSelectView(state) {
|
|
822
872
|
const lines = state.choices.map((choice, index) => {
|
|
823
873
|
const active = index === state.focused;
|
|
824
874
|
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
875
|
});
|
|
826
|
-
return [
|
|
876
|
+
return [renderPromptHeader(state.styler, state.theme, state.message), ...lines].join("\n");
|
|
827
877
|
}
|
|
828
878
|
/**
|
|
829
|
-
*
|
|
879
|
+
* Advances a select prompt by one {@link KeyEvent} — the pure `(state, key) → PromptStep<string>`
|
|
830
880
|
* reducer. `up` / `down` (and `k` / `j`) move the focus, WRAPPING at the ends; return submits the
|
|
831
881
|
* focused choice's `value`; ctrl-c cancels. An empty choice list can never submit (a higher layer
|
|
832
882
|
* guards against it); any other key is ignored.
|
|
883
|
+
*
|
|
884
|
+
* @param state - The select field's current reducer state
|
|
885
|
+
* @param key - The decoded keypress to apply
|
|
886
|
+
* @returns The next step — the state, the rendered view, the status, and the chosen value on submit
|
|
833
887
|
*/
|
|
834
|
-
function
|
|
888
|
+
function reduceSelect(state, key) {
|
|
835
889
|
if (key.ctrl && key.name === "c") return {
|
|
836
890
|
state,
|
|
837
|
-
view:
|
|
891
|
+
view: renderSelectView(state),
|
|
838
892
|
status: "cancel"
|
|
839
893
|
};
|
|
840
894
|
const count = state.choices.length;
|
|
841
895
|
if (count === 0) return {
|
|
842
896
|
state,
|
|
843
|
-
view:
|
|
897
|
+
view: renderSelectView(state),
|
|
844
898
|
status: "active"
|
|
845
899
|
};
|
|
846
900
|
if (key.name === "up" || key.name === "k") {
|
|
@@ -850,7 +904,7 @@ function selectReduce(state, key) {
|
|
|
850
904
|
};
|
|
851
905
|
return {
|
|
852
906
|
state: next,
|
|
853
|
-
view:
|
|
907
|
+
view: renderSelectView(next),
|
|
854
908
|
status: "active"
|
|
855
909
|
};
|
|
856
910
|
}
|
|
@@ -861,7 +915,7 @@ function selectReduce(state, key) {
|
|
|
861
915
|
};
|
|
862
916
|
return {
|
|
863
917
|
state: next,
|
|
864
|
-
view:
|
|
918
|
+
view: renderSelectView(next),
|
|
865
919
|
status: "active"
|
|
866
920
|
};
|
|
867
921
|
}
|
|
@@ -870,19 +924,19 @@ function selectReduce(state, key) {
|
|
|
870
924
|
const value = choice?.value ?? "";
|
|
871
925
|
return {
|
|
872
926
|
state,
|
|
873
|
-
view: `${
|
|
927
|
+
view: `${renderSubmitHeader(state.styler, state.theme, state.message)} ${state.styler.render(state.theme.roles.hint, choice?.label ?? "")}`,
|
|
874
928
|
status: "submit",
|
|
875
929
|
value
|
|
876
930
|
};
|
|
877
931
|
}
|
|
878
932
|
return {
|
|
879
933
|
state,
|
|
880
|
-
view:
|
|
934
|
+
view: renderSelectView(state),
|
|
881
935
|
status: "active"
|
|
882
936
|
};
|
|
883
937
|
}
|
|
884
938
|
/**
|
|
885
|
-
*
|
|
939
|
+
* Builds the initial checkbox-field key state.
|
|
886
940
|
*
|
|
887
941
|
* @param field - The checkbox field to render
|
|
888
942
|
* @param styler - The styler used to render the view
|
|
@@ -904,8 +958,13 @@ function createCheckboxState(field, styler = createStyler(), theme) {
|
|
|
904
958
|
checked
|
|
905
959
|
};
|
|
906
960
|
}
|
|
907
|
-
/**
|
|
908
|
-
|
|
961
|
+
/**
|
|
962
|
+
* Renders a checkbox-field key state as a multi-line styled view.
|
|
963
|
+
*
|
|
964
|
+
* @param state - The checkbox field's current reducer state
|
|
965
|
+
* @returns The rendered view — the header, one box per choice, and the selected count
|
|
966
|
+
*/
|
|
967
|
+
function renderCheckboxView(state) {
|
|
909
968
|
const lines = state.choices.map((choice, index) => {
|
|
910
969
|
const active = index === state.focused;
|
|
911
970
|
const ticked = state.checked.includes(index);
|
|
@@ -913,21 +972,25 @@ function checkboxView(state) {
|
|
|
913
972
|
});
|
|
914
973
|
const summary = state.styler.render(state.theme.roles.hint, `${state.checked.length} selected`);
|
|
915
974
|
return [
|
|
916
|
-
|
|
975
|
+
renderPromptHeader(state.styler, state.theme, state.message),
|
|
917
976
|
...lines,
|
|
918
977
|
summary
|
|
919
978
|
].join("\n");
|
|
920
979
|
}
|
|
921
980
|
/**
|
|
922
|
-
*
|
|
981
|
+
* Advances a checkbox prompt by one {@link KeyEvent} — the pure
|
|
923
982
|
* `(state, key) → PromptStep<readonly string[]>` reducer. `up` / `down` (and `k` / `j`) move the
|
|
924
983
|
* focus (wrapping); `space` toggles the focused index in the checked set; return submits the
|
|
925
984
|
* checked values in choice order; ctrl-c cancels. The form applies selection-count rules.
|
|
985
|
+
*
|
|
986
|
+
* @param state - The checkbox field's current reducer state
|
|
987
|
+
* @param key - The decoded keypress to apply
|
|
988
|
+
* @returns The next step — the state, the rendered view, the status, and the ticked values on submit
|
|
926
989
|
*/
|
|
927
|
-
function
|
|
990
|
+
function reduceCheckbox(state, key) {
|
|
928
991
|
if (key.ctrl && key.name === "c") return {
|
|
929
992
|
state,
|
|
930
|
-
view:
|
|
993
|
+
view: renderCheckboxView(state),
|
|
931
994
|
status: "cancel"
|
|
932
995
|
};
|
|
933
996
|
const count = state.choices.length;
|
|
@@ -938,7 +1001,7 @@ function checkboxReduce(state, key) {
|
|
|
938
1001
|
};
|
|
939
1002
|
return {
|
|
940
1003
|
state: next,
|
|
941
|
-
view:
|
|
1004
|
+
view: renderCheckboxView(next),
|
|
942
1005
|
status: "active"
|
|
943
1006
|
};
|
|
944
1007
|
}
|
|
@@ -949,7 +1012,7 @@ function checkboxReduce(state, key) {
|
|
|
949
1012
|
};
|
|
950
1013
|
return {
|
|
951
1014
|
state: next,
|
|
952
|
-
view:
|
|
1015
|
+
view: renderCheckboxView(next),
|
|
953
1016
|
status: "active"
|
|
954
1017
|
};
|
|
955
1018
|
}
|
|
@@ -961,7 +1024,7 @@ function checkboxReduce(state, key) {
|
|
|
961
1024
|
};
|
|
962
1025
|
return {
|
|
963
1026
|
state: next,
|
|
964
|
-
view:
|
|
1027
|
+
view: renderCheckboxView(next),
|
|
965
1028
|
status: "active"
|
|
966
1029
|
};
|
|
967
1030
|
}
|
|
@@ -971,23 +1034,30 @@ function checkboxReduce(state, key) {
|
|
|
971
1034
|
const summary = ordered.map((index) => state.choices[index]?.label).filter((name) => name !== void 0).join(", ");
|
|
972
1035
|
return {
|
|
973
1036
|
state,
|
|
974
|
-
view: `${
|
|
1037
|
+
view: `${renderSubmitHeader(state.styler, state.theme, state.message)} ${state.styler.render(state.theme.roles.hint, summary)}`,
|
|
975
1038
|
status: "submit",
|
|
976
1039
|
value: values
|
|
977
1040
|
};
|
|
978
1041
|
}
|
|
979
1042
|
return {
|
|
980
1043
|
state,
|
|
981
|
-
view:
|
|
1044
|
+
view: renderCheckboxView(state),
|
|
982
1045
|
status: "active"
|
|
983
1046
|
};
|
|
984
1047
|
}
|
|
985
|
-
/**
|
|
1048
|
+
/**
|
|
1049
|
+
* Toggles `index` in a readonly index list — copy-on-write, returning the new sorted-by-insertion
|
|
1050
|
+
* list.
|
|
1051
|
+
*
|
|
1052
|
+
* @param indices - The ticked indices, in tick order
|
|
1053
|
+
* @param index - The index to add when absent, or drop when present
|
|
1054
|
+
* @returns A new list carrying the toggled membership; the input is never mutated
|
|
1055
|
+
*/
|
|
986
1056
|
function toggleIndex(indices, index) {
|
|
987
1057
|
return indices.includes(index) ? indices.filter((i) => i !== index) : [...indices, index];
|
|
988
1058
|
}
|
|
989
1059
|
/**
|
|
990
|
-
*
|
|
1060
|
+
* Builds the initial editor-field key state.
|
|
991
1061
|
*
|
|
992
1062
|
* @param field - The editor field to render
|
|
993
1063
|
* @param styler - The styler used to render the view
|
|
@@ -1005,23 +1075,30 @@ function createEditorState(field, styler = createStyler(), theme) {
|
|
|
1005
1075
|
};
|
|
1006
1076
|
}
|
|
1007
1077
|
/**
|
|
1008
|
-
*
|
|
1078
|
+
* Renders an editor-field key state as a multi-line styled view with its finish hint.
|
|
1079
|
+
*
|
|
1080
|
+
* @param state - The editor field's current reducer state
|
|
1081
|
+
* @returns The rendered view — the hinted header, the committed lines, and the line in progress
|
|
1009
1082
|
*/
|
|
1010
|
-
function
|
|
1011
|
-
const head =
|
|
1083
|
+
function renderEditorView(state) {
|
|
1084
|
+
const head = renderHintedHeader(state.styler, state.theme, state.message, "(Ctrl+D to finish)");
|
|
1012
1085
|
const pointer = state.styler.render(state.theme.roles.pointer, state.theme.icons.pointer);
|
|
1013
1086
|
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
1087
|
}
|
|
1015
1088
|
/**
|
|
1016
|
-
*
|
|
1089
|
+
* Advances an editor prompt by one {@link KeyEvent} — the pure `(state, key) → PromptStep<string>`
|
|
1017
1090
|
* reducer. Printable characters extend the current line; backspace shrinks it; return commits the
|
|
1018
1091
|
* current line and starts a fresh one; ctrl-d FINISHES (joining all lines, falling back to the
|
|
1019
1092
|
* default when empty); ctrl-c cancels. The form validates the candidate after the driver fills it.
|
|
1093
|
+
*
|
|
1094
|
+
* @param state - The editor field's current reducer state
|
|
1095
|
+
* @param key - The decoded keypress to apply
|
|
1096
|
+
* @returns The next step — the state, the rendered view, the status, and the joined text on submit
|
|
1020
1097
|
*/
|
|
1021
|
-
function
|
|
1098
|
+
function reduceEditor(state, key) {
|
|
1022
1099
|
if (key.ctrl && key.name === "c") return {
|
|
1023
1100
|
state,
|
|
1024
|
-
view:
|
|
1101
|
+
view: renderEditorView(state),
|
|
1025
1102
|
status: "cancel"
|
|
1026
1103
|
};
|
|
1027
1104
|
if (key.ctrl && key.name === "d") {
|
|
@@ -1030,7 +1107,7 @@ function editorReduce(state, key) {
|
|
|
1030
1107
|
const answer = joined.length > 0 ? joined : state.default;
|
|
1031
1108
|
return {
|
|
1032
1109
|
state,
|
|
1033
|
-
view: `${
|
|
1110
|
+
view: `${renderSubmitHeader(state.styler, state.theme, state.message)} ${state.styler.render(state.theme.roles.hint, `${String(lines.length)} line${lines.length === 1 ? "" : "s"}`)}`,
|
|
1034
1111
|
status: "submit",
|
|
1035
1112
|
value: answer
|
|
1036
1113
|
};
|
|
@@ -1043,14 +1120,14 @@ function editorReduce(state, key) {
|
|
|
1043
1120
|
};
|
|
1044
1121
|
return {
|
|
1045
1122
|
state: next,
|
|
1046
|
-
view:
|
|
1123
|
+
view: renderEditorView(next),
|
|
1047
1124
|
status: "active"
|
|
1048
1125
|
};
|
|
1049
1126
|
}
|
|
1050
1127
|
const current = editLine(state.current, key);
|
|
1051
1128
|
if (current === void 0) return {
|
|
1052
1129
|
state,
|
|
1053
|
-
view:
|
|
1130
|
+
view: renderEditorView(state),
|
|
1054
1131
|
status: "active"
|
|
1055
1132
|
};
|
|
1056
1133
|
const next = {
|
|
@@ -1059,47 +1136,65 @@ function editorReduce(state, key) {
|
|
|
1059
1136
|
};
|
|
1060
1137
|
return {
|
|
1061
1138
|
state: next,
|
|
1062
|
-
view:
|
|
1139
|
+
view: renderEditorView(next),
|
|
1063
1140
|
status: "active"
|
|
1064
1141
|
};
|
|
1065
1142
|
}
|
|
1066
1143
|
/**
|
|
1067
|
-
*
|
|
1144
|
+
* Applies a single line-editing {@link KeyEvent} to a text buffer — the editing shared by input /
|
|
1068
1145
|
* password / editor. A printable key appends its character; `backspace` drops the last character;
|
|
1069
|
-
* `space` appends a space; ctrl-u clears the line.
|
|
1070
|
-
*
|
|
1146
|
+
* `space` appends a space; ctrl-u clears the line.
|
|
1147
|
+
*
|
|
1148
|
+
* @param value - The buffer the field holds so far
|
|
1149
|
+
* @param key - The decoded keypress to apply
|
|
1150
|
+
* @returns The new buffer, or `undefined` when the key does not edit the line, so the caller can
|
|
1151
|
+
* leave the state untouched
|
|
1071
1152
|
*/
|
|
1072
1153
|
function editLine(value, key) {
|
|
1073
1154
|
if (key.ctrl && key.name === "u") return "";
|
|
1074
1155
|
if (key.name === "backspace") return value.slice(0, -1);
|
|
1075
1156
|
if (key.name === "space") return `${value} `;
|
|
1076
|
-
if (!key.ctrl && !key.meta && [...key.name].length === 1 && isPrintable(key.name)) return `${value}${key.sequence}`;
|
|
1157
|
+
if (!key.ctrl && !key.meta && key.name !== void 0 && [...key.name].length === 1 && isPrintable(key.name)) return `${value}${key.sequence}`;
|
|
1077
1158
|
}
|
|
1078
1159
|
/**
|
|
1079
|
-
*
|
|
1080
|
-
* wrapper that arms `callback` after `ms` and returns a {@link
|
|
1160
|
+
* Implements the default {@link import('./types.js').TimerHandler} — a thin host `setTimeout` / `clearTimeout`
|
|
1161
|
+
* wrapper that arms `callback` after `ms` and returns a {@link TimerCancelFunction}. The deadline seam
|
|
1081
1162
|
* behind both the {@link import('./Prompt.js').Prompt} broker (its expiry) and the
|
|
1082
1163
|
* {@link import('./PromptClient.js').PromptClient} (its reconnect backoff); a test injects a
|
|
1083
1164
|
* deterministic timer instead, so neither entity touches real time.
|
|
1165
|
+
*
|
|
1166
|
+
* @param callback - The deadline callback to arm
|
|
1167
|
+
* @param ms - How long to wait before firing it, in milliseconds
|
|
1168
|
+
* @returns The {@link TimerCancelFunction} that clears the armed deadline
|
|
1084
1169
|
*/
|
|
1085
1170
|
function defaultTimer(callback, ms) {
|
|
1086
1171
|
const handle = setTimeout(callback, ms);
|
|
1087
1172
|
return () => clearTimeout(handle);
|
|
1088
1173
|
}
|
|
1089
|
-
/**
|
|
1174
|
+
/**
|
|
1175
|
+
* Implements the default {@link import('./types.js').FetchHandler} — the global `fetch`, adapted to
|
|
1176
|
+
* the minimal injected shape the {@link import('./PromptClient.js').PromptClient} uses.
|
|
1177
|
+
*
|
|
1178
|
+
* @param input - The request URL
|
|
1179
|
+
* @param init - The request init the client sets — method, headers, body, and abort signal
|
|
1180
|
+
* @returns The host `fetch` promise for that request
|
|
1181
|
+
*/
|
|
1090
1182
|
function globalFetch(input, init) {
|
|
1091
1183
|
return fetch(input, init);
|
|
1092
1184
|
}
|
|
1093
1185
|
/**
|
|
1094
|
-
*
|
|
1186
|
+
* Checks whether a caught value is an `AbortError` — the {@link import('./PromptClient.js').PromptClient}
|
|
1095
1187
|
* distinguishes a deliberate `disconnect` / teardown (an aborted `fetch`) from a real fault, so it
|
|
1096
1188
|
* exits its connect loop quietly instead of emitting `error` / reconnecting.
|
|
1189
|
+
*
|
|
1190
|
+
* @param error - The caught value to test
|
|
1191
|
+
* @returns True if the value is a host `Error` or `DOMException` named `AbortError`; false otherwise
|
|
1097
1192
|
*/
|
|
1098
1193
|
function isAbortError(error) {
|
|
1099
1194
|
return (error instanceof DOMException || error instanceof Error) && error.name === "AbortError";
|
|
1100
1195
|
}
|
|
1101
1196
|
/**
|
|
1102
|
-
*
|
|
1197
|
+
* Checks whether `url` is an INSECURE remote endpoint — a plain `http://` URL whose host is NOT a
|
|
1103
1198
|
* loopback address. Pure string parsing (no `URL` global), so it stays total on malformed input.
|
|
1104
1199
|
*
|
|
1105
1200
|
* @remarks
|
|
@@ -1108,7 +1203,7 @@ function isAbortError(error) {
|
|
|
1108
1203
|
* An `https://` URL (or any non-`http://` scheme) is never flagged.
|
|
1109
1204
|
*
|
|
1110
1205
|
* @param url - The candidate endpoint URL
|
|
1111
|
-
* @returns
|
|
1206
|
+
* @returns True if `url` is a non-loopback `http://` endpoint; false otherwise
|
|
1112
1207
|
*
|
|
1113
1208
|
* @example
|
|
1114
1209
|
* ```ts
|
|
@@ -1126,7 +1221,12 @@ function isInsecureRemote(url) {
|
|
|
1126
1221
|
const hostname = host.startsWith("[") ? host.slice(0, host.indexOf("]") + 1) : host.split(":")[0] ?? "";
|
|
1127
1222
|
return hostname !== "localhost" && hostname !== "127.0.0.1" && hostname !== "[::1]";
|
|
1128
1223
|
}
|
|
1129
|
-
/**
|
|
1224
|
+
/**
|
|
1225
|
+
* Serializes a parked {@link PendingForm} into a {@link WireEvent}.
|
|
1226
|
+
*
|
|
1227
|
+
* @param form - The parked form's wire-safe record
|
|
1228
|
+
* @returns The `pending` frame — the JSON-stringified record as `data`, and the form's own `id`
|
|
1229
|
+
*/
|
|
1130
1230
|
function serializePending(form) {
|
|
1131
1231
|
return {
|
|
1132
1232
|
event: "pending",
|
|
@@ -1134,24 +1234,33 @@ function serializePending(form) {
|
|
|
1134
1234
|
id: form.id
|
|
1135
1235
|
};
|
|
1136
1236
|
}
|
|
1137
|
-
/**
|
|
1237
|
+
/**
|
|
1238
|
+
* Serializes a parked prompt's expiry or release into a {@link WireEvent}.
|
|
1239
|
+
*
|
|
1240
|
+
* @param id - The id of the parked form that expired or was released
|
|
1241
|
+
* @returns The `expire` frame, carrying the JSON-stringified `{ id }` payload as `data`
|
|
1242
|
+
*/
|
|
1138
1243
|
function serializeExpire(id) {
|
|
1139
1244
|
return {
|
|
1140
1245
|
event: "expire",
|
|
1141
1246
|
data: JSON.stringify({ id })
|
|
1142
1247
|
};
|
|
1143
1248
|
}
|
|
1144
|
-
/**
|
|
1145
|
-
|
|
1249
|
+
/**
|
|
1250
|
+
* Serializes the {@link WireEvent} a broker or manager sends when it is going away.
|
|
1251
|
+
*
|
|
1252
|
+
* @returns The `destroy` frame, whose `data` is empty because the signal carries no payload
|
|
1253
|
+
*/
|
|
1254
|
+
function serializeDestroy() {
|
|
1146
1255
|
return {
|
|
1147
|
-
event: "
|
|
1256
|
+
event: "destroy",
|
|
1148
1257
|
data: ""
|
|
1149
1258
|
};
|
|
1150
1259
|
}
|
|
1151
1260
|
//#endregion
|
|
1152
1261
|
//#region src/core/Prompt.ts
|
|
1153
1262
|
/**
|
|
1154
|
-
*
|
|
1263
|
+
* Implements the headless form broker. It parks live forms, exposes their serialized schemas, and applies
|
|
1155
1264
|
* remote answers to the authoritative form.
|
|
1156
1265
|
*
|
|
1157
1266
|
* @remarks
|
|
@@ -1342,7 +1451,7 @@ var Prompt = class {
|
|
|
1342
1451
|
//#endregion
|
|
1343
1452
|
//#region src/core/PromptClient.ts
|
|
1344
1453
|
/**
|
|
1345
|
-
*
|
|
1454
|
+
* Implements the SSE form bridge. It ingests serialized forms from a remote broker, renders them through a
|
|
1346
1455
|
* local terminal, and posts answers back without blocking the event stream.
|
|
1347
1456
|
*
|
|
1348
1457
|
* @remarks
|
|
@@ -1351,12 +1460,12 @@ var Prompt = class {
|
|
|
1351
1460
|
* {@link disconnect} deliberately stopped it.
|
|
1352
1461
|
* - **Ingest + render.** Each `pending` envelope passes through `isPendingForm`, Form's `parseForm`,
|
|
1353
1462
|
* and terminal's `sanitizeSchema`, then enters a serial render queue. The SSE reader never awaits
|
|
1354
|
-
* that queue, so `expire` and `
|
|
1463
|
+
* that queue, so `expire` and `destroy` remain live while a person is answering.
|
|
1355
1464
|
* - **Safe local form.** The rendering copy omits every wire `pattern`, because Form compiles a
|
|
1356
1465
|
* pattern during local evaluation. The broker's parked form retains it and remains authoritative.
|
|
1357
1466
|
* - **Refusal retry.** A structured `rejected` response seeds a new rendering form with the values
|
|
1358
1467
|
* just submitted, applies every {@link FieldError} through `invalidate`, and asks again. No retry
|
|
1359
|
-
* counter truncates the loop; acceptance, expiry, and
|
|
1468
|
+
* counter truncates the loop; acceptance, expiry, and the broker's own teardown are its bounds.
|
|
1360
1469
|
* - **Replay safety.** A replayed id is skipped while it is queued, rendering, or posting. Once an
|
|
1361
1470
|
* attempt ends, a later delivery of that id may be rendered again.
|
|
1362
1471
|
*
|
|
@@ -1494,7 +1603,7 @@ var PromptClient = class {
|
|
|
1494
1603
|
if (isRecord(parsed) && isString(parsed.id)) this.#expire(parsed.id);
|
|
1495
1604
|
return;
|
|
1496
1605
|
}
|
|
1497
|
-
if (event.event === SSE_EVENTS.
|
|
1606
|
+
if (event.event === SSE_EVENTS.destroy) {
|
|
1498
1607
|
this.disconnect();
|
|
1499
1608
|
this.#interrupt();
|
|
1500
1609
|
}
|
|
@@ -1647,10 +1756,9 @@ var PromptClient = class {
|
|
|
1647
1756
|
//#endregion
|
|
1648
1757
|
//#region src/core/TerminalManager.ts
|
|
1649
1758
|
/**
|
|
1650
|
-
*
|
|
1651
|
-
*
|
|
1652
|
-
*
|
|
1653
|
-
* asks.
|
|
1759
|
+
* Registers named {@link PromptInterface} brokers (one per endpoint), so several parties can
|
|
1760
|
+
* `ask` forms of each other by NAME with a `from` → `to` attribution edge on every parked form,
|
|
1761
|
+
* and a transitive DEADLOCK check across all in-flight asks.
|
|
1654
1762
|
*
|
|
1655
1763
|
* @remarks
|
|
1656
1764
|
* - **Registry.** `add(name, options?)` mints (or, if `name` is already mounted, returns the
|
|
@@ -1665,7 +1773,7 @@ var PromptClient = class {
|
|
|
1665
1773
|
* - **Durable open / save.** `open(name)` restores an EMPTY broker from the `store` (parked
|
|
1666
1774
|
* Promises are process-bound and never resurrected); `save(name)` persists the endpoint's
|
|
1667
1775
|
* configured `timeout`.
|
|
1668
|
-
* - **Removal.** `remove` drops one endpoint, a batch (
|
|
1776
|
+
* - **Removal.** `remove` drops one endpoint, a batch (the array overload declared FIRST), or every
|
|
1669
1777
|
* endpoint when called without an argument. It destroys each broker, which expires every form
|
|
1670
1778
|
* still parked on it. `destroy` is idempotent.
|
|
1671
1779
|
*
|
|
@@ -1710,7 +1818,7 @@ var TerminalManager = class {
|
|
|
1710
1818
|
return this.#terminals.get(name);
|
|
1711
1819
|
}
|
|
1712
1820
|
terminals() {
|
|
1713
|
-
return [...this.#terminals.
|
|
1821
|
+
return [...this.#terminals.values()];
|
|
1714
1822
|
}
|
|
1715
1823
|
add(name, options) {
|
|
1716
1824
|
if (this.#destroyed) throw new TerminalError("DESTROYED", "manager destroyed");
|
|
@@ -1742,7 +1850,7 @@ var TerminalManager = class {
|
|
|
1742
1850
|
ask(from, to, form) {
|
|
1743
1851
|
const broker = this.#terminals.get(to);
|
|
1744
1852
|
if (broker === void 0) {
|
|
1745
|
-
const known = this.
|
|
1853
|
+
const known = [...this.#terminals.keys()];
|
|
1746
1854
|
return Promise.reject(new TerminalError("TARGET", `unknown terminal '${to}' (known: ${known.length > 0 ? known.join(", ") : "none"})`, {
|
|
1747
1855
|
to,
|
|
1748
1856
|
known
|
|
@@ -1780,7 +1888,7 @@ var TerminalManager = class {
|
|
|
1780
1888
|
const broker = this.#terminals.get(to);
|
|
1781
1889
|
if (broker === void 0) return {
|
|
1782
1890
|
success: false,
|
|
1783
|
-
error: { reason: "
|
|
1891
|
+
error: { reason: "target" }
|
|
1784
1892
|
};
|
|
1785
1893
|
const result = broker.answer(id, values);
|
|
1786
1894
|
if (result.success) this.#edges.delete(id);
|
|
@@ -1813,8 +1921,8 @@ var TerminalManager = class {
|
|
|
1813
1921
|
return;
|
|
1814
1922
|
}
|
|
1815
1923
|
if (isArray(names)) {
|
|
1816
|
-
let removed =
|
|
1817
|
-
for (const name of names) if (this.#removeOne(name)) removed =
|
|
1924
|
+
let removed = true;
|
|
1925
|
+
for (const name of names) if (!this.#removeOne(name)) removed = false;
|
|
1818
1926
|
return removed;
|
|
1819
1927
|
}
|
|
1820
1928
|
return this.#removeOne(names);
|
|
@@ -1889,16 +1997,16 @@ var TerminalManager = class {
|
|
|
1889
1997
|
}
|
|
1890
1998
|
};
|
|
1891
1999
|
//#endregion
|
|
1892
|
-
//#region src/core/MemoryTerminalStore.ts
|
|
2000
|
+
//#region src/core/stores/MemoryTerminalStore.ts
|
|
1893
2001
|
/**
|
|
1894
|
-
*
|
|
2002
|
+
* Implements the in-memory {@link TerminalStoreInterface} — a process-lifetime `Map` of
|
|
1895
2003
|
* {@link TerminalSnapshot}s keyed by endpoint id, the DEFAULT store
|
|
1896
|
-
* {@link import('
|
|
2004
|
+
* {@link import('../factories.js').createMemoryTerminalStore} builds. The EXACT twin of
|
|
1897
2005
|
* {@link import('./DatabaseTerminalStore.js').DatabaseTerminalStore}.
|
|
1898
2006
|
*
|
|
1899
2007
|
* @remarks
|
|
1900
|
-
* A plain `Map<string, TerminalSnapshot>`
|
|
1901
|
-
*
|
|
2008
|
+
* A plain `Map<string, TerminalSnapshot>` — the snapshot is already pure, self-contained CONFIG-only
|
|
2009
|
+
* JSON, so the memory tier needs no encoding. There is NO idle-TTL and NO
|
|
1902
2010
|
* eviction: a persisted config lives until an explicit `delete`. A durable backend (JSON / SQLite /
|
|
1903
2011
|
* IndexedDB) swaps in through the SAME interface without touching the manager — its
|
|
1904
2012
|
* driver-pluggable twin is {@link import('./DatabaseTerminalStore.js').DatabaseTerminalStore} (the
|
|
@@ -1908,13 +2016,13 @@ var TerminalManager = class {
|
|
|
1908
2016
|
* - **`set` inserts / replaces under the snapshot's OWN `id`** (no separate id param).
|
|
1909
2017
|
* - **`delete` drops a snapshot by id**; an absent id is a no-op (no throw).
|
|
1910
2018
|
*
|
|
1911
|
-
* The public surface is EXACTLY `get` / `set` / `delete` — no extra members
|
|
1912
|
-
*
|
|
2019
|
+
* The public surface is EXACTLY `get` / `set` / `delete` — no extra members, so the class and
|
|
2020
|
+
* {@link TerminalStoreInterface} carry the same methods. Hydration is a caller concern: `open` always
|
|
1913
2021
|
* restores an EMPTY broker — parked Promises are process-bound and never resurrected.
|
|
1914
2022
|
*
|
|
1915
2023
|
* @example
|
|
1916
2024
|
* ```ts
|
|
1917
|
-
* import { createMemoryTerminalStore } from '@
|
|
2025
|
+
* import { createMemoryTerminalStore } from '@orkestrel/terminal'
|
|
1918
2026
|
*
|
|
1919
2027
|
* const store = createMemoryTerminalStore()
|
|
1920
2028
|
* await store.set({ id: 'shell', timeout: 5000 }) // persist a config
|
|
@@ -1924,22 +2032,40 @@ var TerminalManager = class {
|
|
|
1924
2032
|
*/
|
|
1925
2033
|
var MemoryTerminalStore = class {
|
|
1926
2034
|
#snapshots = /* @__PURE__ */ new Map();
|
|
2035
|
+
/**
|
|
2036
|
+
* Resolves the persisted snapshot for `id`.
|
|
2037
|
+
*
|
|
2038
|
+
* @param id - The endpoint name the snapshot is keyed by
|
|
2039
|
+
* @returns The stored snapshot, or `undefined` when none is held
|
|
2040
|
+
*/
|
|
1927
2041
|
get(id) {
|
|
1928
2042
|
return Promise.resolve(this.#snapshots.get(id));
|
|
1929
2043
|
}
|
|
2044
|
+
/**
|
|
2045
|
+
* Inserts or replaces under the snapshot's OWN `id` (no separate id param).
|
|
2046
|
+
*
|
|
2047
|
+
* @param snapshot - The config snapshot to persist, carrying its own `id`
|
|
2048
|
+
* @returns A promise that settles once the snapshot is held
|
|
2049
|
+
*/
|
|
1930
2050
|
set(snapshot) {
|
|
1931
2051
|
this.#snapshots.set(snapshot.id, snapshot);
|
|
1932
2052
|
return Promise.resolve();
|
|
1933
2053
|
}
|
|
2054
|
+
/**
|
|
2055
|
+
* Drops a snapshot by id; an absent id is a no-op (no throw).
|
|
2056
|
+
*
|
|
2057
|
+
* @param id - The endpoint name to drop
|
|
2058
|
+
* @returns A promise that settles once the snapshot is gone
|
|
2059
|
+
*/
|
|
1934
2060
|
delete(id) {
|
|
1935
2061
|
this.#snapshots.delete(id);
|
|
1936
2062
|
return Promise.resolve();
|
|
1937
2063
|
}
|
|
1938
2064
|
};
|
|
1939
2065
|
//#endregion
|
|
1940
|
-
//#region src/core/DatabaseTerminalStore.ts
|
|
2066
|
+
//#region src/core/stores/DatabaseTerminalStore.ts
|
|
1941
2067
|
/**
|
|
1942
|
-
*
|
|
2068
|
+
* Implements a {@link TerminalStoreInterface} backed by one table of the `databases` layer — an endpoint's
|
|
1943
2069
|
* durable CONFIG state IS a row, so persistence reduces to keyed point-access (`get` / `set` /
|
|
1944
2070
|
* `delete`) over a {@link TableInterface}, the driver-pluggable twin of the plain-`Map`
|
|
1945
2071
|
* {@link import('./MemoryTerminalStore.js').MemoryTerminalStore}.
|
|
@@ -1949,7 +2075,7 @@ var MemoryTerminalStore = class {
|
|
|
1949
2075
|
* JSON, SQLite, IndexedDB) is chosen by whoever builds it (the factories), so a JSON / SQLite /
|
|
1950
2076
|
* IndexedDB backend swaps in WITHOUT touching the manager — the same seam as
|
|
1951
2077
|
* {@link import('./MemoryTerminalStore.js').MemoryTerminalStore}. The driver defaults to memory
|
|
1952
|
-
* ({@link import('
|
|
2078
|
+
* ({@link import('../factories.js').createDatabaseTerminalStore} passes `createMemoryDriver()`), so
|
|
1953
2079
|
* it ALSO works in memory out of the box; you opt into the durable plumbing by passing a JSON /
|
|
1954
2080
|
* SQLite / IndexedDB driver.
|
|
1955
2081
|
*
|
|
@@ -1961,18 +2087,19 @@ var MemoryTerminalStore = class {
|
|
|
1961
2087
|
* - **`set(snapshot)` upserts under the snapshot's OWN `id`** (no separate id param) — it writes
|
|
1962
2088
|
* the row `{ id: snapshot.id, snapshot }`.
|
|
1963
2089
|
* - **`get(id)` resolves the stored snapshot for an id**, narrowing the opaque JSON column back to
|
|
1964
|
-
* a {@link TerminalSnapshot} ({@link import('
|
|
1965
|
-
*
|
|
2090
|
+
* a {@link TerminalSnapshot} ({@link import('../validators.js').isTerminalSnapshot} — the total
|
|
2091
|
+
* guard that narrows an untrusted storage read), or `undefined` if none is stored.
|
|
1966
2092
|
* - **`delete(id)` drops a snapshot by id**; an absent id is a no-op (no throw).
|
|
1967
2093
|
*
|
|
1968
2094
|
* 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}
|
|
2095
|
+
* surface is EXACTLY `get` / `set` / `delete` — no extra members, so the class and
|
|
2096
|
+
* {@link TerminalStoreInterface} carry the same methods. Hydration stays a caller concern: `open` always restores an EMPTY
|
|
1971
2097
|
* broker — parked Promises are process-bound and never resurrected.
|
|
1972
2098
|
*
|
|
1973
2099
|
* @example
|
|
1974
2100
|
* ```ts
|
|
1975
|
-
* import { createDatabaseTerminalStore
|
|
2101
|
+
* import { createDatabaseTerminalStore } from '@orkestrel/terminal'
|
|
2102
|
+
* import { createMemoryDriver } from '@orkestrel/database'
|
|
1976
2103
|
*
|
|
1977
2104
|
* const store = createDatabaseTerminalStore(createMemoryDriver()) // a durable driver swaps in here
|
|
1978
2105
|
* await store.set({ id: 'shell', timeout: 5000 }) // persist the config (one JSON column)
|
|
@@ -1983,7 +2110,7 @@ var MemoryTerminalStore = class {
|
|
|
1983
2110
|
var DatabaseTerminalStore = class {
|
|
1984
2111
|
#table;
|
|
1985
2112
|
/**
|
|
1986
|
-
*
|
|
2113
|
+
* Wraps a table as a terminal store.
|
|
1987
2114
|
*
|
|
1988
2115
|
* @param table - The {@link TableInterface} holding the snapshots — its row is the
|
|
1989
2116
|
* {@link TerminalSnapshotRow} `{ id; snapshot }` shape (the snapshot one opaque JSON column)
|
|
@@ -1991,20 +2118,37 @@ var DatabaseTerminalStore = class {
|
|
|
1991
2118
|
constructor(table) {
|
|
1992
2119
|
this.#table = table;
|
|
1993
2120
|
}
|
|
1994
|
-
/**
|
|
2121
|
+
/**
|
|
2122
|
+
* Resolves the persisted snapshot for `id`, narrowing the opaque JSON column back to a
|
|
2123
|
+
* `TerminalSnapshot`.
|
|
2124
|
+
*
|
|
2125
|
+
* @param id - The endpoint name the row is keyed by
|
|
2126
|
+
* @returns The stored snapshot, or `undefined` when none is held or the column is off-shape
|
|
2127
|
+
*/
|
|
1995
2128
|
async get(id) {
|
|
1996
2129
|
const row = await this.#table.get(id);
|
|
1997
2130
|
if (row === void 0) return void 0;
|
|
1998
2131
|
return isTerminalSnapshot(row.snapshot) ? row.snapshot : void 0;
|
|
1999
2132
|
}
|
|
2000
|
-
/**
|
|
2133
|
+
/**
|
|
2134
|
+
* Inserts or replaces under the snapshot's OWN `id` (no separate id param) — the row is
|
|
2135
|
+
* `{ id, snapshot }`.
|
|
2136
|
+
*
|
|
2137
|
+
* @param snapshot - The config snapshot to persist, carrying its own `id`
|
|
2138
|
+
* @returns A promise that settles once the row is written
|
|
2139
|
+
*/
|
|
2001
2140
|
async set(snapshot) {
|
|
2002
2141
|
await this.#table.set({
|
|
2003
2142
|
id: snapshot.id,
|
|
2004
2143
|
snapshot
|
|
2005
2144
|
});
|
|
2006
2145
|
}
|
|
2007
|
-
/**
|
|
2146
|
+
/**
|
|
2147
|
+
* Drops a snapshot by id; an absent id is a no-op (no throw).
|
|
2148
|
+
*
|
|
2149
|
+
* @param id - The endpoint name to drop
|
|
2150
|
+
* @returns A promise that settles once the row is gone
|
|
2151
|
+
*/
|
|
2008
2152
|
async delete(id) {
|
|
2009
2153
|
await this.#table.remove(id);
|
|
2010
2154
|
}
|
|
@@ -2012,7 +2156,7 @@ var DatabaseTerminalStore = class {
|
|
|
2012
2156
|
//#endregion
|
|
2013
2157
|
//#region src/core/factories.ts
|
|
2014
2158
|
/**
|
|
2015
|
-
*
|
|
2159
|
+
* Creates the headless {@link PromptInterface} broker. It parks live forms and applies remote
|
|
2016
2160
|
* answers to the authoritative instances.
|
|
2017
2161
|
*
|
|
2018
2162
|
* @param options - See {@link PromptOptions}
|
|
@@ -2025,7 +2169,7 @@ var DatabaseTerminalStore = class {
|
|
|
2025
2169
|
*
|
|
2026
2170
|
* @example
|
|
2027
2171
|
* ```ts
|
|
2028
|
-
* import { createPrompt } from '@
|
|
2172
|
+
* import { createPrompt } from '@orkestrel/terminal'
|
|
2029
2173
|
* import { createForm } from '@orkestrel/form'
|
|
2030
2174
|
*
|
|
2031
2175
|
* const prompt = createPrompt()
|
|
@@ -2038,7 +2182,7 @@ function createPrompt(options) {
|
|
|
2038
2182
|
return new Prompt(options);
|
|
2039
2183
|
}
|
|
2040
2184
|
/**
|
|
2041
|
-
*
|
|
2185
|
+
* Creates the SSE prompt {@link PromptClientInterface} BRIDGE — it connects to a remote broker's SSE
|
|
2042
2186
|
* endpoint, dispatches each received form to a local {@link import('./types.js').TerminalInterface},
|
|
2043
2187
|
* and POSTs the answer back. Universal — `fetch` / SSE are web-standard.
|
|
2044
2188
|
*
|
|
@@ -2050,11 +2194,11 @@ function createPrompt(options) {
|
|
|
2050
2194
|
* ends; it reconnects with the `delay` backoff unless `reconnect` is `false` / the client was
|
|
2051
2195
|
* `destroy`ed. Inject `options.fetch` (a scripted `fetch`) and `options.timer` to drive it
|
|
2052
2196
|
* deterministically in tests — no real network.
|
|
2053
|
-
* -
|
|
2197
|
+
* - **Wire narrowing.** Every decoded prompt is guard-narrowed before dispatch (never an `as`).
|
|
2054
2198
|
*
|
|
2055
2199
|
* @example
|
|
2056
2200
|
* ```ts
|
|
2057
|
-
* import { createPromptClient } from '@
|
|
2201
|
+
* import { createPromptClient } from '@orkestrel/terminal'
|
|
2058
2202
|
*
|
|
2059
2203
|
* const client = createPromptClient({ url: 'http://host/prompts', terminal })
|
|
2060
2204
|
* await client.connect()
|
|
@@ -2064,7 +2208,7 @@ function createPromptClient(options) {
|
|
|
2064
2208
|
return new PromptClient(options);
|
|
2065
2209
|
}
|
|
2066
2210
|
/**
|
|
2067
|
-
*
|
|
2211
|
+
* Creates the multi-endpoint {@link TerminalManager} — a named registry of
|
|
2068
2212
|
* {@link PromptInterface} brokers so several parties can `ask` prompts of each other by name,
|
|
2069
2213
|
* with a transitive DEADLOCK check across every in-flight ask.
|
|
2070
2214
|
*
|
|
@@ -2073,7 +2217,7 @@ function createPromptClient(options) {
|
|
|
2073
2217
|
*
|
|
2074
2218
|
* @example
|
|
2075
2219
|
* ```ts
|
|
2076
|
-
* import { createTerminalManager } from '@
|
|
2220
|
+
* import { createTerminalManager } from '@orkestrel/terminal'
|
|
2077
2221
|
*
|
|
2078
2222
|
* const manager = createTerminalManager()
|
|
2079
2223
|
* manager.add('agent')
|
|
@@ -2083,14 +2227,14 @@ function createTerminalManager(options) {
|
|
|
2083
2227
|
return new TerminalManager(options);
|
|
2084
2228
|
}
|
|
2085
2229
|
/**
|
|
2086
|
-
*
|
|
2230
|
+
* Creates the in-memory {@link TerminalStoreInterface} — a process-lifetime `Map` of endpoint
|
|
2087
2231
|
* config snapshots, the default store backing a {@link TerminalManagerInterface}'s `open` / `save`.
|
|
2088
2232
|
*
|
|
2089
2233
|
* @returns A {@link TerminalStoreInterface}
|
|
2090
2234
|
*
|
|
2091
2235
|
* @example
|
|
2092
2236
|
* ```ts
|
|
2093
|
-
* import { createMemoryTerminalStore } from '@
|
|
2237
|
+
* import { createMemoryTerminalStore } from '@orkestrel/terminal'
|
|
2094
2238
|
*
|
|
2095
2239
|
* const store = createMemoryTerminalStore()
|
|
2096
2240
|
* ```
|
|
@@ -2099,7 +2243,7 @@ function createMemoryTerminalStore() {
|
|
|
2099
2243
|
return new MemoryTerminalStore();
|
|
2100
2244
|
}
|
|
2101
2245
|
/**
|
|
2102
|
-
*
|
|
2246
|
+
* Creates a {@link TerminalStoreInterface} backed by one table of the `databases` layer — the
|
|
2103
2247
|
* driver-pluggable twin of {@link createMemoryTerminalStore}, storing each endpoint's config
|
|
2104
2248
|
* snapshot as one opaque JSON column.
|
|
2105
2249
|
*
|
|
@@ -2108,7 +2252,7 @@ function createMemoryTerminalStore() {
|
|
|
2108
2252
|
*
|
|
2109
2253
|
* @example
|
|
2110
2254
|
* ```ts
|
|
2111
|
-
* import { createDatabaseTerminalStore } from '@
|
|
2255
|
+
* import { createDatabaseTerminalStore } from '@orkestrel/terminal'
|
|
2112
2256
|
*
|
|
2113
2257
|
* const store = createDatabaseTerminalStore() // in-memory by default
|
|
2114
2258
|
* ```
|
|
@@ -2124,6 +2268,6 @@ function createDatabaseTerminalStore(driver = createMemoryDriver()) {
|
|
|
2124
2268
|
}).table("terminals"));
|
|
2125
2269
|
}
|
|
2126
2270
|
//#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,
|
|
2271
|
+
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
2272
|
|
|
2129
2273
|
//# sourceMappingURL=index.js.map
|