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