@orkestrel/terminal 0.0.1

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.
@@ -0,0 +1,1651 @@
1
+ import { isArray, isBoolean, isNonEmptyString, isNumber, isRecord, isString, literalOf, recordOf } from "@orkestrel/contract";
2
+ import { STATUS_ICONS, createStyler, stripControls } from "@orkestrel/console";
3
+ import { Emitter } from "@orkestrel/emitter";
4
+ import { createSSEParser } from "@orkestrel/sse";
5
+ //#region src/core/constants.ts
6
+ /** Carriage return (`\r`, U+000D) — Enter on most terminals. */
7
+ var RETURN = String.fromCharCode(13);
8
+ /** Line feed (`\n`, U+000A) — Enter on some terminals / pasted input. */
9
+ var NEWLINE = String.fromCharCode(10);
10
+ /** Tab (`\t`, U+0009). */
11
+ var TAB = String.fromCharCode(9);
12
+ /** Escape (ESC, U+001B) — the lone byte, and the lead byte of every CSI / SS3 sequence. */
13
+ var ESCAPE = String.fromCharCode(27);
14
+ /** Backspace (BS, U+0008) — Ctrl+H / some terminals' Backspace. */
15
+ var BACKSPACE = String.fromCharCode(8);
16
+ /** Delete (DEL, U+007F) — the usual Backspace byte on a Unix TTY. */
17
+ var DELETE = String.fromCharCode(127);
18
+ /** Space (U+0020). */
19
+ var SPACE = " ";
20
+ /** Ctrl+C (ETX, U+0003) — interrupt / cancel. */
21
+ var CTRL_C = String.fromCharCode(3);
22
+ /** Ctrl+D (EOT, U+0004) — end-of-transmission / finish (the editor's commit key). */
23
+ var CTRL_D = String.fromCharCode(4);
24
+ /** Ctrl+U (NAK, U+0015) — clear the current line. */
25
+ var CTRL_U = String.fromCharCode(21);
26
+ /** Ctrl+A (SOH, U+0001) — move to start of line. */
27
+ var CTRL_A = String.fromCharCode(1);
28
+ /** Ctrl+E (ENQ, U+0005) — move to end of line. */
29
+ var CTRL_E = String.fromCharCode(5);
30
+ /**
31
+ * The Control Sequence Introducer lead (`ESC[`) for the navigation keys — the prefix of the
32
+ * arrow / home / end / delete sequences {@link SEQUENCE_NAMES} is keyed by. Named `KEY_CSI`
33
+ * (not `CSI`) so it never collides with the console module's SGR `CSI` (both barrel through
34
+ * `@src/core`).
35
+ */
36
+ var KEY_CSI = `[`;
37
+ /** The Single Shift Three lead (`ESCO`) — the alternate arrow-key prefix some terminals emit (`ESC O A`). */
38
+ var KEY_SS3 = `O`;
39
+ /**
40
+ * The exact escape SEQUENCE → canonical key NAME table {@link import('./helpers.js').parseKey}
41
+ * consults for the navigation / editing keys. Covers BOTH the CSI form (`ESC[A`…) and the SS3
42
+ * form (`ESCOA`…) of the four arrows, plus the `home` / `end` / `delete` CSI sequences (with
43
+ * their numeric-tilde variants). The source of truth for the multi-byte key decode; frozen.
44
+ *
45
+ * @remarks
46
+ * Terminals disagree on these: a cursor key is `ESC[A` (normal) or `ESCOA` (application mode),
47
+ * and Home / End / Delete each have a letter form (`ESC[H` / `ESC[F`) and a numeric form
48
+ * (`ESC[1~` / `ESC[4~` / `ESC[3~`). Every accepted spelling maps to one name so a reducer never
49
+ * sees the wire encoding.
50
+ */
51
+ var SEQUENCE_NAMES = Object.freeze({
52
+ [`${KEY_CSI}A`]: "up",
53
+ [`${KEY_CSI}B`]: "down",
54
+ [`${KEY_CSI}C`]: "right",
55
+ [`${KEY_CSI}D`]: "left",
56
+ [`${KEY_SS3}A`]: "up",
57
+ [`${KEY_SS3}B`]: "down",
58
+ [`${KEY_SS3}C`]: "right",
59
+ [`${KEY_SS3}D`]: "left",
60
+ [`${KEY_CSI}H`]: "home",
61
+ [`${KEY_CSI}F`]: "end",
62
+ [`${KEY_CSI}1~`]: "home",
63
+ [`${KEY_CSI}4~`]: "end",
64
+ [`${KEY_CSI}3~`]: "delete",
65
+ [`${KEY_CSI}7~`]: "home",
66
+ [`${KEY_CSI}8~`]: "end"
67
+ });
68
+ /**
69
+ * The single control BYTE → key descriptor table {@link import('./helpers.js').parseKey}
70
+ * consults for the one-byte keys. Each entry carries the canonical `name` and whether it is a
71
+ * `ctrl` combination. The source of truth for the single-byte key decode; frozen.
72
+ *
73
+ * @remarks
74
+ * `return` / `newline` map to `return` (one canonical Enter name); `delete` / `backspace` both
75
+ * map to `backspace` (the two Backspace bytes); the Ctrl combos (`c` / `d` / `u` / `a` / `e`)
76
+ * carry `ctrl: true` so a reducer can match `key.ctrl && key.name === 'c'`. `escape` / `tab` /
77
+ * `space` are plain named keys.
78
+ */
79
+ var CONTROL_NAMES = Object.freeze({
80
+ ["\r"]: Object.freeze({
81
+ name: "return",
82
+ ctrl: false
83
+ }),
84
+ ["\n"]: Object.freeze({
85
+ name: "return",
86
+ ctrl: false
87
+ }),
88
+ [" "]: Object.freeze({
89
+ name: "tab",
90
+ ctrl: false
91
+ }),
92
+ ["\x1B"]: Object.freeze({
93
+ name: "escape",
94
+ ctrl: false
95
+ }),
96
+ ["\b"]: Object.freeze({
97
+ name: "backspace",
98
+ ctrl: false
99
+ }),
100
+ [""]: Object.freeze({
101
+ name: "backspace",
102
+ ctrl: false
103
+ }),
104
+ [" "]: Object.freeze({
105
+ name: "space",
106
+ ctrl: false
107
+ }),
108
+ [""]: Object.freeze({
109
+ name: "c",
110
+ ctrl: true
111
+ }),
112
+ [""]: Object.freeze({
113
+ name: "d",
114
+ ctrl: true
115
+ }),
116
+ [""]: Object.freeze({
117
+ name: "u",
118
+ ctrl: true
119
+ }),
120
+ [""]: Object.freeze({
121
+ name: "a",
122
+ ctrl: true
123
+ }),
124
+ [""]: Object.freeze({
125
+ name: "e",
126
+ ctrl: true
127
+ })
128
+ });
129
+ /** The default mask glyph a {@link import('./types.js').PasswordState} renders each input character as — `*`. */
130
+ var DEFAULT_MASK = "*";
131
+ /** Matches an email address — a non-trivial `local@domain.tld` shape. The `email` rule tests against this. */
132
+ var EMAIL_PATTERN = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
133
+ /** Matches an HTTP(S) URL. The `url` rule tests against this. */
134
+ var URL_PATTERN = /^https?:\/\/.+/;
135
+ /** Matches a numeric value (integer or decimal, optional sign). The `numeric` rule tests against this. */
136
+ var NUMERIC_PATTERN = /^-?\d+(\.\d+)?$/;
137
+ /** Matches an integer (optional sign). The `integer` rule tests against this. */
138
+ var INTEGER_PATTERN = /^-?\d+$/;
139
+ /** Matches an alphanumeric string (letters and digits only). The `alphanumeric` rule tests against this. */
140
+ var ALPHANUMERIC_PATTERN = /^[a-zA-Z0-9]+$/;
141
+ /**
142
+ * Each built-in validation rule's default error message — what the composed {@link
143
+ * import('./types.js').Validator} returns when that rule fails (the `minimum` / `maximum`
144
+ * messages are interpolated with the configured length at build time). Frozen; the source of
145
+ * truth for the rule-failure copy.
146
+ */
147
+ var RULE_MESSAGES = Object.freeze({
148
+ required: "This field is required",
149
+ minimum: "Must be at least {count} characters",
150
+ maximum: "Must be at most {count} characters",
151
+ pattern: "Must match pattern: {pattern}",
152
+ email: "Must be a valid email address",
153
+ url: "Must be a valid URL",
154
+ numeric: "Must be a numeric value",
155
+ integer: "Must be an integer",
156
+ alphanumeric: "Must contain only letters and digits",
157
+ invalid: "Invalid input"
158
+ });
159
+ /**
160
+ * The prompt-view icon glyphs the reducers render the prompt line and choice rows with. PLAIN
161
+ * glyphs — color is applied by the {@link import('./types.js').PromptState}'s
162
+ * {@link import('@orkestrel/console').StylerInterface} at render time, never baked into the
163
+ * constant (AGENTS — styling orthogonal to data; the rework fixes the terminal's ANSI-in-the-icon).
164
+ * Frozen.
165
+ *
166
+ * @remarks
167
+ * - `question` — the leading mark on a prompt's message line.
168
+ * - `pointer` — the cursor before the input / the focused choice row.
169
+ * - `dot` / `selected` — an unfocused / focused row marker in a select list.
170
+ * - `checked` / `unchecked` — a checked / unchecked box in a checkbox list.
171
+ */
172
+ var PROMPT_ICONS = Object.freeze({
173
+ question: "?",
174
+ pointer: "›",
175
+ dot: "○",
176
+ selected: "●",
177
+ checked: "☑",
178
+ unchecked: "☐"
179
+ });
180
+ /** How long (ms) the {@link import('./types.js').PromptInterface} broker parks an unanswered prompt before it expires — 5 minutes. */
181
+ var DEFAULT_PROMPT_TIMEOUT_MS = 3e5;
182
+ /** How long (ms) the {@link import('./types.js').PromptClientInterface} waits before each reconnect attempt — 2 seconds. */
183
+ var DEFAULT_RECONNECT_DELAY_MS = 2e3;
184
+ /**
185
+ * The SSE `event:` names the broker emits and the {@link import('./types.js').PromptClient}
186
+ * dispatches on. Frozen; the source of truth for the wire event vocabulary.
187
+ *
188
+ * @remarks
189
+ * - `pending` — a serialized {@link import('./types.js').PendingPrompt} to dispatch + answer.
190
+ * - `expire` — an `{ id }` payload: the broker expired a parked prompt (the client drops it).
191
+ * - `shutdown` — the broker is going away; the client disconnects (no auto-reconnect) but stays reusable.
192
+ */
193
+ var SSE_EVENTS = Object.freeze({
194
+ pending: "pending",
195
+ expire: "expire",
196
+ shutdown: "shutdown"
197
+ });
198
+ /** The auth-token request header the {@link import('./types.js').PromptClient} sends when a `token` is configured. */
199
+ var HEADER_TOKEN = "x-orkestrel-token";
200
+ /** The `Accept` header value that opens the broker's SSE stream. */
201
+ var ACCEPT_EVENT_STREAM = "text/event-stream";
202
+ /**
203
+ * The maximum number of characters the {@link import('./types.js').PromptClient} lets its
204
+ * SSE parser buffer before treating the stream as hostile — 1 MiB, comfortably above any
205
+ * legitimate prompt payload. Passed as the `limit` to `createSSEParser` so an unterminated
206
+ * or oversized `data:` field cannot grow the buffer without bound (a memory-exhaustion guard).
207
+ */
208
+ var SSE_BUFFER_LIMIT = 1048576;
209
+ //#endregion
210
+ //#region src/core/errors.ts
211
+ /**
212
+ * An error a {@link import('./Prompt.js').Prompt} broker rejects a parked prompt's Promise with.
213
+ *
214
+ * @remarks
215
+ * Carries a {@link TerminalErrorCode} and an optional `context` bag (the prompt `id`). Thrown —
216
+ * as a Promise rejection on the awaited prompt call — when a parked prompt is not answered before
217
+ * its `timeout`, or when the broker is `destroy`ed while the prompt is still `pending` (both
218
+ * `EXPIRE`); or when the user aborts an interactive server-`Terminal` prompt with ctrl-c
219
+ * (`CANCEL`). Narrow a caught value with {@link isTerminalError} and branch on `error.code`.
220
+ */
221
+ var TerminalError = class extends Error {
222
+ /** The machine-readable condition — see {@link TerminalErrorCode}. */
223
+ code;
224
+ /** An optional context bag naming the offending prompt id. */
225
+ context;
226
+ constructor(code, message, context) {
227
+ super(message);
228
+ this.name = "TerminalError";
229
+ this.code = code;
230
+ this.context = context;
231
+ }
232
+ };
233
+ /**
234
+ * Narrow an unknown caught value to a {@link TerminalError}.
235
+ *
236
+ * @param value - The value to test (typically a `catch` binding or a rejected prompt call)
237
+ * @returns `true` when `value` is a {@link TerminalError}
238
+ *
239
+ * @example
240
+ * ```ts
241
+ * try {
242
+ * const name = await prompt.input({ message: 'Your name' })
243
+ * } catch (error) {
244
+ * if (isTerminalError(error) && error.code === 'EXPIRE') retryLater()
245
+ * }
246
+ * ```
247
+ */
248
+ function isTerminalError(value) {
249
+ return value instanceof TerminalError;
250
+ }
251
+ //#endregion
252
+ //#region src/core/helpers.ts
253
+ /**
254
+ * Decode one keypress's bytes into a {@link KeyEvent} — total, never throws. A `Uint8Array` is
255
+ * read as UTF-8; the resulting string is matched against the known control bytes
256
+ * ({@link CONTROL_NAMES}) and escape sequences ({@link SEQUENCE_NAMES}), falling back to a
257
+ * single printable character. An unrecognized sequence yields `name: ''` with the raw `sequence`
258
+ * preserved.
259
+ *
260
+ * @remarks
261
+ * - **Single control byte.** A one-character control input (`return` / `backspace` / `tab` /
262
+ * `escape` / `space`, or a Ctrl combo `c` / `d` / `u` / `a` / `e`) is looked up in
263
+ * {@link CONTROL_NAMES}, carrying its `ctrl` flag.
264
+ * - **Escape sequence.** A multi-byte ESC sequence (`up` / `down` / `left` / `right` in BOTH the
265
+ * `ESC[A` and `ESCOA` forms, plus `home` / `end` / `delete`) is looked up in
266
+ * {@link SEQUENCE_NAMES} and flagged `meta`.
267
+ * - **Printable character.** A single printable character becomes `name` = that character, with
268
+ * `shift` set when it is an uppercase letter. A multi-code-point printable (an emoji, a pasted
269
+ * run) keeps its first code point as the name and the whole input as `sequence`.
270
+ * - **Unknown.** Anything else (an unrecognized escape, an empty input) yields `name: ''` —
271
+ * total, so the driver never crashes on a stray byte.
272
+ *
273
+ * @param input - The raw keypress bytes, as a string or `Uint8Array`
274
+ * @returns The decoded {@link KeyEvent}
275
+ *
276
+ * @example
277
+ * ```ts
278
+ * parseKey('\r') // { name: 'return', sequence: '\r', ctrl: false, meta: false, shift: false }
279
+ * parseKey('\x1b[A') // { name: 'up', sequence: '\x1b[A', ctrl: false, meta: true, shift: false }
280
+ * parseKey('A') // { name: 'A', sequence: 'A', ctrl: false, meta: false, shift: true }
281
+ * parseKey('\x03') // { name: 'c', sequence: '\x03', ctrl: true, meta: false, shift: false }
282
+ * ```
283
+ */
284
+ function parseKey(input) {
285
+ const sequence = isString(input) ? input : new TextDecoder().decode(input);
286
+ const sequenceName = SEQUENCE_NAMES[sequence];
287
+ if (sequenceName !== void 0) return {
288
+ name: sequenceName,
289
+ sequence,
290
+ ctrl: false,
291
+ meta: true,
292
+ shift: false
293
+ };
294
+ const control = CONTROL_NAMES[sequence];
295
+ if (control !== void 0) return {
296
+ name: control.name,
297
+ sequence,
298
+ ctrl: control.ctrl,
299
+ meta: false,
300
+ shift: false
301
+ };
302
+ const first = [...sequence][0];
303
+ if (first !== void 0 && isPrintable(first)) return {
304
+ name: first,
305
+ sequence,
306
+ ctrl: false,
307
+ meta: false,
308
+ shift: first !== first.toLowerCase()
309
+ };
310
+ return {
311
+ name: "",
312
+ sequence,
313
+ ctrl: false,
314
+ meta: false,
315
+ shift: false
316
+ };
317
+ }
318
+ /** Whether a single character is a printable (non-control) character — used by {@link parseKey}'s char fallback. */
319
+ function isPrintable(character) {
320
+ if (character.length === 0) return false;
321
+ const code = character.codePointAt(0);
322
+ if (code === void 0) return false;
323
+ return code >= 32 && code !== 127;
324
+ }
325
+ /**
326
+ * Evaluate ONE built-in validation rule against `input`, returning its error message when the
327
+ * rule fails or `undefined` when it passes. The atomic check {@link buildRuleValidator} wraps
328
+ * into a {@link Validator}. Pure.
329
+ *
330
+ * @remarks
331
+ * A function `check` is the custom-override path: it is called and its `true` ⇒ pass, a string
332
+ * ⇒ that message, anything else ⇒ the generic {@link RULE_MESSAGES.invalid}. A primitive `check`
333
+ * runs the named built-in: `required` (non-empty trimmed), `minimum` / `maximum` (length
334
+ * bounds, the message interpolated with the count), `pattern` (a regex source), and the
335
+ * `email` / `url` / `numeric` / `integer` / `alphanumeric` pattern tests.
336
+ *
337
+ * @param rule - The rule name (`'required'`, `'minimum'`, …)
338
+ * @param check - The configured value (a primitive toggle / bound / pattern, or a custom {@link Validator})
339
+ * @param input - The input string to test
340
+ * @returns The error message when the rule fails, else `undefined`
341
+ */
342
+ function evaluateRule(rule, check, input) {
343
+ if (typeof check === "function") {
344
+ const result = check(input);
345
+ if (result === true) return void 0;
346
+ return isString(result) ? result : RULE_MESSAGES.invalid;
347
+ }
348
+ switch (rule) {
349
+ case "required":
350
+ if (check === true && input.trim().length === 0) return RULE_MESSAGES.required;
351
+ break;
352
+ case "minimum":
353
+ if (isNumber(check) && [...input].length < check) return RULE_MESSAGES.minimum.replace("{count}", String(check));
354
+ break;
355
+ case "maximum":
356
+ if (isNumber(check) && [...input].length > check) return RULE_MESSAGES.maximum.replace("{count}", String(check));
357
+ break;
358
+ case "pattern":
359
+ if (isString(check)) {
360
+ let compiled;
361
+ try {
362
+ compiled = new RegExp(check);
363
+ } catch {
364
+ return RULE_MESSAGES.pattern.replace("{pattern}", check);
365
+ }
366
+ if (!compiled.test(input)) return RULE_MESSAGES.pattern.replace("{pattern}", check);
367
+ }
368
+ break;
369
+ case "email":
370
+ if (check === true && !EMAIL_PATTERN.test(input)) return RULE_MESSAGES.email;
371
+ break;
372
+ case "url":
373
+ if (check === true && !URL_PATTERN.test(input)) return RULE_MESSAGES.url;
374
+ break;
375
+ case "numeric":
376
+ if (check === true && !NUMERIC_PATTERN.test(input)) return RULE_MESSAGES.numeric;
377
+ break;
378
+ case "integer":
379
+ if (check === true && !INTEGER_PATTERN.test(input)) return RULE_MESSAGES.integer;
380
+ break;
381
+ case "alphanumeric":
382
+ if (check === true && !ALPHANUMERIC_PATTERN.test(input)) return RULE_MESSAGES.alphanumeric;
383
+ break;
384
+ }
385
+ }
386
+ /** Wrap a named rule + its primitive check into a {@link Validator} (returns `true` or the rule's message). */
387
+ function buildRuleValidator(rule, check) {
388
+ return (input) => evaluateRule(rule, check, input) ?? true;
389
+ }
390
+ /**
391
+ * Append a rule-backed {@link Validator} to `validators` when the rule is enabled — a `false` /
392
+ * `undefined` `check` is skipped, a function `check` is added verbatim (the custom override), and
393
+ * a primitive `check` is wrapped via {@link buildRuleValidator}. Mutates `validators` in place.
394
+ */
395
+ function appendRule(validators, rule, check) {
396
+ if (check === void 0 || check === false) return;
397
+ if (typeof check === "function") {
398
+ validators.push(check);
399
+ return;
400
+ }
401
+ validators.push(buildRuleValidator(rule, check));
402
+ }
403
+ /**
404
+ * Compose several {@link Validator}s into ONE short-circuiting validator — it runs them in order
405
+ * and returns the FIRST error message, or `true` when all pass. The empty composition always
406
+ * passes.
407
+ */
408
+ function composeValidators(...validators) {
409
+ return (input) => {
410
+ for (const validator of validators) {
411
+ const result = validator(input);
412
+ if (result !== true) return result;
413
+ }
414
+ return true;
415
+ };
416
+ }
417
+ /**
418
+ * Compile a {@link Validator} or declarative {@link ValidationRules} (or nothing) into ONE
419
+ * composed {@link Validator}. A bare validator passes through; rules are appended in the fixed
420
+ * order (required → minimum → maximum → pattern → email → url → numeric → integer → alphanumeric
421
+ * → custom) and composed; absent / empty input yields an always-passing validator. Pure.
422
+ *
423
+ * @remarks
424
+ * Unlike a prior variant (which returned `Validator | undefined`), this ALWAYS returns a
425
+ * `Validator` — an absent or empty rule set yields a validator that returns `true` for every
426
+ * input. That keeps a prompt's state unconditional (it always holds a real validator to apply on
427
+ * submit), with no `undefined` branch at the call site.
428
+ *
429
+ * @param validate - A custom {@link Validator}, a {@link ValidationRules} bag, or `undefined`
430
+ * @returns The composed {@link Validator} (always-passing when nothing was supplied)
431
+ *
432
+ * @example
433
+ * ```ts
434
+ * const v = resolveValidation({ required: true, minimum: 3 })
435
+ * v('') // 'This field is required'
436
+ * v('ab') // 'Must be at least 3 characters'
437
+ * v('abc') // true
438
+ * ```
439
+ */
440
+ function resolveValidation(validate) {
441
+ if (validate === void 0) return passing;
442
+ if (typeof validate === "function") return validate;
443
+ const validators = [];
444
+ appendRule(validators, "required", validate.required);
445
+ appendRule(validators, "minimum", validate.minimum);
446
+ appendRule(validators, "maximum", validate.maximum);
447
+ appendRule(validators, "pattern", validate.pattern);
448
+ appendRule(validators, "email", validate.email);
449
+ appendRule(validators, "url", validate.url);
450
+ appendRule(validators, "numeric", validate.numeric);
451
+ appendRule(validators, "integer", validate.integer);
452
+ appendRule(validators, "alphanumeric", validate.alphanumeric);
453
+ if (typeof validate.custom === "function") validators.push(validate.custom);
454
+ if (validators.length === 0) return passing;
455
+ return composeValidators(...validators);
456
+ }
457
+ /** The always-passing {@link Validator} — the resolved validator when no rules were supplied. */
458
+ function passing(_input) {
459
+ return true;
460
+ }
461
+ /** Normalize a select choice input into a full {@link PromptChoice} (a bare string becomes both name and value). */
462
+ function normalizeChoice(choice) {
463
+ return isString(choice) ? {
464
+ name: choice,
465
+ value: choice
466
+ } : choice;
467
+ }
468
+ /** Normalize a checkbox choice input into a full {@link CheckboxChoice} (a bare string becomes both name and value). */
469
+ function normalizeCheckboxChoice(choice) {
470
+ return isString(choice) ? {
471
+ name: choice,
472
+ value: choice
473
+ } : choice;
474
+ }
475
+ /** The styled prompt-message header (`? message`) — the leading line every active prompt view shares. */
476
+ function promptHeader(styler, message) {
477
+ return `${styler.cyan(PROMPT_ICONS.question)} ${styler.bold(message)}`;
478
+ }
479
+ /** The styled submit line (`✔ message`) — the committed header an interactive prompt shows once resolved. */
480
+ function submitHeader(styler, message) {
481
+ return `${styler.green(STATUS_ICONS.success)} ${styler.bold(message)}`;
482
+ }
483
+ /** The styled error line (`✖ message`) — appended beneath a prompt view when the last submit failed validation. */
484
+ function errorLine(styler, message) {
485
+ return `${styler.red(STATUS_ICONS.error)} ${styler.red(message)}`;
486
+ }
487
+ /** Build the initial {@link InputState} from {@link InputOptions} — resolving the validator + styler, seeding an empty value. */
488
+ function createInputState(options) {
489
+ return {
490
+ message: options.message,
491
+ default: options.default ?? "",
492
+ validator: resolveValidation(options.validate),
493
+ styler: options.styler ?? createStyler(),
494
+ value: ""
495
+ };
496
+ }
497
+ /** Render an {@link InputState} as a styled view — the header, the typed value (or dimmed default hint), and any error. */
498
+ function inputView(state) {
499
+ const shown = state.value.length > 0 ? state.value : state.styler.dim(state.default);
500
+ const head = `${promptHeader(state.styler, state.message)} ${state.styler.cyan(PROMPT_ICONS.pointer)} ${shown}`;
501
+ return state.error === void 0 ? head : `${head}\n${errorLine(state.styler, state.error)}`;
502
+ }
503
+ /**
504
+ * Advance an input prompt by one {@link KeyEvent} — the pure `(state, key) → PromptStep<string>`
505
+ * reducer. Printable characters extend the value; backspace shrinks it; ctrl-u clears it; ctrl-c
506
+ * cancels; return submits (the empty line falls back to the default) through the validator — an
507
+ * invalid submit stays active with the error in the view.
508
+ */
509
+ function inputReduce(state, key) {
510
+ if (key.ctrl && key.name === "c") return {
511
+ state,
512
+ view: inputView(state),
513
+ status: "cancel"
514
+ };
515
+ if (key.name === "return") {
516
+ const answer = state.value.length > 0 ? state.value : state.default;
517
+ const result = state.validator(answer);
518
+ if (result !== true) {
519
+ const next = {
520
+ ...state,
521
+ error: result
522
+ };
523
+ return {
524
+ state: next,
525
+ view: inputView(next),
526
+ status: "active"
527
+ };
528
+ }
529
+ return {
530
+ state: {
531
+ ...state,
532
+ value: answer,
533
+ error: void 0
534
+ },
535
+ view: `${submitHeader(state.styler, state.message)} ${state.styler.dim(answer)}`,
536
+ status: "submit",
537
+ value: answer
538
+ };
539
+ }
540
+ const value = editLine(state.value, key);
541
+ if (value === void 0) return {
542
+ state,
543
+ view: inputView(state),
544
+ status: "active"
545
+ };
546
+ const next = {
547
+ ...state,
548
+ value,
549
+ error: void 0
550
+ };
551
+ return {
552
+ state: next,
553
+ view: inputView(next),
554
+ status: "active"
555
+ };
556
+ }
557
+ /** Build the initial {@link PasswordState} from {@link PasswordOptions} — resolving the validator + styler + mask. */
558
+ function createPasswordState(options) {
559
+ return {
560
+ message: options.message,
561
+ mask: options.mask ?? "*",
562
+ validator: resolveValidation(options.validate),
563
+ styler: options.styler ?? createStyler(),
564
+ value: ""
565
+ };
566
+ }
567
+ /** Render a {@link PasswordState} as a styled view — the header, the value masked to `mask` repeated, and any error. */
568
+ function passwordView(state) {
569
+ const masked = state.mask.repeat(state.value.length);
570
+ const head = `${promptHeader(state.styler, state.message)} ${state.styler.cyan(PROMPT_ICONS.pointer)} ${masked}`;
571
+ return state.error === void 0 ? head : `${head}\n${errorLine(state.styler, state.error)}`;
572
+ }
573
+ /**
574
+ * Advance a password prompt by one {@link KeyEvent} — the pure `(state, key) → PromptStep<string>`
575
+ * reducer. Identical line-editing to {@link inputReduce} (printable extends, backspace shrinks,
576
+ * ctrl-u clears, ctrl-c cancels) but the view masks the value; return submits through the
577
+ * validator (no default fallback — a password has no echoed default).
578
+ */
579
+ function passwordReduce(state, key) {
580
+ if (key.ctrl && key.name === "c") return {
581
+ state,
582
+ view: passwordView(state),
583
+ status: "cancel"
584
+ };
585
+ if (key.name === "return") {
586
+ const result = state.validator(state.value);
587
+ if (result !== true) {
588
+ const next = {
589
+ ...state,
590
+ error: result
591
+ };
592
+ return {
593
+ state: next,
594
+ view: passwordView(next),
595
+ status: "active"
596
+ };
597
+ }
598
+ return {
599
+ state: {
600
+ ...state,
601
+ error: void 0
602
+ },
603
+ view: `${submitHeader(state.styler, state.message)} ${state.styler.dim(state.mask.repeat(state.value.length))}`,
604
+ status: "submit",
605
+ value: state.value
606
+ };
607
+ }
608
+ const value = editLine(state.value, key);
609
+ if (value === void 0) return {
610
+ state,
611
+ view: passwordView(state),
612
+ status: "active"
613
+ };
614
+ const next = {
615
+ ...state,
616
+ value,
617
+ error: void 0
618
+ };
619
+ return {
620
+ state: next,
621
+ view: passwordView(next),
622
+ status: "active"
623
+ };
624
+ }
625
+ /** Build the initial {@link ConfirmState} from {@link ConfirmOptions} — defaulting the answer to `false`. */
626
+ function createConfirmState(options) {
627
+ return {
628
+ message: options.message,
629
+ default: options.default ?? false,
630
+ styler: options.styler ?? createStyler()
631
+ };
632
+ }
633
+ /** Render a {@link ConfirmState} as a styled view — the header plus a `(Y/n)` hint with the default letter emphasized. */
634
+ function confirmView(state) {
635
+ const hint = state.default ? `${state.styler.green("Y")}${state.styler.dim("/n")}` : `${state.styler.dim("y/")}${state.styler.green("N")}`;
636
+ return `${promptHeader(state.styler, state.message)} ${state.styler.dim("(")}${hint}${state.styler.dim(")")}`;
637
+ }
638
+ /**
639
+ * Advance a confirm prompt by one {@link KeyEvent} — the pure `(state, key) → PromptStep<boolean>`
640
+ * reducer. `y` / `Y` submits `true`, `n` / `N` submits `false`, return on an empty line submits
641
+ * the `default`, ctrl-c cancels; any other key is ignored (stays active).
642
+ */
643
+ function confirmReduce(state, key) {
644
+ if (key.ctrl && key.name === "c") return {
645
+ state,
646
+ view: confirmView(state),
647
+ status: "cancel"
648
+ };
649
+ let answer;
650
+ const choice = key.name.toLowerCase();
651
+ if (key.name === "return") answer = state.default;
652
+ else if (choice === "y") answer = true;
653
+ else if (choice === "n") answer = false;
654
+ if (answer === void 0) return {
655
+ state,
656
+ view: confirmView(state),
657
+ status: "active"
658
+ };
659
+ return {
660
+ state,
661
+ view: `${submitHeader(state.styler, state.message)} ${state.styler.dim(answer ? "yes" : "no")}`,
662
+ status: "submit",
663
+ value: answer
664
+ };
665
+ }
666
+ /** Build the initial {@link SelectState} from {@link SelectOptions} — normalizing choices and pre-focusing the default. */
667
+ function createSelectState(options) {
668
+ const choices = options.choices.map(normalizeChoice);
669
+ const index = choices.findIndex((choice) => choice.value === options.default);
670
+ return {
671
+ message: options.message,
672
+ choices,
673
+ styler: options.styler ?? createStyler(),
674
+ focused: index >= 0 ? index : 0
675
+ };
676
+ }
677
+ /** Render a {@link SelectState} as a MULTI-LINE styled view — the header followed by one row per choice, the focused row marked. */
678
+ function selectView(state) {
679
+ const lines = state.choices.map((choice, index) => {
680
+ const active = index === state.focused;
681
+ return `${active ? state.styler.cyan(PROMPT_ICONS.pointer) : " "} ${active ? state.styler.green(PROMPT_ICONS.selected) : state.styler.dim(PROMPT_ICONS.dot)} ${active ? state.styler.bold(choice.name) : choice.name}${choice.description === void 0 ? "" : ` ${state.styler.dim(choice.description)}`}`;
682
+ });
683
+ return [promptHeader(state.styler, state.message), ...lines].join("\n");
684
+ }
685
+ /**
686
+ * Advance a select prompt by one {@link KeyEvent} — the pure `(state, key) → PromptStep<string>`
687
+ * reducer. `up` / `down` (and `k` / `j`) move the focus, WRAPPING at the ends; return submits the
688
+ * focused choice's `value`; ctrl-c cancels. An empty choice list can never submit (a higher layer
689
+ * guards against it); any other key is ignored.
690
+ */
691
+ function selectReduce(state, key) {
692
+ if (key.ctrl && key.name === "c") return {
693
+ state,
694
+ view: selectView(state),
695
+ status: "cancel"
696
+ };
697
+ const count = state.choices.length;
698
+ if (count === 0) return {
699
+ state,
700
+ view: selectView(state),
701
+ status: "active"
702
+ };
703
+ if (key.name === "up" || key.name === "k") {
704
+ const next = {
705
+ ...state,
706
+ focused: (state.focused - 1 + count) % count
707
+ };
708
+ return {
709
+ state: next,
710
+ view: selectView(next),
711
+ status: "active"
712
+ };
713
+ }
714
+ if (key.name === "down" || key.name === "j") {
715
+ const next = {
716
+ ...state,
717
+ focused: (state.focused + 1) % count
718
+ };
719
+ return {
720
+ state: next,
721
+ view: selectView(next),
722
+ status: "active"
723
+ };
724
+ }
725
+ if (key.name === "return") {
726
+ const choice = state.choices[state.focused];
727
+ const value = choice?.value ?? "";
728
+ return {
729
+ state,
730
+ view: `${submitHeader(state.styler, state.message)} ${state.styler.dim(choice?.name ?? "")}`,
731
+ status: "submit",
732
+ value
733
+ };
734
+ }
735
+ return {
736
+ state,
737
+ view: selectView(state),
738
+ status: "active"
739
+ };
740
+ }
741
+ /** Build the initial {@link CheckboxState} from {@link CheckboxOptions} — normalizing choices, seeding the checked set, carrying min/max. */
742
+ function createCheckboxState(options) {
743
+ const choices = options.choices.map(normalizeCheckboxChoice);
744
+ const checked = choices.reduce((indices, choice, index) => {
745
+ if (choice.checked === true) indices.push(index);
746
+ return indices;
747
+ }, []);
748
+ return {
749
+ message: options.message,
750
+ choices,
751
+ styler: options.styler ?? createStyler(),
752
+ focused: 0,
753
+ checked,
754
+ min: options.min,
755
+ max: options.max
756
+ };
757
+ }
758
+ /** Render a {@link CheckboxState} as a MULTI-LINE styled view — the header, one box per choice (focused + checked marked), a count, and any error. */
759
+ function checkboxView(state) {
760
+ const lines = state.choices.map((choice, index) => {
761
+ const active = index === state.focused;
762
+ const ticked = state.checked.includes(index);
763
+ return `${active ? state.styler.cyan(PROMPT_ICONS.pointer) : " "} ${ticked ? state.styler.green(PROMPT_ICONS.checked) : state.styler.dim(PROMPT_ICONS.unchecked)} ${active ? state.styler.bold(choice.name) : choice.name}${choice.description === void 0 ? "" : ` ${state.styler.dim(choice.description)}`}`;
764
+ });
765
+ const summary = state.styler.dim(`${state.checked.length} selected`);
766
+ const body = [
767
+ promptHeader(state.styler, state.message),
768
+ ...lines,
769
+ summary
770
+ ].join("\n");
771
+ return state.error === void 0 ? body : `${body}\n${errorLine(state.styler, state.error)}`;
772
+ }
773
+ /**
774
+ * Advance a checkbox prompt by one {@link KeyEvent} — the pure
775
+ * `(state, key) → PromptStep<readonly string[]>` reducer. `up` / `down` (and `k` / `j`) move the
776
+ * focus (wrapping); `space` toggles the focused index in the checked set; return submits the
777
+ * checked values in CHOICE order — gated by `min` / `max` (an out-of-range count stays active with
778
+ * the reason in the view); ctrl-c cancels.
779
+ */
780
+ function checkboxReduce(state, key) {
781
+ if (key.ctrl && key.name === "c") return {
782
+ state,
783
+ view: checkboxView(state),
784
+ status: "cancel"
785
+ };
786
+ const count = state.choices.length;
787
+ if ((key.name === "up" || key.name === "k") && count > 0) {
788
+ const next = {
789
+ ...state,
790
+ focused: (state.focused - 1 + count) % count,
791
+ error: void 0
792
+ };
793
+ return {
794
+ state: next,
795
+ view: checkboxView(next),
796
+ status: "active"
797
+ };
798
+ }
799
+ if ((key.name === "down" || key.name === "j") && count > 0) {
800
+ const next = {
801
+ ...state,
802
+ focused: (state.focused + 1) % count,
803
+ error: void 0
804
+ };
805
+ return {
806
+ state: next,
807
+ view: checkboxView(next),
808
+ status: "active"
809
+ };
810
+ }
811
+ if (key.name === "space" && count > 0) {
812
+ const checked = toggleIndex(state.checked, state.focused);
813
+ const next = {
814
+ ...state,
815
+ checked,
816
+ error: void 0
817
+ };
818
+ return {
819
+ state: next,
820
+ view: checkboxView(next),
821
+ status: "active"
822
+ };
823
+ }
824
+ if (key.name === "return") {
825
+ const error = gateSelection(state.checked.length, state.min, state.max);
826
+ if (error !== void 0) {
827
+ const next = {
828
+ ...state,
829
+ error
830
+ };
831
+ return {
832
+ state: next,
833
+ view: checkboxView(next),
834
+ status: "active"
835
+ };
836
+ }
837
+ const ordered = [...state.checked].sort((a, b) => a - b);
838
+ const values = ordered.map((index) => state.choices[index]?.value).filter((value) => value !== void 0);
839
+ const summary = ordered.map((index) => state.choices[index]?.name).filter((name) => name !== void 0).join(", ");
840
+ return {
841
+ state: {
842
+ ...state,
843
+ error: void 0
844
+ },
845
+ view: `${submitHeader(state.styler, state.message)} ${state.styler.dim(summary)}`,
846
+ status: "submit",
847
+ value: values
848
+ };
849
+ }
850
+ return {
851
+ state,
852
+ view: checkboxView(state),
853
+ status: "active"
854
+ };
855
+ }
856
+ /** Toggle `index` in a readonly index list — copy-on-write, returning the new sorted-by-insertion list. */
857
+ function toggleIndex(indices, index) {
858
+ return indices.includes(index) ? indices.filter((i) => i !== index) : [...indices, index];
859
+ }
860
+ /** The min/max gate for a checkbox submit — the rejection message when `count` is out of range, else `undefined`. */
861
+ function gateSelection(count, min, max) {
862
+ if (min !== void 0 && count < min) return `Select at least ${String(min)} option${min === 1 ? "" : "s"}`;
863
+ if (max !== void 0 && count > max) return `Select no more than ${String(max)} option${max === 1 ? "" : "s"}`;
864
+ }
865
+ /** Build the initial {@link EditorState} from {@link EditorOptions} — resolving the validator + styler, seeding empty lines. */
866
+ function createEditorState(options) {
867
+ return {
868
+ message: options.message,
869
+ default: options.default ?? "",
870
+ validator: resolveValidation(options.validate),
871
+ styler: options.styler ?? createStyler(),
872
+ lines: [],
873
+ current: ""
874
+ };
875
+ }
876
+ /** Render an {@link EditorState} as a MULTI-LINE styled view — the header (with a Ctrl+D hint), the committed lines, the in-progress line, and any error. */
877
+ function editorView(state) {
878
+ const view = [`${promptHeader(state.styler, state.message)} ${state.styler.dim("(Ctrl+D to finish)")}`, ...[...state.lines, `${state.styler.cyan(PROMPT_ICONS.pointer)} ${state.current}`]].join("\n");
879
+ return state.error === void 0 ? view : `${view}\n${errorLine(state.styler, state.error)}`;
880
+ }
881
+ /**
882
+ * Advance an editor prompt by one {@link KeyEvent} — the pure `(state, key) → PromptStep<string>`
883
+ * reducer. Printable characters extend the current line; backspace shrinks it; return commits the
884
+ * current line and starts a fresh one; ctrl-d FINISHES (joining all lines, falling back to the
885
+ * default when empty) through the validator; ctrl-c cancels. An invalid finish stays active with
886
+ * the error.
887
+ */
888
+ function editorReduce(state, key) {
889
+ if (key.ctrl && key.name === "c") return {
890
+ state,
891
+ view: editorView(state),
892
+ status: "cancel"
893
+ };
894
+ if (key.ctrl && key.name === "d") {
895
+ const lines = state.current.length > 0 ? [...state.lines, state.current] : state.lines;
896
+ const joined = lines.join("\n");
897
+ const answer = joined.length > 0 ? joined : state.default;
898
+ const result = state.validator(answer);
899
+ if (result !== true) {
900
+ const next = {
901
+ ...state,
902
+ error: result
903
+ };
904
+ return {
905
+ state: next,
906
+ view: editorView(next),
907
+ status: "active"
908
+ };
909
+ }
910
+ return {
911
+ state: {
912
+ ...state,
913
+ error: void 0
914
+ },
915
+ view: `${submitHeader(state.styler, state.message)} ${state.styler.dim(`${String(lines.length)} line${lines.length === 1 ? "" : "s"}`)}`,
916
+ status: "submit",
917
+ value: answer
918
+ };
919
+ }
920
+ if (key.name === "return") {
921
+ const next = {
922
+ ...state,
923
+ lines: [...state.lines, state.current],
924
+ current: "",
925
+ error: void 0
926
+ };
927
+ return {
928
+ state: next,
929
+ view: editorView(next),
930
+ status: "active"
931
+ };
932
+ }
933
+ const current = editLine(state.current, key);
934
+ if (current === void 0) return {
935
+ state,
936
+ view: editorView(state),
937
+ status: "active"
938
+ };
939
+ const next = {
940
+ ...state,
941
+ current,
942
+ error: void 0
943
+ };
944
+ return {
945
+ state: next,
946
+ view: editorView(next),
947
+ status: "active"
948
+ };
949
+ }
950
+ /**
951
+ * Apply a single line-editing {@link KeyEvent} to a text buffer — the editing shared by input /
952
+ * password / editor. A printable key appends its character; `backspace` drops the last character;
953
+ * `space` appends a space; ctrl-u clears the line. Returns the new buffer, or `undefined` when the
954
+ * key does not edit the line (so the caller can leave the state untouched).
955
+ */
956
+ function editLine(value, key) {
957
+ if (key.ctrl && key.name === "u") return "";
958
+ if (key.name === "backspace") return value.slice(0, -1);
959
+ if (key.name === "space") return `${value} `;
960
+ if (!key.ctrl && !key.meta && [...key.name].length === 1 && isPrintable(key.name)) return `${value}${key.sequence}`;
961
+ }
962
+ /**
963
+ * Produce the WIRE-SAFE form of a prompt's options — the {@link PendingPrompt.options} a broker
964
+ * serializes over SSE. Drops everything that cannot cross the wire (a `styler`, and any
965
+ * function-valued `validate` rule), while KEEPING the declarative data a remote client needs to
966
+ * reconstruct the prompt: the {@link ValidationRules} as data, plus `message` / `choices` /
967
+ * `default` / `mask` / `min` / `max`. This is WHY T-a's validation is rules-as-data: a `Validator`
968
+ * FUNCTION can't be serialized, but its declarative rules can — the client rebuilds the validator
969
+ * from them via {@link resolveValidation}.
970
+ *
971
+ * @remarks
972
+ * - **Functions are dropped.** A top-level function value (e.g. a bare-`Validator` `validate`) is
973
+ * omitted entirely; the `styler` (a fluent function-bearing object) is dropped by key.
974
+ * - **`validate` rules are flattened.** A {@link ValidationRules} bag is copied with each
975
+ * function rule replaced by `true` (the rule's INTENT survives as the built-in check; its
976
+ * custom function does not). A bare-function `validate` is dropped (no declarative form to keep).
977
+ * - **`choices` are function-stripped.** Each choice keeps its plain fields (`name` / `value` /
978
+ * `description` / `checked`); a bare string passes through.
979
+ *
980
+ * @param options - The raw prompt options bag (may hold functions / a styler)
981
+ * @returns A JSON-safe options record — only serializable, declarative data
982
+ */
983
+ function serializePromptOptions(options) {
984
+ const result = {};
985
+ for (const [key, value] of Object.entries(options)) {
986
+ if (key === "styler" || typeof value === "function") continue;
987
+ if (key === "validate") {
988
+ const rules = serializeValidationRules(value);
989
+ if (rules !== void 0) result[key] = rules;
990
+ continue;
991
+ }
992
+ if (key === "choices") {
993
+ result[key] = serializeChoices(value);
994
+ continue;
995
+ }
996
+ result[key] = value;
997
+ }
998
+ return result;
999
+ }
1000
+ /**
1001
+ * Flatten a `validate` option to its wire-safe {@link ValidationRules} DATA — a function rule
1002
+ * becomes `true` (its built-in check survives; its body cannot cross the wire). A bare-function
1003
+ * `validate` (no rules object) has no declarative form, so it yields `undefined` (dropped).
1004
+ */
1005
+ function serializeValidationRules(validate) {
1006
+ if (!isRecord(validate)) return void 0;
1007
+ const rules = {};
1008
+ for (const [rule, value] of Object.entries(validate)) rules[rule] = typeof value === "function" ? true : value;
1009
+ return rules;
1010
+ }
1011
+ /** Strip functions from a `choices` option — each choice keeps its plain fields; a bare string passes through. */
1012
+ function serializeChoices(choices) {
1013
+ if (!Array.isArray(choices)) return [];
1014
+ return choices.map((choice) => {
1015
+ if (isString(choice)) return choice;
1016
+ if (!isRecord(choice)) return choice;
1017
+ const normalized = {};
1018
+ for (const [key, value] of Object.entries(choice)) if (typeof value !== "function") normalized[key] = value;
1019
+ return normalized;
1020
+ });
1021
+ }
1022
+ /** Narrow an unknown value to a {@link PromptType} — one of the six prompt forms. */
1023
+ var isPromptType = literalOf("input", "password", "confirm", "select", "checkbox", "editor");
1024
+ /** Narrow an unknown value to a {@link PendingPromptStatus}. */
1025
+ var isPendingPromptStatus = literalOf("pending", "answered", "expired");
1026
+ /**
1027
+ * Narrow an unknown wire value to a {@link PendingPrompt} — the §14 guard a {@link PromptClient}
1028
+ * applies to each decoded SSE `pending` payload before dispatching it (never an `as`).
1029
+ */
1030
+ var isPendingPrompt = recordOf({
1031
+ id: isNonEmptyString,
1032
+ form: isPromptType,
1033
+ message: isString,
1034
+ options: isRecord,
1035
+ status: isPendingPromptStatus,
1036
+ time: isNumber
1037
+ });
1038
+ /**
1039
+ * Rebuild a wire-decoded `validate` payload into a {@link ValidationRules} bag — the inverse of
1040
+ * {@link serializeValidationRules}. Keeps only the primitive rule values (`boolean` / `number` /
1041
+ * `string`) a serialized prompt could carry; an empty / non-record / all-dropped payload yields
1042
+ * `undefined` (no rules to apply). The client feeds the result back through {@link resolveValidation}.
1043
+ */
1044
+ function reconstructValidationRules(value) {
1045
+ if (!isRecord(value)) return void 0;
1046
+ const rules = {};
1047
+ let count = 0;
1048
+ for (const [rule, item] of Object.entries(value)) {
1049
+ if (rule === "pattern") continue;
1050
+ if (isBoolean(item) || isNumber(item) || isString(item)) {
1051
+ rules[rule] = item;
1052
+ count += 1;
1053
+ }
1054
+ }
1055
+ if (count === 0) return void 0;
1056
+ return rules;
1057
+ }
1058
+ /** Read one option by key, narrowed by `guard` — `undefined` when absent or off-shape (§14, never an `as`). */
1059
+ function resolveOption(options, key, guard) {
1060
+ const value = options[key];
1061
+ return guard(value) ? value : void 0;
1062
+ }
1063
+ /** Narrow an unknown value to a {@link PromptChoice} — the `recordOf` shape inlined so no non-exported member lingers (§5). */
1064
+ function isPromptChoice(value) {
1065
+ return recordOf({
1066
+ name: isString,
1067
+ value: isString,
1068
+ description: isString
1069
+ }, ["description"])(value);
1070
+ }
1071
+ /** Narrow an unknown value to a {@link CheckboxChoice} — the `recordOf` shape inlined so no non-exported member lingers (§5). */
1072
+ function isCheckboxChoice(value) {
1073
+ return recordOf({
1074
+ name: isString,
1075
+ value: isString,
1076
+ description: isString,
1077
+ checked: isBoolean
1078
+ }, ["description", "checked"])(value);
1079
+ }
1080
+ /** Read a `choices` option as a list of bare strings / full choices — each element narrowed by `guard`, off-shape elements stringified. */
1081
+ function resolveChoices(options, guard) {
1082
+ const choices = options.choices;
1083
+ if (!Array.isArray(choices)) return [];
1084
+ return choices.map((choice) => {
1085
+ if (isString(choice)) return choice;
1086
+ if (guard(choice)) return choice;
1087
+ return String(choice);
1088
+ });
1089
+ }
1090
+ /**
1091
+ * Sanitize a list of resolved choices' human-readable labels (`name` + `description`) with
1092
+ * {@link stripControls} — shared by the `select` and `checkbox` branches of
1093
+ * {@link dispatchPendingPrompt} so a remote-supplied choice can never inject raw control bytes
1094
+ * into the local terminal's rendered view.
1095
+ *
1096
+ * @param choices - The resolved choices (bare strings or full {@link PromptChoice} /
1097
+ * {@link CheckboxChoice} objects) as returned by {@link resolveChoices}
1098
+ * @returns The same choices with every `name` / `description` control-stripped
1099
+ */
1100
+ function sanitizeChoiceLabels(choices) {
1101
+ return choices.map((choice) => {
1102
+ if (isString(choice)) return stripControls(choice);
1103
+ const description = choice.description === void 0 ? void 0 : stripControls(choice.description);
1104
+ return {
1105
+ ...choice,
1106
+ name: stripControls(choice.name),
1107
+ description
1108
+ };
1109
+ });
1110
+ }
1111
+ /**
1112
+ * Dispatch a {@link PendingPrompt} to the matching {@link PromptFormInterface} method — the bridge
1113
+ * step a {@link PromptClient} runs to drive a LOCAL terminal with a prompt issued elsewhere.
1114
+ * Reconstructs typed options from the wire-safe {@link PendingPrompt.options} (every field
1115
+ * §14-narrowed, never an `as`; the validator rebuilt from rules via {@link reconstructValidationRules}),
1116
+ * then calls the matching prompt form and returns its resolved value.
1117
+ *
1118
+ * @param terminal - The local {@link PromptFormInterface} to drive
1119
+ * @param pending - The decoded pending prompt to dispatch
1120
+ * @returns The prompt's resolved value (a `string` / `boolean` / `readonly string[]` per form)
1121
+ */
1122
+ function dispatchPendingPrompt(terminal, pending) {
1123
+ const options = pending.options;
1124
+ const validate = reconstructValidationRules(options.validate);
1125
+ const message = stripControls(pending.message);
1126
+ switch (pending.form) {
1127
+ case "input": {
1128
+ const value = resolveOption(options, "default", isString);
1129
+ return terminal.input({
1130
+ message,
1131
+ default: value === void 0 ? void 0 : stripControls(value),
1132
+ validate
1133
+ });
1134
+ }
1135
+ case "password": {
1136
+ const value = resolveOption(options, "mask", isString);
1137
+ return terminal.password({
1138
+ message,
1139
+ mask: value === void 0 ? void 0 : stripControls(value),
1140
+ validate
1141
+ });
1142
+ }
1143
+ case "confirm": return terminal.confirm({
1144
+ message,
1145
+ default: resolveOption(options, "default", isBoolean)
1146
+ });
1147
+ case "select": {
1148
+ const value = resolveOption(options, "default", isString);
1149
+ return terminal.select({
1150
+ message,
1151
+ choices: sanitizeChoiceLabels(resolveChoices(options, isPromptChoice)),
1152
+ default: value === void 0 ? void 0 : stripControls(value)
1153
+ });
1154
+ }
1155
+ case "checkbox": return terminal.checkbox({
1156
+ message,
1157
+ choices: sanitizeChoiceLabels(resolveChoices(options, isCheckboxChoice)),
1158
+ min: resolveOption(options, "min", isNumber),
1159
+ max: resolveOption(options, "max", isNumber)
1160
+ });
1161
+ case "editor": {
1162
+ const value = resolveOption(options, "default", isString);
1163
+ return terminal.editor({
1164
+ message,
1165
+ default: value === void 0 ? void 0 : stripControls(value),
1166
+ validate
1167
+ });
1168
+ }
1169
+ }
1170
+ }
1171
+ /**
1172
+ * The default {@link import('./types.js').TimerHandler} — a thin host `setTimeout` / `clearTimeout`
1173
+ * wrapper that arms `callback` after `ms` and returns a {@link TimerCancel}. The deadline seam
1174
+ * behind both the {@link import('./Prompt.js').Prompt} broker (its expiry) and the
1175
+ * {@link import('./PromptClient.js').PromptClient} (its reconnect backoff); a test injects a
1176
+ * deterministic timer instead, so neither entity touches real time.
1177
+ */
1178
+ function defaultTimer(callback, ms) {
1179
+ const handle = setTimeout(callback, ms);
1180
+ return () => clearTimeout(handle);
1181
+ }
1182
+ /** The default {@link import('./types.js').FetchHandler} — the global `fetch`, adapted to the minimal injected shape the {@link import('./PromptClient.js').PromptClient} uses. */
1183
+ function globalFetch(input, init) {
1184
+ return fetch(input, init);
1185
+ }
1186
+ /**
1187
+ * Whether a caught value is an `AbortError` — the {@link import('./PromptClient.js').PromptClient}
1188
+ * distinguishes a deliberate `disconnect` / teardown (an aborted `fetch`) from a real fault, so it
1189
+ * exits its connect loop quietly instead of emitting `error` / reconnecting.
1190
+ */
1191
+ function isAbortError(error) {
1192
+ return (error instanceof DOMException || error instanceof Error) && error.name === "AbortError";
1193
+ }
1194
+ /**
1195
+ * Parse a JSON wire string TOTAL — a malformed / empty payload yields `undefined` (the caller's
1196
+ * guard then rejects it), never a throw. The {@link import('./PromptClient.js').PromptClient}
1197
+ * decodes every SSE `data` field through this before §14-narrowing it.
1198
+ */
1199
+ function parseWireJSON(text) {
1200
+ if (text.length === 0) return void 0;
1201
+ try {
1202
+ return JSON.parse(text);
1203
+ } catch {
1204
+ return;
1205
+ }
1206
+ }
1207
+ /**
1208
+ * Whether `url` is an INSECURE remote endpoint — a plain `http://` URL whose host is NOT a
1209
+ * loopback address. Pure string parsing (no `URL` global), so it stays total on malformed input.
1210
+ *
1211
+ * @remarks
1212
+ * A loopback host (`localhost`, `127.0.0.1`, `[::1]`) over `http://` is exempt (local
1213
+ * development has no network hop to eavesdrop on); every other `http://` host is insecure.
1214
+ * An `https://` URL (or any non-`http://` scheme) is never flagged.
1215
+ *
1216
+ * @param url - The candidate endpoint URL
1217
+ * @returns `true` when `url` is a non-loopback `http://` endpoint
1218
+ *
1219
+ * @example
1220
+ * ```ts
1221
+ * isInsecureRemote('http://example.com') // true
1222
+ * isInsecureRemote('http://localhost:3000') // false
1223
+ * isInsecureRemote('https://example.com') // false
1224
+ * ```
1225
+ */
1226
+ function isInsecureRemote(url) {
1227
+ if (!url.startsWith("http://")) return false;
1228
+ const rest = url.slice(7);
1229
+ const hostEnd = rest.search(/[/?#]/);
1230
+ const authority = hostEnd === -1 ? rest : rest.slice(0, hostEnd);
1231
+ const host = authority.includes("@") ? authority.slice(authority.indexOf("@") + 1) : authority;
1232
+ const hostname = host.startsWith("[") ? host.slice(0, host.indexOf("]") + 1) : host.split(":")[0] ?? "";
1233
+ return hostname !== "localhost" && hostname !== "127.0.0.1" && hostname !== "[::1]";
1234
+ }
1235
+ //#endregion
1236
+ //#region src/core/Prompt.ts
1237
+ /**
1238
+ * The headless prompt BROKER (observable §13) — parks each {@link PromptInterface} call as a
1239
+ * pending prompt and returns a Promise that resolves when the prompt is {@link answer}ed (or
1240
+ * rejects on timeout / teardown). The tri-surface's headless arm: there is no terminal here, so a
1241
+ * transport forwards each `pending` event to whoever can answer, and {@link answer} resolves the
1242
+ * parked Promise — for environments where direct user access is unavailable (a headless server on
1243
+ * stdio, a browser with no TTY, a prompt issued on one machine answered on another).
1244
+ *
1245
+ * @remarks
1246
+ * - **Park-as-Promise.** Each `input` / `password` / `confirm` / `select` / `checkbox` / `editor`
1247
+ * mints an id (`crypto.randomUUID()`), parks a wire-safe {@link PendingPrompt}, emits `pending`,
1248
+ * and returns an unresolved Promise. The prompt's options are serialized
1249
+ * ({@link serializePromptOptions}) so a transport can forward them as-is.
1250
+ * - **Answer validates + type-checks.** {@link answer} runs the prompt's per-form gate: it
1251
+ * type-checks `value` to the form (string / boolean / string[]) AND, for the text forms, runs
1252
+ * the validator resolved from the original `validate` rules. A rejected answer returns `false`
1253
+ * and the prompt stays `pending`; an accepted answer resolves the Promise, emits `answer`, and
1254
+ * removes the prompt.
1255
+ * - **Timeout → expire → reject.** An unanswered prompt expires after `timeout` ms (via the
1256
+ * INJECTED timer): `expire` fires and the parked Promise rejects with a {@link TerminalError}
1257
+ * (`code: 'EXPIRE'`). {@link destroy} expires every still-pending prompt the same way.
1258
+ * - **Deterministic.** The timer is injectable ({@link import('./types.js').TimerHandler}); a test
1259
+ * supplies one that fires on demand, so expiry is driven without real time.
1260
+ *
1261
+ * @example
1262
+ * ```ts
1263
+ * const prompt = createPrompt({ timeout: 60_000 })
1264
+ * prompt.emitter.on('pending', (pending) => broadcast(pending)) // forward to a client
1265
+ *
1266
+ * const name = await prompt.input({ message: 'Your name' }) // parks; resolves on answer()
1267
+ * // ...elsewhere, a client POSTs the answer back:
1268
+ * prompt.answer(id, 'Ada') // resolves the awaited input() above
1269
+ * ```
1270
+ */
1271
+ var Prompt = class {
1272
+ #timeout;
1273
+ #timer;
1274
+ #parked = /* @__PURE__ */ new Map();
1275
+ #emitter;
1276
+ #destroyed = false;
1277
+ constructor(options) {
1278
+ this.#timeout = options?.timeout ?? 3e5;
1279
+ this.#timer = options?.timer ?? defaultTimer;
1280
+ this.#emitter = new Emitter({
1281
+ on: options?.on,
1282
+ error: options?.error
1283
+ });
1284
+ }
1285
+ get emitter() {
1286
+ return this.#emitter;
1287
+ }
1288
+ get count() {
1289
+ return this.#parked.size;
1290
+ }
1291
+ pending(id) {
1292
+ if (id !== void 0) return this.#parked.get(id)?.prompt;
1293
+ const result = [];
1294
+ for (const parked of this.#parked.values()) result.push(parked.prompt);
1295
+ return result;
1296
+ }
1297
+ answer(id, value) {
1298
+ const parked = this.#parked.get(id);
1299
+ if (parked === void 0) return false;
1300
+ const accepted = parked.respond(value);
1301
+ if (accepted === void 0) return false;
1302
+ parked.cancel();
1303
+ this.#emitter.emit("answer", id, accepted);
1304
+ this.#parked.delete(id);
1305
+ return true;
1306
+ }
1307
+ input(options) {
1308
+ const validator = resolveValidation(options.validate);
1309
+ return this.#park("input", options.message, options, (value) => isString(value) && validator(value) === true ? value : void 0);
1310
+ }
1311
+ password(options) {
1312
+ const validator = resolveValidation(options.validate);
1313
+ return this.#park("password", options.message, options, (value) => isString(value) && validator(value) === true ? value : void 0);
1314
+ }
1315
+ confirm(options) {
1316
+ return this.#park("confirm", options.message, options, (value) => isBoolean(value) ? value : void 0);
1317
+ }
1318
+ select(options) {
1319
+ const values = new Set(options.choices.map(normalizeChoice).map((choice) => choice.value));
1320
+ return this.#park("select", options.message, options, (value) => isString(value) && values.has(value) ? value : void 0);
1321
+ }
1322
+ checkbox(options) {
1323
+ const values = new Set(options.choices.map(normalizeCheckboxChoice).map((choice) => choice.value));
1324
+ const { min, max } = options;
1325
+ return this.#park("checkbox", options.message, options, (value) => {
1326
+ if (!isArray(value) || !value.every(isString)) return void 0;
1327
+ if (!value.every((item) => values.has(item))) return void 0;
1328
+ if (min !== void 0 && value.length < min) return void 0;
1329
+ if (max !== void 0 && value.length > max) return void 0;
1330
+ return value;
1331
+ });
1332
+ }
1333
+ editor(options) {
1334
+ const validator = resolveValidation(options.validate);
1335
+ return this.#park("editor", options.message, options, (value) => isString(value) && validator(value) === true ? value : void 0);
1336
+ }
1337
+ destroy() {
1338
+ if (this.#destroyed) return;
1339
+ this.#destroyed = true;
1340
+ for (const id of [...this.#parked.keys()]) this.#expire(id);
1341
+ this.#parked.clear();
1342
+ this.#emitter.destroy();
1343
+ }
1344
+ #park(form, message, options, gate) {
1345
+ if (this.#destroyed) return Promise.reject(new TerminalError("EXPIRE", "broker destroyed"));
1346
+ const id = crypto.randomUUID();
1347
+ const prompt = {
1348
+ id,
1349
+ form,
1350
+ message,
1351
+ options: serializePromptOptions(options),
1352
+ status: "pending",
1353
+ time: Date.now()
1354
+ };
1355
+ return new Promise((resolve, reject) => {
1356
+ const cancel = this.#timer(() => this.#expire(id), this.#timeout);
1357
+ const respond = (value) => {
1358
+ const accepted = gate(value);
1359
+ if (accepted === void 0) return void 0;
1360
+ const current = this.#parked.get(id);
1361
+ if (current !== void 0) this.#parked.set(id, {
1362
+ ...current,
1363
+ prompt: {
1364
+ ...current.prompt,
1365
+ status: "answered"
1366
+ }
1367
+ });
1368
+ resolve(accepted);
1369
+ return accepted;
1370
+ };
1371
+ const expire = () => {
1372
+ reject(new TerminalError("EXPIRE", `prompt ${id} expired`, { id }));
1373
+ };
1374
+ this.#parked.set(id, {
1375
+ prompt,
1376
+ respond,
1377
+ expire,
1378
+ cancel
1379
+ });
1380
+ this.#emitter.emit("pending", prompt);
1381
+ });
1382
+ }
1383
+ #expire(id) {
1384
+ const parked = this.#parked.get(id);
1385
+ if (parked === void 0) return;
1386
+ parked.cancel();
1387
+ this.#parked.set(id, {
1388
+ ...parked,
1389
+ prompt: {
1390
+ ...parked.prompt,
1391
+ status: "expired"
1392
+ }
1393
+ });
1394
+ this.#emitter.emit("expire", id);
1395
+ this.#parked.delete(id);
1396
+ parked.expire();
1397
+ }
1398
+ };
1399
+ //#endregion
1400
+ //#region src/core/PromptClient.ts
1401
+ /**
1402
+ * The SSE prompt BRIDGE (observable §13) — the client-side counterpart to
1403
+ * {@link import('./Prompt.js').Prompt}. Connects to a remote broker's SSE endpoint, receives
1404
+ * serialized pending prompts, dispatches EACH to a LOCAL {@link PromptFormInterface} terminal (so
1405
+ * a human at THIS machine answers a prompt parked elsewhere), and POSTs the answer back.
1406
+ * Universal — `fetch` + SSE are web-standard, so it runs in a browser or on a server; the
1407
+ * injected `fetch` / timer make it fully deterministic in tests.
1408
+ *
1409
+ * @remarks
1410
+ * - **Connect + reconnect.** {@link connect} opens the SSE stream (via the injected `fetch` + the
1411
+ * core `SSEParser`) and loops, reconnecting after the stream drops with the `delay` backoff
1412
+ * (driven by the injected timer) — unless `reconnect` is `false`, the client was
1413
+ * {@link destroy}ed, or the drop was a deliberate {@link disconnect} (an abort).
1414
+ * - **Dispatch + answer.** Each decoded `pending` event is narrowed to a `PendingPrompt` (§14 —
1415
+ * never an `as`), dispatched to `terminal`, and its resolved value POSTed back to `url`.
1416
+ * Dispatch is strictly SERIAL: the read loop drives the terminal for ONE prompt at a time and
1417
+ * only reads/dispatches the next event after the current prompt fully settles (its answer
1418
+ * POSTed). A prompt id redelivered by the broker AFTER its prior dispatch has settled is
1419
+ * dispatched again — the client does not dedupe across completion.
1420
+ * - **Server signals.** An `expire` event (the broker dropped a parked prompt) emits `expire`; a
1421
+ * `shutdown` event calls {@link disconnect} (not {@link destroy}) — the client stops streaming
1422
+ * without auto-reconnect but stays reusable; a later {@link connect} recovers it.
1423
+ * - **Lean events (§13).** `connect` / `disconnect` / `expire` / `error` — errors are `unknown`.
1424
+ * `disconnect` fires exactly once per connected-to-disconnected transition, whether triggered by
1425
+ * the server ending the stream cleanly or by a deliberate {@link disconnect} / {@link destroy}.
1426
+ *
1427
+ * @example
1428
+ * ```ts
1429
+ * const client = createPromptClient({
1430
+ * url: 'http://localhost:3001/prompts',
1431
+ * terminal: createTerminal(), // a local PromptFormInterface (T-c)
1432
+ * on: { connect: () => log('connected') },
1433
+ * })
1434
+ * await client.connect() // streams remote prompts to the local terminal, POSTs answers back
1435
+ * ```
1436
+ */
1437
+ var PromptClient = class {
1438
+ url;
1439
+ #terminal;
1440
+ #token;
1441
+ #reconnect;
1442
+ #delay;
1443
+ #fetch;
1444
+ #timer;
1445
+ #emitter;
1446
+ #controller;
1447
+ #backoff;
1448
+ #wake;
1449
+ #connecting = false;
1450
+ #connected = false;
1451
+ #destroyed = false;
1452
+ #warnedInsecureToken = false;
1453
+ constructor(options) {
1454
+ this.url = options.url;
1455
+ this.#terminal = options.terminal;
1456
+ this.#token = options.token;
1457
+ this.#reconnect = options.reconnect ?? true;
1458
+ this.#delay = options.delay ?? 2e3;
1459
+ this.#fetch = options.fetch ?? globalFetch;
1460
+ this.#timer = options.timer ?? defaultTimer;
1461
+ this.#emitter = new Emitter({
1462
+ on: options.on,
1463
+ error: options.error
1464
+ });
1465
+ }
1466
+ get emitter() {
1467
+ return this.#emitter;
1468
+ }
1469
+ get connected() {
1470
+ return this.#connected;
1471
+ }
1472
+ async connect() {
1473
+ if (this.#destroyed) return;
1474
+ if (this.#connecting) return;
1475
+ this.#connecting = true;
1476
+ while (this.#connecting && !this.#destroyed) {
1477
+ try {
1478
+ await this.#stream();
1479
+ } catch (error) {
1480
+ this.#markDisconnected();
1481
+ if (this.#destroyed || isAbortError(error)) return;
1482
+ this.#emitter.emit("error", error);
1483
+ }
1484
+ if (!this.#reconnect || !this.#connecting || this.#destroyed) return;
1485
+ await this.#wait(this.#delay);
1486
+ }
1487
+ }
1488
+ disconnect() {
1489
+ this.#connecting = false;
1490
+ this.#controller?.abort();
1491
+ this.#controller = void 0;
1492
+ this.#backoff?.();
1493
+ this.#backoff = void 0;
1494
+ const wake = this.#wake;
1495
+ this.#wake = void 0;
1496
+ wake?.();
1497
+ this.#markDisconnected();
1498
+ }
1499
+ destroy() {
1500
+ if (this.#destroyed) return;
1501
+ this.#destroyed = true;
1502
+ this.disconnect();
1503
+ this.#emitter.destroy();
1504
+ }
1505
+ async #stream() {
1506
+ if (this.#token !== void 0 && isInsecureRemote(this.url) && !this.#warnedInsecureToken) {
1507
+ this.#warnedInsecureToken = true;
1508
+ this.#emitter.emit("error", /* @__PURE__ */ new Error("auth token sent as cleartext over insecure http; use https"));
1509
+ }
1510
+ const controller = new AbortController();
1511
+ this.#controller = controller;
1512
+ const response = await this.#fetch(this.url, {
1513
+ headers: this.#headers({ Accept: ACCEPT_EVENT_STREAM }),
1514
+ signal: controller.signal
1515
+ });
1516
+ if (!response.ok) throw new Error(`broker returned ${String(response.status)}`);
1517
+ const body = response.body;
1518
+ if (body === null) throw new Error("broker sent no stream");
1519
+ this.#connected = true;
1520
+ this.#emitter.emit("connect");
1521
+ const reader = body.getReader();
1522
+ const decoder = new TextDecoder();
1523
+ const parser = createSSEParser({ limit: SSE_BUFFER_LIMIT });
1524
+ try {
1525
+ for (;;) {
1526
+ const { done, value } = await reader.read();
1527
+ if (done) break;
1528
+ for (const event of parser.parse(decoder.decode(value, { stream: true }))) await this.#handle(event);
1529
+ }
1530
+ } finally {
1531
+ reader.releaseLock();
1532
+ }
1533
+ this.#markDisconnected();
1534
+ }
1535
+ async #handle(event) {
1536
+ if (event.event === SSE_EVENTS.pending) {
1537
+ const parsed = parseWireJSON(event.data);
1538
+ if (isPendingPrompt(parsed)) await this.#dispatch(parsed.id, parsed);
1539
+ return;
1540
+ }
1541
+ if (event.event === SSE_EVENTS.expire) {
1542
+ const parsed = parseWireJSON(event.data);
1543
+ if (isRecord(parsed) && isString(parsed.id)) this.#emitter.emit("expire", parsed.id);
1544
+ return;
1545
+ }
1546
+ if (event.event === SSE_EVENTS.shutdown) this.disconnect();
1547
+ }
1548
+ async #dispatch(id, pending) {
1549
+ try {
1550
+ const value = await dispatchPendingPrompt(this.#terminal, pending);
1551
+ await this.#post(id, value);
1552
+ } catch (error) {
1553
+ this.#emitter.emit("error", error);
1554
+ }
1555
+ }
1556
+ async #post(id, value) {
1557
+ if (!(await this.#fetch(this.url, {
1558
+ method: "POST",
1559
+ headers: this.#headers({ "Content-Type": "application/json" }),
1560
+ body: JSON.stringify({
1561
+ id,
1562
+ value
1563
+ })
1564
+ })).ok) this.#emitter.emit("error", /* @__PURE__ */ new Error(`broker rejected answer ${id}`));
1565
+ }
1566
+ #markDisconnected() {
1567
+ if (!this.#connected) return;
1568
+ this.#connected = false;
1569
+ this.#emitter.emit("disconnect");
1570
+ }
1571
+ #headers(base) {
1572
+ if (this.#token !== void 0) base[HEADER_TOKEN] = this.#token;
1573
+ return base;
1574
+ }
1575
+ #wait(ms) {
1576
+ return new Promise((resolve) => {
1577
+ const settle = () => {
1578
+ this.#backoff = void 0;
1579
+ this.#wake = void 0;
1580
+ resolve();
1581
+ };
1582
+ this.#wake = settle;
1583
+ this.#backoff = this.#timer(() => {
1584
+ settle();
1585
+ }, ms);
1586
+ });
1587
+ }
1588
+ };
1589
+ //#endregion
1590
+ //#region src/core/factories.ts
1591
+ /**
1592
+ * Create the headless prompt {@link PromptInterface} BROKER — it parks each prompt call as a
1593
+ * pending prompt and resolves it when {@link PromptInterface.answer} arrives (or rejects on
1594
+ * timeout). The tri-surface's headless arm: subscribe `emitter.on('pending', …)` to forward each
1595
+ * prompt to whoever can answer (an SSE transport, a remote terminal), then route the answer back
1596
+ * through `answer(id, value)`.
1597
+ *
1598
+ * @param options - See {@link PromptOptions}
1599
+ * @returns A {@link PromptInterface}
1600
+ *
1601
+ * @remarks
1602
+ * - **Park-as-Promise.** `await prompt.input({ message })` blocks until `answer(id, value)` accepts
1603
+ * a matching value; the value is validated (text forms) and type-checked to the form first.
1604
+ * - **Timeout → expire → reject (deterministic).** An unanswered prompt expires after
1605
+ * `options.timeout` (default {@link import('./constants.js').DEFAULT_PROMPT_TIMEOUT_MS}) and its
1606
+ * Promise rejects with a {@link import('./errors.js').TerminalError}; inject `options.timer` to
1607
+ * drive expiry without real time.
1608
+ *
1609
+ * @example
1610
+ * ```ts
1611
+ * import { createPrompt } from '@src/core'
1612
+ *
1613
+ * const prompt = createPrompt()
1614
+ * prompt.emitter.on('pending', (pending) => send(pending)) // forward to a remote client
1615
+ * const name = await prompt.input({ message: 'Your name', validate: { required: true } })
1616
+ * ```
1617
+ */
1618
+ function createPrompt(options) {
1619
+ return new Prompt(options);
1620
+ }
1621
+ /**
1622
+ * Create the SSE prompt {@link PromptClientInterface} BRIDGE — it connects to a remote broker's SSE
1623
+ * endpoint, dispatches each received prompt to a LOCAL {@link import('./types.js').PromptFormInterface}
1624
+ * terminal (so a human at this machine answers a prompt parked elsewhere), and POSTs the answer
1625
+ * back. Universal — `fetch` / SSE are web-standard.
1626
+ *
1627
+ * @param options - See {@link PromptClientOptions} (`url` + `terminal` required)
1628
+ * @returns A {@link PromptClientInterface}
1629
+ *
1630
+ * @remarks
1631
+ * - **Connect + reconnect.** `await client.connect()` streams remote prompts until the stream
1632
+ * ends; it reconnects with the `delay` backoff unless `reconnect` is `false` / the client was
1633
+ * `destroy`ed. Inject `options.fetch` (a scripted `fetch`) and `options.timer` to drive it
1634
+ * deterministically in tests — no real network.
1635
+ * - **§14 wire narrowing.** Every decoded prompt is guard-narrowed before dispatch (never an `as`).
1636
+ *
1637
+ * @example
1638
+ * ```ts
1639
+ * import { createPromptClient } from '@src/core'
1640
+ *
1641
+ * const client = createPromptClient({ url: 'http://host/prompts', terminal })
1642
+ * await client.connect()
1643
+ * ```
1644
+ */
1645
+ function createPromptClient(options) {
1646
+ return new PromptClient(options);
1647
+ }
1648
+ //#endregion
1649
+ export { ACCEPT_EVENT_STREAM, ALPHANUMERIC_PATTERN, BACKSPACE, CONTROL_NAMES, CTRL_A, CTRL_C, CTRL_D, CTRL_E, CTRL_U, DEFAULT_MASK, DEFAULT_PROMPT_TIMEOUT_MS, DEFAULT_RECONNECT_DELAY_MS, DELETE, EMAIL_PATTERN, ESCAPE, HEADER_TOKEN, INTEGER_PATTERN, KEY_CSI, KEY_SS3, NEWLINE, NUMERIC_PATTERN, PROMPT_ICONS, Prompt, PromptClient, RETURN, RULE_MESSAGES, SEQUENCE_NAMES, SPACE, SSE_BUFFER_LIMIT, SSE_EVENTS, TAB, TerminalError, URL_PATTERN, appendRule, buildRuleValidator, checkboxReduce, checkboxView, composeValidators, confirmReduce, confirmView, createCheckboxState, createConfirmState, createEditorState, createInputState, createPasswordState, createPrompt, createPromptClient, createSelectState, defaultTimer, dispatchPendingPrompt, editLine, editorReduce, editorView, errorLine, evaluateRule, gateSelection, globalFetch, inputReduce, inputView, isAbortError, isCheckboxChoice, isInsecureRemote, isPendingPrompt, isPendingPromptStatus, isPrintable, isPromptChoice, isPromptType, isTerminalError, normalizeCheckboxChoice, normalizeChoice, parseKey, parseWireJSON, passing, passwordReduce, passwordView, promptHeader, reconstructValidationRules, resolveChoices, resolveOption, resolveValidation, sanitizeChoiceLabels, selectReduce, selectView, serializeChoices, serializePromptOptions, serializeValidationRules, submitHeader, toggleIndex };
1650
+
1651
+ //# sourceMappingURL=index.js.map