@orkestrel/terminal 0.0.13 → 0.0.15

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