@orkestrel/terminal 0.0.12 → 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 +408 -258
- package/dist/src/core/index.cjs.map +1 -1
- package/dist/src/core/index.d.cts +544 -297
- package/dist/src/core/index.d.ts +544 -297
- package/dist/src/core/index.js +393 -241
- 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 +23 -19
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,32 +48,34 @@ 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
|
-
*
|
|
73
|
-
* consults for the one-byte keys
|
|
74
|
-
* `ctrl` combination. The source of truth for
|
|
68
|
+
* Holds the control BYTE (or CRLF pair) → key descriptor table {@link import('./helpers.js').parseKey}
|
|
69
|
+
* consults for the one-byte keys and the two-byte CRLF Enter chunk. Each entry carries the
|
|
70
|
+
* canonical `name` and whether it is a `ctrl` combination. The source of truth for that decode;
|
|
71
|
+
* frozen.
|
|
75
72
|
*
|
|
76
73
|
* @remarks
|
|
77
|
-
* `return` / `newline` map to `return` (one canonical Enter name)
|
|
78
|
-
*
|
|
79
|
-
*
|
|
80
|
-
* `
|
|
74
|
+
* `return` / `newline` / `return + newline` all map to `return` (one canonical Enter name) — a
|
|
75
|
+
* terminal or a paste can deliver Enter as `\r`, `\n`, or the `\r\n` pair in one chunk;
|
|
76
|
+
* `delete` / `backspace` both map to `backspace` (the Backspace bytes); the Ctrl combos
|
|
77
|
+
* (`c` / `d` / `u` / `a` / `e`) carry `ctrl: true` so a reducer can match
|
|
78
|
+
* `key.ctrl && key.name === 'c'`. `escape` / `tab` / `space` are plain named keys.
|
|
81
79
|
*/
|
|
82
80
|
var CONTROL_NAMES = Object.freeze({
|
|
83
81
|
["\r"]: Object.freeze({
|
|
@@ -88,11 +86,16 @@ var CONTROL_NAMES = Object.freeze({
|
|
|
88
86
|
name: "return",
|
|
89
87
|
ctrl: false
|
|
90
88
|
}),
|
|
89
|
+
[`\r
|
|
90
|
+
`]: Object.freeze({
|
|
91
|
+
name: "return",
|
|
92
|
+
ctrl: false
|
|
93
|
+
}),
|
|
91
94
|
[" "]: Object.freeze({
|
|
92
95
|
name: "tab",
|
|
93
96
|
ctrl: false
|
|
94
97
|
}),
|
|
95
|
-
[
|
|
98
|
+
[_orkestrel_console.ESC]: Object.freeze({
|
|
96
99
|
name: "escape",
|
|
97
100
|
ctrl: false
|
|
98
101
|
}),
|
|
@@ -129,10 +132,10 @@ var CONTROL_NAMES = Object.freeze({
|
|
|
129
132
|
ctrl: true
|
|
130
133
|
})
|
|
131
134
|
});
|
|
132
|
-
/**
|
|
135
|
+
/** Names the default mask glyph {@link import('./helpers.js').createPasswordState} uses — `*`. */
|
|
133
136
|
var DEFAULT_MASK = "*";
|
|
134
137
|
/**
|
|
135
|
-
*
|
|
138
|
+
* Holds the default glyphs {@link DEFAULT_PROMPT_THEME} assembles its `icons` from. Read only when
|
|
136
139
|
* the default theme is assembled; a view reads its resolved theme and never this constant. Frozen.
|
|
137
140
|
*
|
|
138
141
|
* @remarks
|
|
@@ -150,7 +153,7 @@ var PROMPT_ICONS = Object.freeze({
|
|
|
150
153
|
unchecked: "☐"
|
|
151
154
|
});
|
|
152
155
|
/**
|
|
153
|
-
*
|
|
156
|
+
* Holds every {@link import('./types.js').PromptRole}, in one frozen list — the role axis's source of
|
|
154
157
|
* truth. {@link import('./helpers.js').createPromptTheme} walks it to merge a partial theme, and
|
|
155
158
|
* a consumer building a complete role map reads it rather than retyping every name.
|
|
156
159
|
*/
|
|
@@ -168,7 +171,7 @@ var PROMPT_ROLES = Object.freeze([
|
|
|
168
171
|
"description"
|
|
169
172
|
]);
|
|
170
173
|
/**
|
|
171
|
-
*
|
|
174
|
+
* Holds the {@link import('./types.js').PromptTheme} every prompt renders with unless its options supply
|
|
172
175
|
* another — the glyph set assembled from {@link PROMPT_ICONS} plus the console
|
|
173
176
|
* {@link import('@orkestrel/console').STATUS_ICONS} `success` / `error` marks, and the console
|
|
174
177
|
* {@link import('@orkestrel/console').Style} each role is painted with. Deeply frozen through the
|
|
@@ -222,30 +225,30 @@ var DEFAULT_PROMPT_THEME = Object.freeze({
|
|
|
222
225
|
description: (0, _orkestrel_console.freezeStyle)({ attributes: ["dim"] })
|
|
223
226
|
})
|
|
224
227
|
});
|
|
225
|
-
/**
|
|
228
|
+
/** Holds how long (ms) the {@link import('./types.js').PromptInterface} broker parks an unanswered prompt before it expires — 5 minutes. */
|
|
226
229
|
var DEFAULT_PROMPT_TIMEOUT_MS = 3e5;
|
|
227
|
-
/**
|
|
230
|
+
/** Holds how long (ms) the {@link import('./types.js').PromptClientInterface} waits before each reconnect attempt — 2 seconds. */
|
|
228
231
|
var DEFAULT_RECONNECT_DELAY_MS = 2e3;
|
|
229
232
|
/**
|
|
230
|
-
*
|
|
233
|
+
* Holds the SSE `event:` names the broker emits and the {@link import('./types.js').PromptClientInterface}
|
|
231
234
|
* dispatches on. Frozen; the source of truth for the wire event vocabulary.
|
|
232
235
|
*
|
|
233
236
|
* @remarks
|
|
234
237
|
* - `pending` — a serialized {@link import('./types.js').PendingForm} to dispatch and answer.
|
|
235
238
|
* - `expire` — an `{ id }` payload: the broker expired or released a parked prompt (the client drops it).
|
|
236
|
-
* - `
|
|
239
|
+
* - `destroy` — the broker is going away; the client disconnects (no auto-reconnect) but stays reusable.
|
|
237
240
|
*/
|
|
238
241
|
var SSE_EVENTS = Object.freeze({
|
|
239
242
|
pending: "pending",
|
|
240
243
|
expire: "expire",
|
|
241
|
-
|
|
244
|
+
destroy: "destroy"
|
|
242
245
|
});
|
|
243
|
-
/**
|
|
246
|
+
/** Names the auth-token request header the {@link import('./types.js').PromptClientInterface} sends. */
|
|
244
247
|
var HEADER_TOKEN = "x-orkestrel-token";
|
|
245
|
-
/**
|
|
248
|
+
/** Names the `Accept` header value that opens the broker's SSE stream. */
|
|
246
249
|
var ACCEPT_EVENT_STREAM = "text/event-stream";
|
|
247
250
|
/**
|
|
248
|
-
*
|
|
251
|
+
* Sets the maximum number of characters the {@link import('./types.js').PromptClientInterface} lets its
|
|
249
252
|
* SSE parser buffer before treating the stream as hostile — 1 MiB, comfortably above any
|
|
250
253
|
* legitimate prompt payload. Passed as the `limit` to `createSSEParser` so an unterminated
|
|
251
254
|
* or oversized `data:` field cannot grow the buffer without bound (a memory-exhaustion guard).
|
|
@@ -254,7 +257,7 @@ var SSE_BUFFER_LIMIT = 1048576;
|
|
|
254
257
|
//#endregion
|
|
255
258
|
//#region src/core/errors.ts
|
|
256
259
|
/**
|
|
257
|
-
*
|
|
260
|
+
* Represents the error the terminal surfaces for its own refusals: parking on a destroyed or full broker, an
|
|
258
261
|
* unusable driver stream, a manager routing fault, or a ctrl-c cancellation. A parked form's own
|
|
259
262
|
* lifecycle failures reject through the form's `answer` with the form package's error, never with
|
|
260
263
|
* this one.
|
|
@@ -265,10 +268,17 @@ var SSE_BUFFER_LIMIT = 1048576;
|
|
|
265
268
|
* a caught value with {@link isTerminalError} and branch on `error.code`.
|
|
266
269
|
*/
|
|
267
270
|
var TerminalError = class extends Error {
|
|
268
|
-
/**
|
|
271
|
+
/** Holds the machine-readable condition — see {@link TerminalErrorCode}. */
|
|
269
272
|
code;
|
|
270
|
-
/**
|
|
273
|
+
/** Holds an optional context bag naming the offending values — see the class {@link TerminalError remarks}. */
|
|
271
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
|
+
*/
|
|
272
282
|
constructor(code, message, context) {
|
|
273
283
|
super(message);
|
|
274
284
|
this.name = "TerminalError";
|
|
@@ -277,10 +287,10 @@ var TerminalError = class extends Error {
|
|
|
277
287
|
}
|
|
278
288
|
};
|
|
279
289
|
/**
|
|
280
|
-
*
|
|
290
|
+
* Narrows an unknown caught value to a {@link TerminalError}.
|
|
281
291
|
*
|
|
282
292
|
* @param value - The value to test (typically a `catch` binding or a rejected prompt call)
|
|
283
|
-
* @returns
|
|
293
|
+
* @returns True if `value` is a {@link TerminalError}; false otherwise
|
|
284
294
|
*
|
|
285
295
|
* @example
|
|
286
296
|
* ```ts
|
|
@@ -298,17 +308,22 @@ function isTerminalError(value) {
|
|
|
298
308
|
}
|
|
299
309
|
//#endregion
|
|
300
310
|
//#region src/core/validators.ts
|
|
301
|
-
/**
|
|
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
|
+
*/
|
|
302
317
|
var isPendingFormStatus = (0, _orkestrel_contract.literalOf)("pending", "answered", "expired");
|
|
303
318
|
/**
|
|
304
|
-
*
|
|
319
|
+
* Narrows an unknown wire value to a {@link PendingForm} envelope.
|
|
305
320
|
*
|
|
306
321
|
* @remarks
|
|
307
322
|
* This guard checks the transport record and proves only that `schema` is a record. The Form
|
|
308
323
|
* package's `parseForm` owns the schema payload and its semantic audit.
|
|
309
324
|
*
|
|
310
325
|
* @param value - The decoded wire value to inspect
|
|
311
|
-
* @returns
|
|
326
|
+
* @returns True if the value is a complete pending-form envelope; false otherwise
|
|
312
327
|
*/
|
|
313
328
|
function isPendingForm(value) {
|
|
314
329
|
return (0, _orkestrel_contract.recordOf)({
|
|
@@ -321,17 +336,23 @@ function isPendingForm(value) {
|
|
|
321
336
|
}, ["from", "to"])(value);
|
|
322
337
|
}
|
|
323
338
|
/**
|
|
324
|
-
*
|
|
339
|
+
* Narrows an unknown value to a transport-neutral {@link WireEvent}.
|
|
325
340
|
*
|
|
326
341
|
* @param value - The candidate wire event
|
|
327
|
-
* @returns
|
|
342
|
+
* @returns True if the value carries an event name, serialized data, and an optional id; false otherwise
|
|
328
343
|
*/
|
|
329
344
|
var isWireEvent = (0, _orkestrel_contract.recordOf)({
|
|
330
345
|
event: _orkestrel_contract.isString,
|
|
331
346
|
data: _orkestrel_contract.isString,
|
|
332
347
|
id: _orkestrel_contract.isString
|
|
333
348
|
}, ["id"]);
|
|
334
|
-
/**
|
|
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
|
+
*/
|
|
335
356
|
var isTerminalSnapshot = (0, _orkestrel_contract.recordOf)({
|
|
336
357
|
id: _orkestrel_contract.isNonEmptyString,
|
|
337
358
|
timeout: _orkestrel_contract.isNumber
|
|
@@ -339,24 +360,24 @@ var isTerminalSnapshot = (0, _orkestrel_contract.recordOf)({
|
|
|
339
360
|
//#endregion
|
|
340
361
|
//#region src/core/helpers.ts
|
|
341
362
|
/**
|
|
342
|
-
*
|
|
343
|
-
* read as UTF-8; the resulting string is matched against the known control bytes
|
|
344
|
-
* ({@link CONTROL_NAMES}) and escape sequences ({@link SEQUENCE_NAMES}), falling back to a
|
|
345
|
-
* single printable character. An unrecognized sequence
|
|
363
|
+
* Decodes one keypress's bytes into a {@link KeyEvent} — total, never throws. A `Uint8Array` is
|
|
364
|
+
* read as UTF-8; the resulting string is matched against the known control bytes and the CRLF
|
|
365
|
+
* pair ({@link CONTROL_NAMES}) and escape sequences ({@link SEQUENCE_NAMES}), falling back to a
|
|
366
|
+
* single printable character. An unrecognized sequence carries NO `name`, with the raw `sequence`
|
|
346
367
|
* preserved.
|
|
347
368
|
*
|
|
348
369
|
* @remarks
|
|
349
370
|
* - **Single control byte.** A one-character control input (`return` / `backspace` / `tab` /
|
|
350
|
-
* `escape` / `space`, or a Ctrl combo `c` / `d` / `u` / `a` / `e`)
|
|
351
|
-
* {@link CONTROL_NAMES}, carrying its `ctrl` flag.
|
|
371
|
+
* `escape` / `space`, or a Ctrl combo `c` / `d` / `u` / `a` / `e`), or the two-byte `\r\n`
|
|
372
|
+
* CRLF pair, is looked up in {@link CONTROL_NAMES}, carrying its `ctrl` flag.
|
|
352
373
|
* - **Escape sequence.** A multi-byte ESC sequence (`up` / `down` / `left` / `right` in BOTH the
|
|
353
374
|
* `ESC[A` and `ESCOA` forms, plus `home` / `end` / `delete`) is looked up in
|
|
354
375
|
* {@link SEQUENCE_NAMES} and flagged `meta`.
|
|
355
376
|
* - **Printable character.** A single printable character becomes `name` = that character, with
|
|
356
377
|
* `shift` set when it is an uppercase letter. A multi-code-point printable (an emoji, a pasted
|
|
357
378
|
* run) keeps its first code point as the name and the whole input as `sequence`.
|
|
358
|
-
* - **Unknown.** Anything else (an unrecognized escape, an empty input) yields
|
|
359
|
-
* 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.
|
|
360
381
|
*
|
|
361
382
|
* @param input - The raw keypress bytes, as a string or `Uint8Array`
|
|
362
383
|
* @returns The decoded {@link KeyEvent}
|
|
@@ -364,6 +385,7 @@ var isTerminalSnapshot = (0, _orkestrel_contract.recordOf)({
|
|
|
364
385
|
* @example
|
|
365
386
|
* ```ts
|
|
366
387
|
* parseKey('\r') // { name: 'return', sequence: '\r', ctrl: false, meta: false, shift: false }
|
|
388
|
+
* parseKey('\r\n') // { name: 'return', sequence: '\r\n', ctrl: false, meta: false, shift: false }
|
|
367
389
|
* parseKey('\x1b[A') // { name: 'up', sequence: '\x1b[A', ctrl: false, meta: true, shift: false }
|
|
368
390
|
* parseKey('A') // { name: 'A', sequence: 'A', ctrl: false, meta: false, shift: true }
|
|
369
391
|
* parseKey('\x03') // { name: 'c', sequence: '\x03', ctrl: true, meta: false, shift: false }
|
|
@@ -396,14 +418,19 @@ function parseKey(input) {
|
|
|
396
418
|
shift: first !== first.toLowerCase()
|
|
397
419
|
};
|
|
398
420
|
return {
|
|
399
|
-
name: "",
|
|
400
421
|
sequence,
|
|
401
422
|
ctrl: false,
|
|
402
423
|
meta: false,
|
|
403
424
|
shift: false
|
|
404
425
|
};
|
|
405
426
|
}
|
|
406
|
-
/**
|
|
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
|
+
*/
|
|
407
434
|
function isPrintable(character) {
|
|
408
435
|
if (character.length === 0) return false;
|
|
409
436
|
const code = character.codePointAt(0);
|
|
@@ -411,7 +438,7 @@ function isPrintable(character) {
|
|
|
411
438
|
return code >= 32 && code !== 127;
|
|
412
439
|
}
|
|
413
440
|
/**
|
|
414
|
-
*
|
|
441
|
+
* Builds a complete {@link PromptTheme} by merging a partial one over
|
|
415
442
|
* {@link DEFAULT_PROMPT_THEME}, leaf by leaf — each supplied icon replaces that glyph, each
|
|
416
443
|
* supplied role replaces that {@link Style}, and everything else keeps its default. Each supplied
|
|
417
444
|
* style is snapshotted through the console module's own
|
|
@@ -446,7 +473,7 @@ function createPromptTheme(options) {
|
|
|
446
473
|
});
|
|
447
474
|
}
|
|
448
475
|
/**
|
|
449
|
-
*
|
|
476
|
+
* Sanitizes text for one single-line display slot. Composes console's ANSI {@link strip} and C0
|
|
450
477
|
* {@link stripControls} passes with removal of tab, line feed, and carriage return.
|
|
451
478
|
*
|
|
452
479
|
* @param text - The text to sanitize for a glyph or hint slot
|
|
@@ -461,7 +488,7 @@ function sanitizeDisplayText(text) {
|
|
|
461
488
|
return (0, _orkestrel_console.stripControls)((0, _orkestrel_console.strip)(text)).replaceAll(" ", "").replaceAll("\n", "").replaceAll("\r", "");
|
|
462
489
|
}
|
|
463
490
|
/**
|
|
464
|
-
*
|
|
491
|
+
* Sanitizes every terminal-readable string in a parsed form schema.
|
|
465
492
|
*
|
|
466
493
|
* @remarks
|
|
467
494
|
* Display strings pass through {@link sanitizeDisplayText}: labels, help, placeholders, masks,
|
|
@@ -503,6 +530,7 @@ function sanitizeSchema(schema) {
|
|
|
503
530
|
switch (field.control) {
|
|
504
531
|
case "text":
|
|
505
532
|
case "editor":
|
|
533
|
+
case "number":
|
|
506
534
|
fields.push({
|
|
507
535
|
...field,
|
|
508
536
|
...shared,
|
|
@@ -516,22 +544,10 @@ function sanitizeSchema(schema) {
|
|
|
516
544
|
...field.mask !== void 0 ? { mask: sanitizeDisplayText(field.mask) } : {}
|
|
517
545
|
});
|
|
518
546
|
break;
|
|
519
|
-
case "number":
|
|
520
|
-
fields.push({
|
|
521
|
-
...field,
|
|
522
|
-
...shared,
|
|
523
|
-
...field.placeholder !== void 0 ? { placeholder: sanitizeDisplayText(field.placeholder) } : {}
|
|
524
|
-
});
|
|
525
|
-
break;
|
|
526
547
|
case "date":
|
|
527
548
|
case "time":
|
|
528
549
|
case "datetime":
|
|
529
550
|
case "color":
|
|
530
|
-
fields.push({
|
|
531
|
-
...field,
|
|
532
|
-
...shared
|
|
533
|
-
});
|
|
534
|
-
break;
|
|
535
551
|
case "confirm":
|
|
536
552
|
fields.push({
|
|
537
553
|
...field,
|
|
@@ -539,17 +555,6 @@ function sanitizeSchema(schema) {
|
|
|
539
555
|
});
|
|
540
556
|
break;
|
|
541
557
|
case "select":
|
|
542
|
-
fields.push({
|
|
543
|
-
...field,
|
|
544
|
-
...shared,
|
|
545
|
-
choices: field.choices.map((choice) => ({
|
|
546
|
-
...choice,
|
|
547
|
-
value: choice.value,
|
|
548
|
-
label: sanitizeDisplayText(choice.label),
|
|
549
|
-
...choice.help !== void 0 ? { help: sanitizeDisplayText(choice.help) } : {}
|
|
550
|
-
}))
|
|
551
|
-
});
|
|
552
|
-
break;
|
|
553
558
|
case "checkbox":
|
|
554
559
|
fields.push({
|
|
555
560
|
...field,
|
|
@@ -578,7 +583,7 @@ function sanitizeSchema(schema) {
|
|
|
578
583
|
};
|
|
579
584
|
}
|
|
580
585
|
/**
|
|
581
|
-
*
|
|
586
|
+
* Sanitizes every glyph a wire-supplied {@link PromptThemeOptions} carries for a single-line display
|
|
582
587
|
* slot. Only the icons need it: a role is guard-narrowed to a console {@link Style}, whose colors
|
|
583
588
|
* and attributes are fixed name sets, so no role can carry a byte a terminal would act on.
|
|
584
589
|
*
|
|
@@ -595,13 +600,21 @@ function sanitizeThemeIcons(theme) {
|
|
|
595
600
|
icons: sanitized
|
|
596
601
|
};
|
|
597
602
|
}
|
|
598
|
-
/**
|
|
599
|
-
|
|
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) {
|
|
600
613
|
return `${styler.render(theme.roles.question, theme.icons.question)} ${styler.render(theme.roles.message, message)}`;
|
|
601
614
|
}
|
|
602
615
|
/**
|
|
603
|
-
*
|
|
604
|
-
* 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.
|
|
605
618
|
*
|
|
606
619
|
* @param styler - The console styler that renders each role
|
|
607
620
|
* @param theme - The resolved prompt theme
|
|
@@ -609,20 +622,35 @@ function promptHeader(styler, theme, message) {
|
|
|
609
622
|
* @param hint - The optional key hint to append
|
|
610
623
|
* @returns The rendered header with the optional hint
|
|
611
624
|
*/
|
|
612
|
-
function
|
|
613
|
-
const head =
|
|
625
|
+
function renderHintedHeader(styler, theme, message, hint) {
|
|
626
|
+
const head = renderPromptHeader(styler, theme, message);
|
|
614
627
|
return hint === void 0 ? head : `${head} ${styler.render(theme.roles.hint, hint)}`;
|
|
615
628
|
}
|
|
616
|
-
/**
|
|
617
|
-
|
|
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) {
|
|
618
639
|
return `${styler.render(theme.roles.success, theme.icons.success)} ${styler.render(theme.roles.message, message)}`;
|
|
619
640
|
}
|
|
620
|
-
/**
|
|
621
|
-
|
|
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) {
|
|
622
650
|
return `${styler.render(theme.roles.error, theme.icons.error)} ${styler.render(theme.roles.error, message)}`;
|
|
623
651
|
}
|
|
624
652
|
/**
|
|
625
|
-
*
|
|
653
|
+
* Builds the initial text-field key state.
|
|
626
654
|
*
|
|
627
655
|
* @param field - The text field to render
|
|
628
656
|
* @param styler - The styler used to render the view
|
|
@@ -638,22 +666,31 @@ function createInputState(field, styler = (0, _orkestrel_console.createStyler)()
|
|
|
638
666
|
value: ""
|
|
639
667
|
};
|
|
640
668
|
}
|
|
641
|
-
/**
|
|
642
|
-
|
|
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) {
|
|
643
676
|
const content = state.value.length > 0 ? state.value : state.default;
|
|
644
677
|
const role = state.value.length > 0 ? state.theme.roles.content : state.theme.roles.hint;
|
|
645
678
|
const shown = state.styler.render(role, sanitizeDisplayText(content));
|
|
646
|
-
return `${
|
|
679
|
+
return `${renderPromptHeader(state.styler, state.theme, state.message)} ${state.styler.render(state.theme.roles.pointer, state.theme.icons.pointer)} ${shown}`;
|
|
647
680
|
}
|
|
648
681
|
/**
|
|
649
|
-
*
|
|
682
|
+
* Advances an input prompt by one {@link KeyEvent} — the pure `(state, key) → PromptStep<string>`
|
|
650
683
|
* reducer. Printable characters extend the value; backspace shrinks it; ctrl-u clears it; ctrl-c
|
|
651
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
|
|
652
689
|
*/
|
|
653
|
-
function
|
|
690
|
+
function reduceInput(state, key) {
|
|
654
691
|
if (key.ctrl && key.name === "c") return {
|
|
655
692
|
state,
|
|
656
|
-
view:
|
|
693
|
+
view: renderInputView(state),
|
|
657
694
|
status: "cancel"
|
|
658
695
|
};
|
|
659
696
|
if (key.name === "return") {
|
|
@@ -663,7 +700,7 @@ function inputReduce(state, key) {
|
|
|
663
700
|
...state,
|
|
664
701
|
value: answer
|
|
665
702
|
},
|
|
666
|
-
view: `${
|
|
703
|
+
view: `${renderSubmitHeader(state.styler, state.theme, state.message)} ${state.styler.render(state.theme.roles.hint, sanitizeDisplayText(answer))}`,
|
|
667
704
|
status: "submit",
|
|
668
705
|
value: answer
|
|
669
706
|
};
|
|
@@ -671,7 +708,7 @@ function inputReduce(state, key) {
|
|
|
671
708
|
const value = editLine(state.value, key);
|
|
672
709
|
if (value === void 0) return {
|
|
673
710
|
state,
|
|
674
|
-
view:
|
|
711
|
+
view: renderInputView(state),
|
|
675
712
|
status: "active"
|
|
676
713
|
};
|
|
677
714
|
const next = {
|
|
@@ -680,12 +717,12 @@ function inputReduce(state, key) {
|
|
|
680
717
|
};
|
|
681
718
|
return {
|
|
682
719
|
state: next,
|
|
683
|
-
view:
|
|
720
|
+
view: renderInputView(next),
|
|
684
721
|
status: "active"
|
|
685
722
|
};
|
|
686
723
|
}
|
|
687
724
|
/**
|
|
688
|
-
*
|
|
725
|
+
* Builds the initial password-field key state.
|
|
689
726
|
*
|
|
690
727
|
* @param field - The password field to render
|
|
691
728
|
* @param styler - The styler used to render the view
|
|
@@ -701,32 +738,41 @@ function createPasswordState(field, styler = (0, _orkestrel_console.createStyler
|
|
|
701
738
|
value: ""
|
|
702
739
|
};
|
|
703
740
|
}
|
|
704
|
-
/**
|
|
705
|
-
|
|
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) {
|
|
706
748
|
const masked = state.styler.render(state.theme.roles.content, state.mask.repeat(state.value.length));
|
|
707
|
-
return `${
|
|
749
|
+
return `${renderPromptHeader(state.styler, state.theme, state.message)} ${state.styler.render(state.theme.roles.pointer, state.theme.icons.pointer)} ${masked}`;
|
|
708
750
|
}
|
|
709
751
|
/**
|
|
710
|
-
*
|
|
711
|
-
* 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,
|
|
712
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
|
|
713
759
|
*/
|
|
714
|
-
function
|
|
760
|
+
function reducePassword(state, key) {
|
|
715
761
|
if (key.ctrl && key.name === "c") return {
|
|
716
762
|
state,
|
|
717
|
-
view:
|
|
763
|
+
view: renderPasswordView(state),
|
|
718
764
|
status: "cancel"
|
|
719
765
|
};
|
|
720
766
|
if (key.name === "return") return {
|
|
721
767
|
state,
|
|
722
|
-
view: `${
|
|
768
|
+
view: `${renderSubmitHeader(state.styler, state.theme, state.message)} ${state.styler.render(state.theme.roles.hint, state.mask.repeat(state.value.length))}`,
|
|
723
769
|
status: "submit",
|
|
724
770
|
value: state.value
|
|
725
771
|
};
|
|
726
772
|
const value = editLine(state.value, key);
|
|
727
773
|
if (value === void 0) return {
|
|
728
774
|
state,
|
|
729
|
-
view:
|
|
775
|
+
view: renderPasswordView(state),
|
|
730
776
|
status: "active"
|
|
731
777
|
};
|
|
732
778
|
const next = {
|
|
@@ -735,12 +781,12 @@ function passwordReduce(state, key) {
|
|
|
735
781
|
};
|
|
736
782
|
return {
|
|
737
783
|
state: next,
|
|
738
|
-
view:
|
|
784
|
+
view: renderPasswordView(next),
|
|
739
785
|
status: "active"
|
|
740
786
|
};
|
|
741
787
|
}
|
|
742
788
|
/**
|
|
743
|
-
*
|
|
789
|
+
* Builds the initial confirm-field key state.
|
|
744
790
|
*
|
|
745
791
|
* @param field - The confirm field to render
|
|
746
792
|
* @param styler - The styler used to render the view
|
|
@@ -756,43 +802,50 @@ function createConfirmState(field, styler = (0, _orkestrel_console.createStyler)
|
|
|
756
802
|
};
|
|
757
803
|
}
|
|
758
804
|
/**
|
|
759
|
-
*
|
|
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
|
|
760
809
|
*/
|
|
761
|
-
function
|
|
762
|
-
const head =
|
|
810
|
+
function renderConfirmView(state) {
|
|
811
|
+
const head = renderPromptHeader(state.styler, state.theme, state.message);
|
|
763
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")}`;
|
|
764
813
|
return `${head} ${state.styler.render(state.theme.roles.hint, "(")}${answer}${state.styler.render(state.theme.roles.hint, ")")}`;
|
|
765
814
|
}
|
|
766
815
|
/**
|
|
767
|
-
*
|
|
816
|
+
* Advances a confirm prompt by one {@link KeyEvent} — the pure `(state, key) → PromptStep<boolean>`
|
|
768
817
|
* reducer. `y` / `Y` submits `true`, `n` / `N` submits `false`, return on an empty line submits
|
|
769
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
|
|
770
823
|
*/
|
|
771
|
-
function
|
|
824
|
+
function reduceConfirm(state, key) {
|
|
772
825
|
if (key.ctrl && key.name === "c") return {
|
|
773
826
|
state,
|
|
774
|
-
view:
|
|
827
|
+
view: renderConfirmView(state),
|
|
775
828
|
status: "cancel"
|
|
776
829
|
};
|
|
777
830
|
let answer;
|
|
778
|
-
const choice = key.name
|
|
831
|
+
const choice = key.name?.toLowerCase();
|
|
779
832
|
if (key.name === "return") answer = state.default;
|
|
780
833
|
else if (choice === "y") answer = true;
|
|
781
834
|
else if (choice === "n") answer = false;
|
|
782
835
|
if (answer === void 0) return {
|
|
783
836
|
state,
|
|
784
|
-
view:
|
|
837
|
+
view: renderConfirmView(state),
|
|
785
838
|
status: "active"
|
|
786
839
|
};
|
|
787
840
|
return {
|
|
788
841
|
state,
|
|
789
|
-
view: `${
|
|
842
|
+
view: `${renderSubmitHeader(state.styler, state.theme, state.message)} ${state.styler.render(state.theme.roles.hint, answer ? "yes" : "no")}`,
|
|
790
843
|
status: "submit",
|
|
791
844
|
value: answer
|
|
792
845
|
};
|
|
793
846
|
}
|
|
794
847
|
/**
|
|
795
|
-
*
|
|
848
|
+
* Builds the initial select-field key state.
|
|
796
849
|
*
|
|
797
850
|
* @param field - The select field to render
|
|
798
851
|
* @param styler - The styler used to render the view
|
|
@@ -810,30 +863,39 @@ function createSelectState(field, styler = (0, _orkestrel_console.createStyler)(
|
|
|
810
863
|
focused: index >= 0 ? index : 0
|
|
811
864
|
};
|
|
812
865
|
}
|
|
813
|
-
/**
|
|
814
|
-
|
|
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) {
|
|
815
873
|
const lines = state.choices.map((choice, index) => {
|
|
816
874
|
const active = index === state.focused;
|
|
817
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)}`}`;
|
|
818
876
|
});
|
|
819
|
-
return [
|
|
877
|
+
return [renderPromptHeader(state.styler, state.theme, state.message), ...lines].join("\n");
|
|
820
878
|
}
|
|
821
879
|
/**
|
|
822
|
-
*
|
|
880
|
+
* Advances a select prompt by one {@link KeyEvent} — the pure `(state, key) → PromptStep<string>`
|
|
823
881
|
* reducer. `up` / `down` (and `k` / `j`) move the focus, WRAPPING at the ends; return submits the
|
|
824
882
|
* focused choice's `value`; ctrl-c cancels. An empty choice list can never submit (a higher layer
|
|
825
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
|
|
826
888
|
*/
|
|
827
|
-
function
|
|
889
|
+
function reduceSelect(state, key) {
|
|
828
890
|
if (key.ctrl && key.name === "c") return {
|
|
829
891
|
state,
|
|
830
|
-
view:
|
|
892
|
+
view: renderSelectView(state),
|
|
831
893
|
status: "cancel"
|
|
832
894
|
};
|
|
833
895
|
const count = state.choices.length;
|
|
834
896
|
if (count === 0) return {
|
|
835
897
|
state,
|
|
836
|
-
view:
|
|
898
|
+
view: renderSelectView(state),
|
|
837
899
|
status: "active"
|
|
838
900
|
};
|
|
839
901
|
if (key.name === "up" || key.name === "k") {
|
|
@@ -843,7 +905,7 @@ function selectReduce(state, key) {
|
|
|
843
905
|
};
|
|
844
906
|
return {
|
|
845
907
|
state: next,
|
|
846
|
-
view:
|
|
908
|
+
view: renderSelectView(next),
|
|
847
909
|
status: "active"
|
|
848
910
|
};
|
|
849
911
|
}
|
|
@@ -854,7 +916,7 @@ function selectReduce(state, key) {
|
|
|
854
916
|
};
|
|
855
917
|
return {
|
|
856
918
|
state: next,
|
|
857
|
-
view:
|
|
919
|
+
view: renderSelectView(next),
|
|
858
920
|
status: "active"
|
|
859
921
|
};
|
|
860
922
|
}
|
|
@@ -863,19 +925,19 @@ function selectReduce(state, key) {
|
|
|
863
925
|
const value = choice?.value ?? "";
|
|
864
926
|
return {
|
|
865
927
|
state,
|
|
866
|
-
view: `${
|
|
928
|
+
view: `${renderSubmitHeader(state.styler, state.theme, state.message)} ${state.styler.render(state.theme.roles.hint, choice?.label ?? "")}`,
|
|
867
929
|
status: "submit",
|
|
868
930
|
value
|
|
869
931
|
};
|
|
870
932
|
}
|
|
871
933
|
return {
|
|
872
934
|
state,
|
|
873
|
-
view:
|
|
935
|
+
view: renderSelectView(state),
|
|
874
936
|
status: "active"
|
|
875
937
|
};
|
|
876
938
|
}
|
|
877
939
|
/**
|
|
878
|
-
*
|
|
940
|
+
* Builds the initial checkbox-field key state.
|
|
879
941
|
*
|
|
880
942
|
* @param field - The checkbox field to render
|
|
881
943
|
* @param styler - The styler used to render the view
|
|
@@ -897,8 +959,13 @@ function createCheckboxState(field, styler = (0, _orkestrel_console.createStyler
|
|
|
897
959
|
checked
|
|
898
960
|
};
|
|
899
961
|
}
|
|
900
|
-
/**
|
|
901
|
-
|
|
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) {
|
|
902
969
|
const lines = state.choices.map((choice, index) => {
|
|
903
970
|
const active = index === state.focused;
|
|
904
971
|
const ticked = state.checked.includes(index);
|
|
@@ -906,21 +973,25 @@ function checkboxView(state) {
|
|
|
906
973
|
});
|
|
907
974
|
const summary = state.styler.render(state.theme.roles.hint, `${state.checked.length} selected`);
|
|
908
975
|
return [
|
|
909
|
-
|
|
976
|
+
renderPromptHeader(state.styler, state.theme, state.message),
|
|
910
977
|
...lines,
|
|
911
978
|
summary
|
|
912
979
|
].join("\n");
|
|
913
980
|
}
|
|
914
981
|
/**
|
|
915
|
-
*
|
|
982
|
+
* Advances a checkbox prompt by one {@link KeyEvent} — the pure
|
|
916
983
|
* `(state, key) → PromptStep<readonly string[]>` reducer. `up` / `down` (and `k` / `j`) move the
|
|
917
984
|
* focus (wrapping); `space` toggles the focused index in the checked set; return submits the
|
|
918
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
|
|
919
990
|
*/
|
|
920
|
-
function
|
|
991
|
+
function reduceCheckbox(state, key) {
|
|
921
992
|
if (key.ctrl && key.name === "c") return {
|
|
922
993
|
state,
|
|
923
|
-
view:
|
|
994
|
+
view: renderCheckboxView(state),
|
|
924
995
|
status: "cancel"
|
|
925
996
|
};
|
|
926
997
|
const count = state.choices.length;
|
|
@@ -931,7 +1002,7 @@ function checkboxReduce(state, key) {
|
|
|
931
1002
|
};
|
|
932
1003
|
return {
|
|
933
1004
|
state: next,
|
|
934
|
-
view:
|
|
1005
|
+
view: renderCheckboxView(next),
|
|
935
1006
|
status: "active"
|
|
936
1007
|
};
|
|
937
1008
|
}
|
|
@@ -942,7 +1013,7 @@ function checkboxReduce(state, key) {
|
|
|
942
1013
|
};
|
|
943
1014
|
return {
|
|
944
1015
|
state: next,
|
|
945
|
-
view:
|
|
1016
|
+
view: renderCheckboxView(next),
|
|
946
1017
|
status: "active"
|
|
947
1018
|
};
|
|
948
1019
|
}
|
|
@@ -954,7 +1025,7 @@ function checkboxReduce(state, key) {
|
|
|
954
1025
|
};
|
|
955
1026
|
return {
|
|
956
1027
|
state: next,
|
|
957
|
-
view:
|
|
1028
|
+
view: renderCheckboxView(next),
|
|
958
1029
|
status: "active"
|
|
959
1030
|
};
|
|
960
1031
|
}
|
|
@@ -964,23 +1035,30 @@ function checkboxReduce(state, key) {
|
|
|
964
1035
|
const summary = ordered.map((index) => state.choices[index]?.label).filter((name) => name !== void 0).join(", ");
|
|
965
1036
|
return {
|
|
966
1037
|
state,
|
|
967
|
-
view: `${
|
|
1038
|
+
view: `${renderSubmitHeader(state.styler, state.theme, state.message)} ${state.styler.render(state.theme.roles.hint, summary)}`,
|
|
968
1039
|
status: "submit",
|
|
969
1040
|
value: values
|
|
970
1041
|
};
|
|
971
1042
|
}
|
|
972
1043
|
return {
|
|
973
1044
|
state,
|
|
974
|
-
view:
|
|
1045
|
+
view: renderCheckboxView(state),
|
|
975
1046
|
status: "active"
|
|
976
1047
|
};
|
|
977
1048
|
}
|
|
978
|
-
/**
|
|
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
|
+
*/
|
|
979
1057
|
function toggleIndex(indices, index) {
|
|
980
1058
|
return indices.includes(index) ? indices.filter((i) => i !== index) : [...indices, index];
|
|
981
1059
|
}
|
|
982
1060
|
/**
|
|
983
|
-
*
|
|
1061
|
+
* Builds the initial editor-field key state.
|
|
984
1062
|
*
|
|
985
1063
|
* @param field - The editor field to render
|
|
986
1064
|
* @param styler - The styler used to render the view
|
|
@@ -998,23 +1076,30 @@ function createEditorState(field, styler = (0, _orkestrel_console.createStyler)(
|
|
|
998
1076
|
};
|
|
999
1077
|
}
|
|
1000
1078
|
/**
|
|
1001
|
-
*
|
|
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
|
|
1002
1083
|
*/
|
|
1003
|
-
function
|
|
1004
|
-
const head =
|
|
1084
|
+
function renderEditorView(state) {
|
|
1085
|
+
const head = renderHintedHeader(state.styler, state.theme, state.message, "(Ctrl+D to finish)");
|
|
1005
1086
|
const pointer = state.styler.render(state.theme.roles.pointer, state.theme.icons.pointer);
|
|
1006
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");
|
|
1007
1088
|
}
|
|
1008
1089
|
/**
|
|
1009
|
-
*
|
|
1090
|
+
* Advances an editor prompt by one {@link KeyEvent} — the pure `(state, key) → PromptStep<string>`
|
|
1010
1091
|
* reducer. Printable characters extend the current line; backspace shrinks it; return commits the
|
|
1011
1092
|
* current line and starts a fresh one; ctrl-d FINISHES (joining all lines, falling back to the
|
|
1012
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
|
|
1013
1098
|
*/
|
|
1014
|
-
function
|
|
1099
|
+
function reduceEditor(state, key) {
|
|
1015
1100
|
if (key.ctrl && key.name === "c") return {
|
|
1016
1101
|
state,
|
|
1017
|
-
view:
|
|
1102
|
+
view: renderEditorView(state),
|
|
1018
1103
|
status: "cancel"
|
|
1019
1104
|
};
|
|
1020
1105
|
if (key.ctrl && key.name === "d") {
|
|
@@ -1023,7 +1108,7 @@ function editorReduce(state, key) {
|
|
|
1023
1108
|
const answer = joined.length > 0 ? joined : state.default;
|
|
1024
1109
|
return {
|
|
1025
1110
|
state,
|
|
1026
|
-
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"}`)}`,
|
|
1027
1112
|
status: "submit",
|
|
1028
1113
|
value: answer
|
|
1029
1114
|
};
|
|
@@ -1036,14 +1121,14 @@ function editorReduce(state, key) {
|
|
|
1036
1121
|
};
|
|
1037
1122
|
return {
|
|
1038
1123
|
state: next,
|
|
1039
|
-
view:
|
|
1124
|
+
view: renderEditorView(next),
|
|
1040
1125
|
status: "active"
|
|
1041
1126
|
};
|
|
1042
1127
|
}
|
|
1043
1128
|
const current = editLine(state.current, key);
|
|
1044
1129
|
if (current === void 0) return {
|
|
1045
1130
|
state,
|
|
1046
|
-
view:
|
|
1131
|
+
view: renderEditorView(state),
|
|
1047
1132
|
status: "active"
|
|
1048
1133
|
};
|
|
1049
1134
|
const next = {
|
|
@@ -1052,47 +1137,65 @@ function editorReduce(state, key) {
|
|
|
1052
1137
|
};
|
|
1053
1138
|
return {
|
|
1054
1139
|
state: next,
|
|
1055
|
-
view:
|
|
1140
|
+
view: renderEditorView(next),
|
|
1056
1141
|
status: "active"
|
|
1057
1142
|
};
|
|
1058
1143
|
}
|
|
1059
1144
|
/**
|
|
1060
|
-
*
|
|
1145
|
+
* Applies a single line-editing {@link KeyEvent} to a text buffer — the editing shared by input /
|
|
1061
1146
|
* password / editor. A printable key appends its character; `backspace` drops the last character;
|
|
1062
|
-
* `space` appends a space; ctrl-u clears the line.
|
|
1063
|
-
*
|
|
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
|
|
1064
1153
|
*/
|
|
1065
1154
|
function editLine(value, key) {
|
|
1066
1155
|
if (key.ctrl && key.name === "u") return "";
|
|
1067
1156
|
if (key.name === "backspace") return value.slice(0, -1);
|
|
1068
1157
|
if (key.name === "space") return `${value} `;
|
|
1069
|
-
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}`;
|
|
1070
1159
|
}
|
|
1071
1160
|
/**
|
|
1072
|
-
*
|
|
1073
|
-
* 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
|
|
1074
1163
|
* behind both the {@link import('./Prompt.js').Prompt} broker (its expiry) and the
|
|
1075
1164
|
* {@link import('./PromptClient.js').PromptClient} (its reconnect backoff); a test injects a
|
|
1076
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
|
|
1077
1170
|
*/
|
|
1078
1171
|
function defaultTimer(callback, ms) {
|
|
1079
1172
|
const handle = setTimeout(callback, ms);
|
|
1080
1173
|
return () => clearTimeout(handle);
|
|
1081
1174
|
}
|
|
1082
|
-
/**
|
|
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
|
+
*/
|
|
1083
1183
|
function globalFetch(input, init) {
|
|
1084
1184
|
return fetch(input, init);
|
|
1085
1185
|
}
|
|
1086
1186
|
/**
|
|
1087
|
-
*
|
|
1187
|
+
* Checks whether a caught value is an `AbortError` — the {@link import('./PromptClient.js').PromptClient}
|
|
1088
1188
|
* distinguishes a deliberate `disconnect` / teardown (an aborted `fetch`) from a real fault, so it
|
|
1089
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
|
|
1090
1193
|
*/
|
|
1091
1194
|
function isAbortError(error) {
|
|
1092
1195
|
return (error instanceof DOMException || error instanceof Error) && error.name === "AbortError";
|
|
1093
1196
|
}
|
|
1094
1197
|
/**
|
|
1095
|
-
*
|
|
1198
|
+
* Checks whether `url` is an INSECURE remote endpoint — a plain `http://` URL whose host is NOT a
|
|
1096
1199
|
* loopback address. Pure string parsing (no `URL` global), so it stays total on malformed input.
|
|
1097
1200
|
*
|
|
1098
1201
|
* @remarks
|
|
@@ -1101,7 +1204,7 @@ function isAbortError(error) {
|
|
|
1101
1204
|
* An `https://` URL (or any non-`http://` scheme) is never flagged.
|
|
1102
1205
|
*
|
|
1103
1206
|
* @param url - The candidate endpoint URL
|
|
1104
|
-
* @returns
|
|
1207
|
+
* @returns True if `url` is a non-loopback `http://` endpoint; false otherwise
|
|
1105
1208
|
*
|
|
1106
1209
|
* @example
|
|
1107
1210
|
* ```ts
|
|
@@ -1119,7 +1222,12 @@ function isInsecureRemote(url) {
|
|
|
1119
1222
|
const hostname = host.startsWith("[") ? host.slice(0, host.indexOf("]") + 1) : host.split(":")[0] ?? "";
|
|
1120
1223
|
return hostname !== "localhost" && hostname !== "127.0.0.1" && hostname !== "[::1]";
|
|
1121
1224
|
}
|
|
1122
|
-
/**
|
|
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
|
+
*/
|
|
1123
1231
|
function serializePending(form) {
|
|
1124
1232
|
return {
|
|
1125
1233
|
event: "pending",
|
|
@@ -1127,24 +1235,33 @@ function serializePending(form) {
|
|
|
1127
1235
|
id: form.id
|
|
1128
1236
|
};
|
|
1129
1237
|
}
|
|
1130
|
-
/**
|
|
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
|
+
*/
|
|
1131
1244
|
function serializeExpire(id) {
|
|
1132
1245
|
return {
|
|
1133
1246
|
event: "expire",
|
|
1134
1247
|
data: JSON.stringify({ id })
|
|
1135
1248
|
};
|
|
1136
1249
|
}
|
|
1137
|
-
/**
|
|
1138
|
-
|
|
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() {
|
|
1139
1256
|
return {
|
|
1140
|
-
event: "
|
|
1257
|
+
event: "destroy",
|
|
1141
1258
|
data: ""
|
|
1142
1259
|
};
|
|
1143
1260
|
}
|
|
1144
1261
|
//#endregion
|
|
1145
1262
|
//#region src/core/Prompt.ts
|
|
1146
1263
|
/**
|
|
1147
|
-
*
|
|
1264
|
+
* Implements the headless form broker. It parks live forms, exposes their serialized schemas, and applies
|
|
1148
1265
|
* remote answers to the authoritative form.
|
|
1149
1266
|
*
|
|
1150
1267
|
* @remarks
|
|
@@ -1335,7 +1452,7 @@ var Prompt = class {
|
|
|
1335
1452
|
//#endregion
|
|
1336
1453
|
//#region src/core/PromptClient.ts
|
|
1337
1454
|
/**
|
|
1338
|
-
*
|
|
1455
|
+
* Implements the SSE form bridge. It ingests serialized forms from a remote broker, renders them through a
|
|
1339
1456
|
* local terminal, and posts answers back without blocking the event stream.
|
|
1340
1457
|
*
|
|
1341
1458
|
* @remarks
|
|
@@ -1344,12 +1461,12 @@ var Prompt = class {
|
|
|
1344
1461
|
* {@link disconnect} deliberately stopped it.
|
|
1345
1462
|
* - **Ingest + render.** Each `pending` envelope passes through `isPendingForm`, Form's `parseForm`,
|
|
1346
1463
|
* and terminal's `sanitizeSchema`, then enters a serial render queue. The SSE reader never awaits
|
|
1347
|
-
* that queue, so `expire` and `
|
|
1464
|
+
* that queue, so `expire` and `destroy` remain live while a person is answering.
|
|
1348
1465
|
* - **Safe local form.** The rendering copy omits every wire `pattern`, because Form compiles a
|
|
1349
1466
|
* pattern during local evaluation. The broker's parked form retains it and remains authoritative.
|
|
1350
1467
|
* - **Refusal retry.** A structured `rejected` response seeds a new rendering form with the values
|
|
1351
1468
|
* just submitted, applies every {@link FieldError} through `invalidate`, and asks again. No retry
|
|
1352
|
-
* counter truncates the loop; acceptance, expiry, and
|
|
1469
|
+
* counter truncates the loop; acceptance, expiry, and the broker's own teardown are its bounds.
|
|
1353
1470
|
* - **Replay safety.** A replayed id is skipped while it is queued, rendering, or posting. Once an
|
|
1354
1471
|
* attempt ends, a later delivery of that id may be rendered again.
|
|
1355
1472
|
*
|
|
@@ -1487,7 +1604,7 @@ var PromptClient = class {
|
|
|
1487
1604
|
if ((0, _orkestrel_contract.isRecord)(parsed) && (0, _orkestrel_contract.isString)(parsed.id)) this.#expire(parsed.id);
|
|
1488
1605
|
return;
|
|
1489
1606
|
}
|
|
1490
|
-
if (event.event === SSE_EVENTS.
|
|
1607
|
+
if (event.event === SSE_EVENTS.destroy) {
|
|
1491
1608
|
this.disconnect();
|
|
1492
1609
|
this.#interrupt();
|
|
1493
1610
|
}
|
|
@@ -1640,10 +1757,9 @@ var PromptClient = class {
|
|
|
1640
1757
|
//#endregion
|
|
1641
1758
|
//#region src/core/TerminalManager.ts
|
|
1642
1759
|
/**
|
|
1643
|
-
*
|
|
1644
|
-
*
|
|
1645
|
-
*
|
|
1646
|
-
* 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.
|
|
1647
1763
|
*
|
|
1648
1764
|
* @remarks
|
|
1649
1765
|
* - **Registry.** `add(name, options?)` mints (or, if `name` is already mounted, returns the
|
|
@@ -1658,7 +1774,7 @@ var PromptClient = class {
|
|
|
1658
1774
|
* - **Durable open / save.** `open(name)` restores an EMPTY broker from the `store` (parked
|
|
1659
1775
|
* Promises are process-bound and never resurrected); `save(name)` persists the endpoint's
|
|
1660
1776
|
* configured `timeout`.
|
|
1661
|
-
* - **Removal.** `remove` drops one endpoint, a batch (
|
|
1777
|
+
* - **Removal.** `remove` drops one endpoint, a batch (the array overload declared FIRST), or every
|
|
1662
1778
|
* endpoint when called without an argument. It destroys each broker, which expires every form
|
|
1663
1779
|
* still parked on it. `destroy` is idempotent.
|
|
1664
1780
|
*
|
|
@@ -1703,7 +1819,7 @@ var TerminalManager = class {
|
|
|
1703
1819
|
return this.#terminals.get(name);
|
|
1704
1820
|
}
|
|
1705
1821
|
terminals() {
|
|
1706
|
-
return [...this.#terminals.
|
|
1822
|
+
return [...this.#terminals.values()];
|
|
1707
1823
|
}
|
|
1708
1824
|
add(name, options) {
|
|
1709
1825
|
if (this.#destroyed) throw new TerminalError("DESTROYED", "manager destroyed");
|
|
@@ -1735,7 +1851,7 @@ var TerminalManager = class {
|
|
|
1735
1851
|
ask(from, to, form) {
|
|
1736
1852
|
const broker = this.#terminals.get(to);
|
|
1737
1853
|
if (broker === void 0) {
|
|
1738
|
-
const known = this.
|
|
1854
|
+
const known = [...this.#terminals.keys()];
|
|
1739
1855
|
return Promise.reject(new TerminalError("TARGET", `unknown terminal '${to}' (known: ${known.length > 0 ? known.join(", ") : "none"})`, {
|
|
1740
1856
|
to,
|
|
1741
1857
|
known
|
|
@@ -1773,7 +1889,7 @@ var TerminalManager = class {
|
|
|
1773
1889
|
const broker = this.#terminals.get(to);
|
|
1774
1890
|
if (broker === void 0) return {
|
|
1775
1891
|
success: false,
|
|
1776
|
-
error: { reason: "
|
|
1892
|
+
error: { reason: "target" }
|
|
1777
1893
|
};
|
|
1778
1894
|
const result = broker.answer(id, values);
|
|
1779
1895
|
if (result.success) this.#edges.delete(id);
|
|
@@ -1806,8 +1922,8 @@ var TerminalManager = class {
|
|
|
1806
1922
|
return;
|
|
1807
1923
|
}
|
|
1808
1924
|
if ((0, _orkestrel_contract.isArray)(names)) {
|
|
1809
|
-
let removed =
|
|
1810
|
-
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;
|
|
1811
1927
|
return removed;
|
|
1812
1928
|
}
|
|
1813
1929
|
return this.#removeOne(names);
|
|
@@ -1882,16 +1998,16 @@ var TerminalManager = class {
|
|
|
1882
1998
|
}
|
|
1883
1999
|
};
|
|
1884
2000
|
//#endregion
|
|
1885
|
-
//#region src/core/MemoryTerminalStore.ts
|
|
2001
|
+
//#region src/core/stores/MemoryTerminalStore.ts
|
|
1886
2002
|
/**
|
|
1887
|
-
*
|
|
2003
|
+
* Implements the in-memory {@link TerminalStoreInterface} — a process-lifetime `Map` of
|
|
1888
2004
|
* {@link TerminalSnapshot}s keyed by endpoint id, the DEFAULT store
|
|
1889
|
-
* {@link import('
|
|
2005
|
+
* {@link import('../factories.js').createMemoryTerminalStore} builds. The EXACT twin of
|
|
1890
2006
|
* {@link import('./DatabaseTerminalStore.js').DatabaseTerminalStore}.
|
|
1891
2007
|
*
|
|
1892
2008
|
* @remarks
|
|
1893
|
-
* A plain `Map<string, TerminalSnapshot>`
|
|
1894
|
-
*
|
|
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
|
|
1895
2011
|
* eviction: a persisted config lives until an explicit `delete`. A durable backend (JSON / SQLite /
|
|
1896
2012
|
* IndexedDB) swaps in through the SAME interface without touching the manager — its
|
|
1897
2013
|
* driver-pluggable twin is {@link import('./DatabaseTerminalStore.js').DatabaseTerminalStore} (the
|
|
@@ -1901,13 +2017,13 @@ var TerminalManager = class {
|
|
|
1901
2017
|
* - **`set` inserts / replaces under the snapshot's OWN `id`** (no separate id param).
|
|
1902
2018
|
* - **`delete` drops a snapshot by id**; an absent id is a no-op (no throw).
|
|
1903
2019
|
*
|
|
1904
|
-
* The public surface is EXACTLY `get` / `set` / `delete` — no extra members
|
|
1905
|
-
*
|
|
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
|
|
1906
2022
|
* restores an EMPTY broker — parked Promises are process-bound and never resurrected.
|
|
1907
2023
|
*
|
|
1908
2024
|
* @example
|
|
1909
2025
|
* ```ts
|
|
1910
|
-
* import { createMemoryTerminalStore } from '@
|
|
2026
|
+
* import { createMemoryTerminalStore } from '@orkestrel/terminal'
|
|
1911
2027
|
*
|
|
1912
2028
|
* const store = createMemoryTerminalStore()
|
|
1913
2029
|
* await store.set({ id: 'shell', timeout: 5000 }) // persist a config
|
|
@@ -1917,22 +2033,40 @@ var TerminalManager = class {
|
|
|
1917
2033
|
*/
|
|
1918
2034
|
var MemoryTerminalStore = class {
|
|
1919
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
|
+
*/
|
|
1920
2042
|
get(id) {
|
|
1921
2043
|
return Promise.resolve(this.#snapshots.get(id));
|
|
1922
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
|
+
*/
|
|
1923
2051
|
set(snapshot) {
|
|
1924
2052
|
this.#snapshots.set(snapshot.id, snapshot);
|
|
1925
2053
|
return Promise.resolve();
|
|
1926
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
|
+
*/
|
|
1927
2061
|
delete(id) {
|
|
1928
2062
|
this.#snapshots.delete(id);
|
|
1929
2063
|
return Promise.resolve();
|
|
1930
2064
|
}
|
|
1931
2065
|
};
|
|
1932
2066
|
//#endregion
|
|
1933
|
-
//#region src/core/DatabaseTerminalStore.ts
|
|
2067
|
+
//#region src/core/stores/DatabaseTerminalStore.ts
|
|
1934
2068
|
/**
|
|
1935
|
-
*
|
|
2069
|
+
* Implements a {@link TerminalStoreInterface} backed by one table of the `databases` layer — an endpoint's
|
|
1936
2070
|
* durable CONFIG state IS a row, so persistence reduces to keyed point-access (`get` / `set` /
|
|
1937
2071
|
* `delete`) over a {@link TableInterface}, the driver-pluggable twin of the plain-`Map`
|
|
1938
2072
|
* {@link import('./MemoryTerminalStore.js').MemoryTerminalStore}.
|
|
@@ -1942,7 +2076,7 @@ var MemoryTerminalStore = class {
|
|
|
1942
2076
|
* JSON, SQLite, IndexedDB) is chosen by whoever builds it (the factories), so a JSON / SQLite /
|
|
1943
2077
|
* IndexedDB backend swaps in WITHOUT touching the manager — the same seam as
|
|
1944
2078
|
* {@link import('./MemoryTerminalStore.js').MemoryTerminalStore}. The driver defaults to memory
|
|
1945
|
-
* ({@link import('
|
|
2079
|
+
* ({@link import('../factories.js').createDatabaseTerminalStore} passes `createMemoryDriver()`), so
|
|
1946
2080
|
* it ALSO works in memory out of the box; you opt into the durable plumbing by passing a JSON /
|
|
1947
2081
|
* SQLite / IndexedDB driver.
|
|
1948
2082
|
*
|
|
@@ -1954,18 +2088,19 @@ var MemoryTerminalStore = class {
|
|
|
1954
2088
|
* - **`set(snapshot)` upserts under the snapshot's OWN `id`** (no separate id param) — it writes
|
|
1955
2089
|
* the row `{ id: snapshot.id, snapshot }`.
|
|
1956
2090
|
* - **`get(id)` resolves the stored snapshot for an id**, narrowing the opaque JSON column back to
|
|
1957
|
-
* a {@link TerminalSnapshot} ({@link import('
|
|
1958
|
-
*
|
|
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.
|
|
1959
2093
|
* - **`delete(id)` drops a snapshot by id**; an absent id is a no-op (no throw).
|
|
1960
2094
|
*
|
|
1961
2095
|
* There is NO idle-TTL / eviction — a persisted config lives until an explicit `delete`. The public
|
|
1962
|
-
* surface is EXACTLY `get` / `set` / `delete` — no extra members
|
|
1963
|
-
* {@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
|
|
1964
2098
|
* broker — parked Promises are process-bound and never resurrected.
|
|
1965
2099
|
*
|
|
1966
2100
|
* @example
|
|
1967
2101
|
* ```ts
|
|
1968
|
-
* import { createDatabaseTerminalStore
|
|
2102
|
+
* import { createDatabaseTerminalStore } from '@orkestrel/terminal'
|
|
2103
|
+
* import { createMemoryDriver } from '@orkestrel/database'
|
|
1969
2104
|
*
|
|
1970
2105
|
* const store = createDatabaseTerminalStore(createMemoryDriver()) // a durable driver swaps in here
|
|
1971
2106
|
* await store.set({ id: 'shell', timeout: 5000 }) // persist the config (one JSON column)
|
|
@@ -1976,7 +2111,7 @@ var MemoryTerminalStore = class {
|
|
|
1976
2111
|
var DatabaseTerminalStore = class {
|
|
1977
2112
|
#table;
|
|
1978
2113
|
/**
|
|
1979
|
-
*
|
|
2114
|
+
* Wraps a table as a terminal store.
|
|
1980
2115
|
*
|
|
1981
2116
|
* @param table - The {@link TableInterface} holding the snapshots — its row is the
|
|
1982
2117
|
* {@link TerminalSnapshotRow} `{ id; snapshot }` shape (the snapshot one opaque JSON column)
|
|
@@ -1984,20 +2119,37 @@ var DatabaseTerminalStore = class {
|
|
|
1984
2119
|
constructor(table) {
|
|
1985
2120
|
this.#table = table;
|
|
1986
2121
|
}
|
|
1987
|
-
/**
|
|
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
|
+
*/
|
|
1988
2129
|
async get(id) {
|
|
1989
2130
|
const row = await this.#table.get(id);
|
|
1990
2131
|
if (row === void 0) return void 0;
|
|
1991
2132
|
return isTerminalSnapshot(row.snapshot) ? row.snapshot : void 0;
|
|
1992
2133
|
}
|
|
1993
|
-
/**
|
|
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
|
+
*/
|
|
1994
2141
|
async set(snapshot) {
|
|
1995
2142
|
await this.#table.set({
|
|
1996
2143
|
id: snapshot.id,
|
|
1997
2144
|
snapshot
|
|
1998
2145
|
});
|
|
1999
2146
|
}
|
|
2000
|
-
/**
|
|
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
|
+
*/
|
|
2001
2153
|
async delete(id) {
|
|
2002
2154
|
await this.#table.remove(id);
|
|
2003
2155
|
}
|
|
@@ -2005,7 +2157,7 @@ var DatabaseTerminalStore = class {
|
|
|
2005
2157
|
//#endregion
|
|
2006
2158
|
//#region src/core/factories.ts
|
|
2007
2159
|
/**
|
|
2008
|
-
*
|
|
2160
|
+
* Creates the headless {@link PromptInterface} broker. It parks live forms and applies remote
|
|
2009
2161
|
* answers to the authoritative instances.
|
|
2010
2162
|
*
|
|
2011
2163
|
* @param options - See {@link PromptOptions}
|
|
@@ -2018,7 +2170,7 @@ var DatabaseTerminalStore = class {
|
|
|
2018
2170
|
*
|
|
2019
2171
|
* @example
|
|
2020
2172
|
* ```ts
|
|
2021
|
-
* import { createPrompt } from '@
|
|
2173
|
+
* import { createPrompt } from '@orkestrel/terminal'
|
|
2022
2174
|
* import { createForm } from '@orkestrel/form'
|
|
2023
2175
|
*
|
|
2024
2176
|
* const prompt = createPrompt()
|
|
@@ -2031,7 +2183,7 @@ function createPrompt(options) {
|
|
|
2031
2183
|
return new Prompt(options);
|
|
2032
2184
|
}
|
|
2033
2185
|
/**
|
|
2034
|
-
*
|
|
2186
|
+
* Creates the SSE prompt {@link PromptClientInterface} BRIDGE — it connects to a remote broker's SSE
|
|
2035
2187
|
* endpoint, dispatches each received form to a local {@link import('./types.js').TerminalInterface},
|
|
2036
2188
|
* and POSTs the answer back. Universal — `fetch` / SSE are web-standard.
|
|
2037
2189
|
*
|
|
@@ -2043,11 +2195,11 @@ function createPrompt(options) {
|
|
|
2043
2195
|
* ends; it reconnects with the `delay` backoff unless `reconnect` is `false` / the client was
|
|
2044
2196
|
* `destroy`ed. Inject `options.fetch` (a scripted `fetch`) and `options.timer` to drive it
|
|
2045
2197
|
* deterministically in tests — no real network.
|
|
2046
|
-
* -
|
|
2198
|
+
* - **Wire narrowing.** Every decoded prompt is guard-narrowed before dispatch (never an `as`).
|
|
2047
2199
|
*
|
|
2048
2200
|
* @example
|
|
2049
2201
|
* ```ts
|
|
2050
|
-
* import { createPromptClient } from '@
|
|
2202
|
+
* import { createPromptClient } from '@orkestrel/terminal'
|
|
2051
2203
|
*
|
|
2052
2204
|
* const client = createPromptClient({ url: 'http://host/prompts', terminal })
|
|
2053
2205
|
* await client.connect()
|
|
@@ -2057,7 +2209,7 @@ function createPromptClient(options) {
|
|
|
2057
2209
|
return new PromptClient(options);
|
|
2058
2210
|
}
|
|
2059
2211
|
/**
|
|
2060
|
-
*
|
|
2212
|
+
* Creates the multi-endpoint {@link TerminalManager} — a named registry of
|
|
2061
2213
|
* {@link PromptInterface} brokers so several parties can `ask` prompts of each other by name,
|
|
2062
2214
|
* with a transitive DEADLOCK check across every in-flight ask.
|
|
2063
2215
|
*
|
|
@@ -2066,7 +2218,7 @@ function createPromptClient(options) {
|
|
|
2066
2218
|
*
|
|
2067
2219
|
* @example
|
|
2068
2220
|
* ```ts
|
|
2069
|
-
* import { createTerminalManager } from '@
|
|
2221
|
+
* import { createTerminalManager } from '@orkestrel/terminal'
|
|
2070
2222
|
*
|
|
2071
2223
|
* const manager = createTerminalManager()
|
|
2072
2224
|
* manager.add('agent')
|
|
@@ -2076,14 +2228,14 @@ function createTerminalManager(options) {
|
|
|
2076
2228
|
return new TerminalManager(options);
|
|
2077
2229
|
}
|
|
2078
2230
|
/**
|
|
2079
|
-
*
|
|
2231
|
+
* Creates the in-memory {@link TerminalStoreInterface} — a process-lifetime `Map` of endpoint
|
|
2080
2232
|
* config snapshots, the default store backing a {@link TerminalManagerInterface}'s `open` / `save`.
|
|
2081
2233
|
*
|
|
2082
2234
|
* @returns A {@link TerminalStoreInterface}
|
|
2083
2235
|
*
|
|
2084
2236
|
* @example
|
|
2085
2237
|
* ```ts
|
|
2086
|
-
* import { createMemoryTerminalStore } from '@
|
|
2238
|
+
* import { createMemoryTerminalStore } from '@orkestrel/terminal'
|
|
2087
2239
|
*
|
|
2088
2240
|
* const store = createMemoryTerminalStore()
|
|
2089
2241
|
* ```
|
|
@@ -2092,7 +2244,7 @@ function createMemoryTerminalStore() {
|
|
|
2092
2244
|
return new MemoryTerminalStore();
|
|
2093
2245
|
}
|
|
2094
2246
|
/**
|
|
2095
|
-
*
|
|
2247
|
+
* Creates a {@link TerminalStoreInterface} backed by one table of the `databases` layer — the
|
|
2096
2248
|
* driver-pluggable twin of {@link createMemoryTerminalStore}, storing each endpoint's config
|
|
2097
2249
|
* snapshot as one opaque JSON column.
|
|
2098
2250
|
*
|
|
@@ -2101,7 +2253,7 @@ function createMemoryTerminalStore() {
|
|
|
2101
2253
|
*
|
|
2102
2254
|
* @example
|
|
2103
2255
|
* ```ts
|
|
2104
|
-
* import { createDatabaseTerminalStore } from '@
|
|
2256
|
+
* import { createDatabaseTerminalStore } from '@orkestrel/terminal'
|
|
2105
2257
|
*
|
|
2106
2258
|
* const store = createDatabaseTerminalStore() // in-memory by default
|
|
2107
2259
|
* ```
|
|
@@ -2131,9 +2283,7 @@ exports.DEFAULT_PROMPT_TIMEOUT_MS = DEFAULT_PROMPT_TIMEOUT_MS;
|
|
|
2131
2283
|
exports.DEFAULT_RECONNECT_DELAY_MS = DEFAULT_RECONNECT_DELAY_MS;
|
|
2132
2284
|
exports.DELETE = DELETE;
|
|
2133
2285
|
exports.DatabaseTerminalStore = DatabaseTerminalStore;
|
|
2134
|
-
exports.ESCAPE = ESCAPE;
|
|
2135
2286
|
exports.HEADER_TOKEN = HEADER_TOKEN;
|
|
2136
|
-
exports.KEY_CSI = KEY_CSI;
|
|
2137
2287
|
exports.KEY_SS3 = KEY_SS3;
|
|
2138
2288
|
exports.MemoryTerminalStore = MemoryTerminalStore;
|
|
2139
2289
|
exports.NEWLINE = NEWLINE;
|
|
@@ -2149,10 +2299,6 @@ exports.SSE_EVENTS = SSE_EVENTS;
|
|
|
2149
2299
|
exports.TAB = TAB;
|
|
2150
2300
|
exports.TerminalError = TerminalError;
|
|
2151
2301
|
exports.TerminalManager = TerminalManager;
|
|
2152
|
-
exports.checkboxReduce = checkboxReduce;
|
|
2153
|
-
exports.checkboxView = checkboxView;
|
|
2154
|
-
exports.confirmReduce = confirmReduce;
|
|
2155
|
-
exports.confirmView = confirmView;
|
|
2156
2302
|
exports.createCheckboxState = createCheckboxState;
|
|
2157
2303
|
exports.createConfirmState = createConfirmState;
|
|
2158
2304
|
exports.createDatabaseTerminalStore = createDatabaseTerminalStore;
|
|
@@ -2167,13 +2313,7 @@ exports.createSelectState = createSelectState;
|
|
|
2167
2313
|
exports.createTerminalManager = createTerminalManager;
|
|
2168
2314
|
exports.defaultTimer = defaultTimer;
|
|
2169
2315
|
exports.editLine = editLine;
|
|
2170
|
-
exports.editorReduce = editorReduce;
|
|
2171
|
-
exports.editorView = editorView;
|
|
2172
|
-
exports.errorLine = errorLine;
|
|
2173
2316
|
exports.globalFetch = globalFetch;
|
|
2174
|
-
exports.hintedHeader = hintedHeader;
|
|
2175
|
-
exports.inputReduce = inputReduce;
|
|
2176
|
-
exports.inputView = inputView;
|
|
2177
2317
|
exports.isAbortError = isAbortError;
|
|
2178
2318
|
exports.isInsecureRemote = isInsecureRemote;
|
|
2179
2319
|
exports.isPendingForm = isPendingForm;
|
|
@@ -2183,18 +2323,28 @@ exports.isTerminalError = isTerminalError;
|
|
|
2183
2323
|
exports.isTerminalSnapshot = isTerminalSnapshot;
|
|
2184
2324
|
exports.isWireEvent = isWireEvent;
|
|
2185
2325
|
exports.parseKey = parseKey;
|
|
2186
|
-
exports.
|
|
2187
|
-
exports.
|
|
2188
|
-
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;
|
|
2189
2342
|
exports.sanitizeDisplayText = sanitizeDisplayText;
|
|
2190
2343
|
exports.sanitizeSchema = sanitizeSchema;
|
|
2191
2344
|
exports.sanitizeThemeIcons = sanitizeThemeIcons;
|
|
2192
|
-
exports.
|
|
2193
|
-
exports.selectView = selectView;
|
|
2345
|
+
exports.serializeDestroy = serializeDestroy;
|
|
2194
2346
|
exports.serializeExpire = serializeExpire;
|
|
2195
2347
|
exports.serializePending = serializePending;
|
|
2196
|
-
exports.serializeShutdown = serializeShutdown;
|
|
2197
|
-
exports.submitHeader = submitHeader;
|
|
2198
2348
|
exports.toggleIndex = toggleIndex;
|
|
2199
2349
|
|
|
2200
2350
|
//# sourceMappingURL=index.cjs.map
|