@orkestrel/terminal 0.0.8 → 0.0.10
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +51 -33
- package/dist/src/core/index.cjs +611 -943
- package/dist/src/core/index.d.cts +522 -963
- package/dist/src/core/index.d.ts +522 -963
- package/dist/src/core/index.js +610 -916
- package/dist/src/server/index.cjs +556 -203
- package/dist/src/server/index.d.cts +232 -129
- package/dist/src/server/index.d.ts +232 -129
- package/dist/src/server/index.js +541 -204
- package/package.json +10 -8
- package/dist/src/core/index.cjs.map +0 -1
- package/dist/src/core/index.js.map +0 -1
- package/dist/src/server/index.cjs.map +0 -1
- package/dist/src/server/index.js.map +0 -1
package/dist/src/core/index.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
1
|
+
import { STATUS_ICONS, createStyler, freezeStyle, strip, stripControls } from "@orkestrel/console";
|
|
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
|
+
import { createForm, isFieldError, isFormError, parseForm, serializeForm } from "@orkestrel/form";
|
|
4
5
|
import { createSSEParser } from "@orkestrel/sse";
|
|
5
6
|
import { createDatabase, createMemoryDriver } from "@orkestrel/database";
|
|
6
7
|
//#region src/core/constants.ts
|
|
@@ -127,38 +128,9 @@ var CONTROL_NAMES = Object.freeze({
|
|
|
127
128
|
ctrl: true
|
|
128
129
|
})
|
|
129
130
|
});
|
|
130
|
-
/** The default mask glyph
|
|
131
|
+
/** The default mask glyph {@link import('./helpers.js').createPasswordState} uses — `*`. */
|
|
131
132
|
var DEFAULT_MASK = "*";
|
|
132
133
|
/**
|
|
133
|
-
* Matches an HTTP(S) URL. The `url` rule tests against this. Kept locally because it is
|
|
134
|
-
* narrower than `FORMAT_PATTERNS.uri`, which accepts other URI schemes.
|
|
135
|
-
*/
|
|
136
|
-
var URL_PATTERN = /^https?:\/\/.+/;
|
|
137
|
-
/** Matches a numeric value (integer or decimal, optional sign). The `numeric` rule tests against this. */
|
|
138
|
-
var NUMERIC_PATTERN = /^-?\d+(\.\d+)?$/;
|
|
139
|
-
/** Matches an integer (optional sign). The `integer` rule tests against this. */
|
|
140
|
-
var INTEGER_PATTERN = /^-?\d+$/;
|
|
141
|
-
/** Matches an alphanumeric string (letters and digits only). The `alphanumeric` rule tests against this. */
|
|
142
|
-
var ALPHANUMERIC_PATTERN = /^[a-zA-Z0-9]+$/;
|
|
143
|
-
/**
|
|
144
|
-
* Each built-in validation rule's default error message — what the composed {@link
|
|
145
|
-
* import('./types.js').Validator} returns when that rule fails (the `minimum` / `maximum`
|
|
146
|
-
* messages are interpolated with the configured length at build time). Frozen; the source of
|
|
147
|
-
* truth for the rule-failure copy.
|
|
148
|
-
*/
|
|
149
|
-
var RULE_MESSAGES = Object.freeze({
|
|
150
|
-
required: "This field is required",
|
|
151
|
-
minimum: "Must be at least {count} characters",
|
|
152
|
-
maximum: "Must be at most {count} characters",
|
|
153
|
-
pattern: "Must match pattern: {pattern}",
|
|
154
|
-
email: "Must be a valid email address",
|
|
155
|
-
url: "Must be a valid URL",
|
|
156
|
-
numeric: "Must be a numeric value",
|
|
157
|
-
integer: "Must be an integer",
|
|
158
|
-
alphanumeric: "Must contain only letters and digits",
|
|
159
|
-
invalid: "Invalid input"
|
|
160
|
-
});
|
|
161
|
-
/**
|
|
162
134
|
* The six default glyphs {@link DEFAULT_PROMPT_THEME} assembles its `icons` from. Read only when
|
|
163
135
|
* the default theme is assembled; a view reads its resolved theme and never this constant. Frozen.
|
|
164
136
|
*
|
|
@@ -254,11 +226,11 @@ var DEFAULT_PROMPT_TIMEOUT_MS = 3e5;
|
|
|
254
226
|
/** How long (ms) the {@link import('./types.js').PromptClientInterface} waits before each reconnect attempt — 2 seconds. */
|
|
255
227
|
var DEFAULT_RECONNECT_DELAY_MS = 2e3;
|
|
256
228
|
/**
|
|
257
|
-
* The SSE `event:` names the broker emits and the {@link import('./types.js').
|
|
229
|
+
* The SSE `event:` names the broker emits and the {@link import('./types.js').PromptClientInterface}
|
|
258
230
|
* dispatches on. Frozen; the source of truth for the wire event vocabulary.
|
|
259
231
|
*
|
|
260
232
|
* @remarks
|
|
261
|
-
* - `pending` — a serialized {@link import('./types.js').
|
|
233
|
+
* - `pending` — a serialized {@link import('./types.js').PendingForm} to dispatch and answer.
|
|
262
234
|
* - `expire` — an `{ id }` payload: the broker expired a parked prompt (the client drops it).
|
|
263
235
|
* - `shutdown` — the broker is going away; the client disconnects (no auto-reconnect) but stays reusable.
|
|
264
236
|
*/
|
|
@@ -267,12 +239,12 @@ var SSE_EVENTS = Object.freeze({
|
|
|
267
239
|
expire: "expire",
|
|
268
240
|
shutdown: "shutdown"
|
|
269
241
|
});
|
|
270
|
-
/** The auth-token request header the {@link import('./types.js').
|
|
242
|
+
/** The auth-token request header the {@link import('./types.js').PromptClientInterface} sends. */
|
|
271
243
|
var HEADER_TOKEN = "x-orkestrel-token";
|
|
272
244
|
/** The `Accept` header value that opens the broker's SSE stream. */
|
|
273
245
|
var ACCEPT_EVENT_STREAM = "text/event-stream";
|
|
274
246
|
/**
|
|
275
|
-
* The maximum number of characters the {@link import('./types.js').
|
|
247
|
+
* The maximum number of characters the {@link import('./types.js').PromptClientInterface} lets its
|
|
276
248
|
* SSE parser buffer before treating the stream as hostile — 1 MiB, comfortably above any
|
|
277
249
|
* legitimate prompt payload. Passed as the `limit` to `createSSEParser` so an unterminated
|
|
278
250
|
* or oversized `data:` field cannot grow the buffer without bound (a memory-exhaustion guard).
|
|
@@ -281,19 +253,20 @@ var SSE_BUFFER_LIMIT = 1048576;
|
|
|
281
253
|
//#endregion
|
|
282
254
|
//#region src/core/errors.ts
|
|
283
255
|
/**
|
|
284
|
-
*
|
|
256
|
+
* The error the terminal surfaces for its own refusals: parking on a destroyed or full broker, an
|
|
257
|
+
* unusable driver stream, a manager routing fault, or a ctrl-c cancellation. A parked form's own
|
|
258
|
+
* lifecycle failures reject through the form's `answer` with the form package's error, never with
|
|
259
|
+
* this one.
|
|
285
260
|
*
|
|
286
261
|
* @remarks
|
|
287
|
-
* Carries a {@link TerminalErrorCode} and an optional `context` bag
|
|
288
|
-
*
|
|
289
|
-
*
|
|
290
|
-
* `EXPIRE`); or when the user aborts an interactive server-`Terminal` prompt with ctrl-c
|
|
291
|
-
* (`CANCEL`). Narrow a caught value with {@link isTerminalError} and branch on `error.code`.
|
|
262
|
+
* Carries a {@link TerminalErrorCode} and an optional `context` bag naming the offending values:
|
|
263
|
+
* `{ cap }` on `LIMIT`, `{ to, known }` on `TARGET`, and `{ from, to, path }` on `DEADLOCK`. Narrow
|
|
264
|
+
* a caught value with {@link isTerminalError} and branch on `error.code`.
|
|
292
265
|
*/
|
|
293
266
|
var TerminalError = class extends Error {
|
|
294
267
|
/** The machine-readable condition — see {@link TerminalErrorCode}. */
|
|
295
268
|
code;
|
|
296
|
-
/** An optional context bag naming the offending
|
|
269
|
+
/** An optional context bag naming the offending values — see the class {@link TerminalError remarks}. */
|
|
297
270
|
context;
|
|
298
271
|
constructor(code, message, context) {
|
|
299
272
|
super(message);
|
|
@@ -311,9 +284,11 @@ var TerminalError = class extends Error {
|
|
|
311
284
|
* @example
|
|
312
285
|
* ```ts
|
|
313
286
|
* try {
|
|
314
|
-
*
|
|
287
|
+
* await terminal.ask(form)
|
|
315
288
|
* } catch (error) {
|
|
316
|
-
* if (isTerminalError(error) && error.code === '
|
|
289
|
+
* if (isTerminalError(error) && error.code === 'CANCEL') {
|
|
290
|
+
* // the person aborted
|
|
291
|
+
* }
|
|
317
292
|
* }
|
|
318
293
|
* ```
|
|
319
294
|
*/
|
|
@@ -322,83 +297,39 @@ function isTerminalError(value) {
|
|
|
322
297
|
}
|
|
323
298
|
//#endregion
|
|
324
299
|
//#region src/core/validators.ts
|
|
325
|
-
/** Narrow an unknown value to a {@link
|
|
326
|
-
var
|
|
327
|
-
/** Narrow an unknown value to a {@link PendingPromptStatus}. */
|
|
328
|
-
var isPendingPromptStatus = literalOf("pending", "answered", "expired");
|
|
329
|
-
/**
|
|
330
|
-
* Narrow an unknown wire value to a {@link PendingPrompt} before dispatching it.
|
|
331
|
-
*
|
|
332
|
-
* @param value - The decoded wire value to inspect
|
|
333
|
-
* @returns Whether `value` is a complete pending-prompt record
|
|
334
|
-
*/
|
|
335
|
-
var isPendingPrompt = recordOf({
|
|
336
|
-
id: isNonEmptyString,
|
|
337
|
-
form: isPromptType,
|
|
338
|
-
message: isString,
|
|
339
|
-
options: isRecord,
|
|
340
|
-
status: isPendingPromptStatus,
|
|
341
|
-
time: isNumber,
|
|
342
|
-
from: isString,
|
|
343
|
-
to: isString
|
|
344
|
-
}, ["from", "to"]);
|
|
300
|
+
/** Narrow an unknown value to a {@link PendingFormStatus}. */
|
|
301
|
+
var isPendingFormStatus = literalOf("pending", "answered", "expired");
|
|
345
302
|
/**
|
|
346
|
-
* Narrow an unknown value to a
|
|
347
|
-
* {@link import('./types.js').PromptRole} carries, built from the console module's own
|
|
348
|
-
* {@link import('@orkestrel/console').COLORS} + {@link import('@orkestrel/console').ATTRIBUTES} so
|
|
349
|
-
* the style vocabulary has ONE source.
|
|
303
|
+
* Narrow an unknown wire value to a {@link PendingForm} envelope.
|
|
350
304
|
*
|
|
351
305
|
* @remarks
|
|
352
|
-
*
|
|
353
|
-
*
|
|
354
|
-
*
|
|
355
|
-
*
|
|
306
|
+
* This guard checks the transport record and proves only that `schema` is a record. The Form
|
|
307
|
+
* package's `parseForm` owns the schema payload and its semantic audit.
|
|
308
|
+
*
|
|
309
|
+
* @param value - The decoded wire value to inspect
|
|
310
|
+
* @returns Whether the value is a complete pending-form envelope
|
|
356
311
|
*/
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
312
|
+
function isPendingForm(value) {
|
|
313
|
+
return recordOf({
|
|
314
|
+
id: isNonEmptyString,
|
|
315
|
+
schema: isRecord,
|
|
316
|
+
status: isPendingFormStatus,
|
|
317
|
+
time: isNumber,
|
|
318
|
+
from: isString,
|
|
319
|
+
to: isString
|
|
320
|
+
}, ["from", "to"])(value);
|
|
321
|
+
}
|
|
362
322
|
/**
|
|
363
|
-
* Narrow an unknown
|
|
364
|
-
* `theme` option passes before it reaches a local prompt.
|
|
323
|
+
* Narrow an unknown value to a transport-neutral {@link WireEvent}.
|
|
365
324
|
*
|
|
366
|
-
* @
|
|
367
|
-
*
|
|
368
|
-
* a role whose value is not a {@link isStyle}-shaped console style rejects the whole theme, so a
|
|
369
|
-
* hostile payload degrades to the default theme rather than partially applying. Glyphs still carry
|
|
370
|
-
* arbitrary text at this point — {@link import('./helpers.js').sanitizeThemeIcons} strips their
|
|
371
|
-
* control bytes on dispatch.
|
|
372
|
-
*
|
|
373
|
-
* The icons and roles shapes are checked against their complete union-keyed guard records. A slot
|
|
374
|
-
* added to either union without an entry here is a compile error rather than a wire theme that is
|
|
375
|
-
* silently refused.
|
|
325
|
+
* @param value - The candidate wire event
|
|
326
|
+
* @returns Whether the value carries an event name, serialized data, and an optional id
|
|
376
327
|
*/
|
|
377
|
-
var
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
selected: isString,
|
|
383
|
-
checked: isString,
|
|
384
|
-
unchecked: isString,
|
|
385
|
-
success: isString,
|
|
386
|
-
error: isString
|
|
387
|
-
}, true),
|
|
388
|
-
roles: recordOf({
|
|
389
|
-
question: isStyle,
|
|
390
|
-
pointer: isStyle,
|
|
391
|
-
message: isStyle,
|
|
392
|
-
content: isStyle,
|
|
393
|
-
success: isStyle,
|
|
394
|
-
error: isStyle,
|
|
395
|
-
selected: isStyle,
|
|
396
|
-
focus: isStyle,
|
|
397
|
-
hint: isStyle,
|
|
398
|
-
description: isStyle,
|
|
399
|
-
muted: isStyle
|
|
400
|
-
}, true)
|
|
401
|
-
}, true);
|
|
328
|
+
var isWireEvent = recordOf({
|
|
329
|
+
event: isString,
|
|
330
|
+
data: isString,
|
|
331
|
+
id: isString
|
|
332
|
+
}, ["id"]);
|
|
402
333
|
/** Narrow an unknown value to a {@link TerminalSnapshot}. */
|
|
403
334
|
var isTerminalSnapshot = recordOf({
|
|
404
335
|
id: isNonEmptyString,
|
|
@@ -479,156 +410,6 @@ function isPrintable(character) {
|
|
|
479
410
|
return code >= 32 && code !== 127;
|
|
480
411
|
}
|
|
481
412
|
/**
|
|
482
|
-
* Evaluate ONE built-in validation rule against `input`, returning its error message when the
|
|
483
|
-
* rule fails or `undefined` when it passes. The atomic check {@link buildRuleValidator} wraps
|
|
484
|
-
* into a {@link Validator}. Pure.
|
|
485
|
-
*
|
|
486
|
-
* @remarks
|
|
487
|
-
* A function `check` is the custom-override path: it is called and its `true` ⇒ pass, a string
|
|
488
|
-
* ⇒ that message, anything else ⇒ the generic {@link RULE_MESSAGES.invalid}. A primitive `check`
|
|
489
|
-
* runs the named built-in: `required` (non-empty trimmed), `minimum` / `maximum` (length
|
|
490
|
-
* bounds, the message interpolated with the count), `pattern` (a regex source), and the
|
|
491
|
-
* `email` / `url` / `numeric` / `integer` / `alphanumeric` pattern tests.
|
|
492
|
-
*
|
|
493
|
-
* @param rule - The rule name (`'required'`, `'minimum'`, …)
|
|
494
|
-
* @param check - The configured value (a primitive toggle / bound / pattern, or a custom {@link Validator})
|
|
495
|
-
* @param input - The input string to test
|
|
496
|
-
* @returns The error message when the rule fails, else `undefined`
|
|
497
|
-
*/
|
|
498
|
-
function evaluateRule(rule, check, input) {
|
|
499
|
-
if (typeof check === "function") {
|
|
500
|
-
const result = check(input);
|
|
501
|
-
if (result === true) return void 0;
|
|
502
|
-
return isString(result) ? result : RULE_MESSAGES.invalid;
|
|
503
|
-
}
|
|
504
|
-
switch (rule) {
|
|
505
|
-
case "required":
|
|
506
|
-
if (check === true && input.trim().length === 0) return RULE_MESSAGES.required;
|
|
507
|
-
break;
|
|
508
|
-
case "minimum":
|
|
509
|
-
if (isNumber(check) && [...input].length < check) return RULE_MESSAGES.minimum.replace("{count}", String(check));
|
|
510
|
-
break;
|
|
511
|
-
case "maximum":
|
|
512
|
-
if (isNumber(check) && [...input].length > check) return RULE_MESSAGES.maximum.replace("{count}", String(check));
|
|
513
|
-
break;
|
|
514
|
-
case "pattern":
|
|
515
|
-
if (isString(check)) {
|
|
516
|
-
let compiled;
|
|
517
|
-
try {
|
|
518
|
-
compiled = new RegExp(check);
|
|
519
|
-
} catch {
|
|
520
|
-
return RULE_MESSAGES.pattern.replace("{pattern}", check);
|
|
521
|
-
}
|
|
522
|
-
if (!compiled.test(input)) return RULE_MESSAGES.pattern.replace("{pattern}", check);
|
|
523
|
-
}
|
|
524
|
-
break;
|
|
525
|
-
case "email":
|
|
526
|
-
if (check === true && !FORMAT_PATTERNS.email.test(input)) return RULE_MESSAGES.email;
|
|
527
|
-
break;
|
|
528
|
-
case "url":
|
|
529
|
-
if (check === true && !URL_PATTERN.test(input)) return RULE_MESSAGES.url;
|
|
530
|
-
break;
|
|
531
|
-
case "numeric":
|
|
532
|
-
if (check === true && !NUMERIC_PATTERN.test(input)) return RULE_MESSAGES.numeric;
|
|
533
|
-
break;
|
|
534
|
-
case "integer":
|
|
535
|
-
if (check === true && !INTEGER_PATTERN.test(input)) return RULE_MESSAGES.integer;
|
|
536
|
-
break;
|
|
537
|
-
case "alphanumeric":
|
|
538
|
-
if (check === true && !ALPHANUMERIC_PATTERN.test(input)) return RULE_MESSAGES.alphanumeric;
|
|
539
|
-
break;
|
|
540
|
-
}
|
|
541
|
-
}
|
|
542
|
-
/** Wrap a named rule + its primitive check into a {@link Validator} (returns `true` or the rule's message). */
|
|
543
|
-
function buildRuleValidator(rule, check) {
|
|
544
|
-
return (input) => evaluateRule(rule, check, input) ?? true;
|
|
545
|
-
}
|
|
546
|
-
/**
|
|
547
|
-
* Append a rule-backed {@link Validator} to `validators` when the rule is enabled — a `false` /
|
|
548
|
-
* `undefined` `check` is skipped, a function `check` is added verbatim (the custom override), and
|
|
549
|
-
* a primitive `check` is wrapped via {@link buildRuleValidator}. Mutates `validators` in place.
|
|
550
|
-
*/
|
|
551
|
-
function appendRule(validators, rule, check) {
|
|
552
|
-
if (check === void 0 || check === false) return;
|
|
553
|
-
if (typeof check === "function") {
|
|
554
|
-
validators.push(check);
|
|
555
|
-
return;
|
|
556
|
-
}
|
|
557
|
-
validators.push(buildRuleValidator(rule, check));
|
|
558
|
-
}
|
|
559
|
-
/**
|
|
560
|
-
* Compose several {@link Validator}s into ONE short-circuiting validator — it runs them in order
|
|
561
|
-
* and returns the FIRST error message, or `true` when all pass. The empty composition always
|
|
562
|
-
* passes.
|
|
563
|
-
*/
|
|
564
|
-
function composeValidators(...validators) {
|
|
565
|
-
return (input) => {
|
|
566
|
-
for (const validator of validators) {
|
|
567
|
-
const result = validator(input);
|
|
568
|
-
if (result !== true) return result;
|
|
569
|
-
}
|
|
570
|
-
return true;
|
|
571
|
-
};
|
|
572
|
-
}
|
|
573
|
-
/**
|
|
574
|
-
* Compile a {@link Validator} or declarative {@link ValidationRules} (or nothing) into ONE
|
|
575
|
-
* composed {@link Validator}. A bare validator passes through; rules are appended in the fixed
|
|
576
|
-
* order (required → minimum → maximum → pattern → email → url → numeric → integer → alphanumeric
|
|
577
|
-
* → custom) and composed; absent / empty input yields an always-passing validator. Pure.
|
|
578
|
-
*
|
|
579
|
-
* @remarks
|
|
580
|
-
* Unlike a prior variant (which returned `Validator | undefined`), this ALWAYS returns a
|
|
581
|
-
* `Validator` — an absent or empty rule set yields a validator that returns `true` for every
|
|
582
|
-
* input. That keeps a prompt's state unconditional (it always holds a real validator to apply on
|
|
583
|
-
* submit), with no `undefined` branch at the call site.
|
|
584
|
-
*
|
|
585
|
-
* @param validate - A custom {@link Validator}, a {@link ValidationRules} bag, or `undefined`
|
|
586
|
-
* @returns The composed {@link Validator} (always-passing when nothing was supplied)
|
|
587
|
-
*
|
|
588
|
-
* @example
|
|
589
|
-
* ```ts
|
|
590
|
-
* const v = resolveValidation({ required: true, minimum: 3 })
|
|
591
|
-
* v('') // 'This field is required'
|
|
592
|
-
* v('ab') // 'Must be at least 3 characters'
|
|
593
|
-
* v('abc') // true
|
|
594
|
-
* ```
|
|
595
|
-
*/
|
|
596
|
-
function resolveValidation(validate) {
|
|
597
|
-
if (validate === void 0) return passing;
|
|
598
|
-
if (typeof validate === "function") return validate;
|
|
599
|
-
const validators = [];
|
|
600
|
-
appendRule(validators, "required", validate.required);
|
|
601
|
-
appendRule(validators, "minimum", validate.minimum);
|
|
602
|
-
appendRule(validators, "maximum", validate.maximum);
|
|
603
|
-
appendRule(validators, "pattern", validate.pattern);
|
|
604
|
-
appendRule(validators, "email", validate.email);
|
|
605
|
-
appendRule(validators, "url", validate.url);
|
|
606
|
-
appendRule(validators, "numeric", validate.numeric);
|
|
607
|
-
appendRule(validators, "integer", validate.integer);
|
|
608
|
-
appendRule(validators, "alphanumeric", validate.alphanumeric);
|
|
609
|
-
if (typeof validate.custom === "function") validators.push(validate.custom);
|
|
610
|
-
if (validators.length === 0) return passing;
|
|
611
|
-
return composeValidators(...validators);
|
|
612
|
-
}
|
|
613
|
-
/** The always-passing {@link Validator} — the resolved validator when no rules were supplied. */
|
|
614
|
-
function passing(_input) {
|
|
615
|
-
return true;
|
|
616
|
-
}
|
|
617
|
-
/** Normalize a select choice input into a full {@link PromptChoice} (a bare string becomes both name and value). */
|
|
618
|
-
function normalizeChoice(choice) {
|
|
619
|
-
return isString(choice) ? {
|
|
620
|
-
name: choice,
|
|
621
|
-
value: choice
|
|
622
|
-
} : choice;
|
|
623
|
-
}
|
|
624
|
-
/** Normalize a checkbox choice input into a full {@link CheckboxChoice} (a bare string becomes both name and value). */
|
|
625
|
-
function normalizeCheckboxChoice(choice) {
|
|
626
|
-
return isString(choice) ? {
|
|
627
|
-
name: choice,
|
|
628
|
-
value: choice
|
|
629
|
-
} : choice;
|
|
630
|
-
}
|
|
631
|
-
/**
|
|
632
413
|
* Build a complete {@link PromptTheme} by merging a partial one over
|
|
633
414
|
* {@link DEFAULT_PROMPT_THEME}, leaf by leaf — each supplied icon replaces that glyph, each
|
|
634
415
|
* supplied role replaces that {@link Style}, and everything else keeps its default. Each supplied
|
|
@@ -664,11 +445,11 @@ function createPromptTheme(options) {
|
|
|
664
445
|
});
|
|
665
446
|
}
|
|
666
447
|
/**
|
|
667
|
-
* Sanitize text for one single-line display slot. Composes console's {@link
|
|
668
|
-
* with removal of tab, line feed, and carriage return
|
|
448
|
+
* Sanitize text for one single-line display slot. Composes console's ANSI {@link strip} and C0
|
|
449
|
+
* {@link stripControls} passes with removal of tab, line feed, and carriage return.
|
|
669
450
|
*
|
|
670
451
|
* @param text - The text to sanitize for a glyph or hint slot
|
|
671
|
-
* @returns The text with every C0 control character and DEL removed
|
|
452
|
+
* @returns The text with ANSI sequences, every C0 control character, and DEL removed
|
|
672
453
|
*
|
|
673
454
|
* @example
|
|
674
455
|
* ```ts
|
|
@@ -676,7 +457,126 @@ function createPromptTheme(options) {
|
|
|
676
457
|
* ```
|
|
677
458
|
*/
|
|
678
459
|
function sanitizeDisplayText(text) {
|
|
679
|
-
return stripControls(text).replaceAll(" ", "").replaceAll("\n", "").replaceAll("\r", "");
|
|
460
|
+
return stripControls(strip(text)).replaceAll(" ", "").replaceAll("\n", "").replaceAll("\r", "");
|
|
461
|
+
}
|
|
462
|
+
/**
|
|
463
|
+
* Sanitize every terminal-readable string in a parsed form schema.
|
|
464
|
+
*
|
|
465
|
+
* @remarks
|
|
466
|
+
* Display strings pass through {@link sanitizeDisplayText}: labels, help, placeholders, masks,
|
|
467
|
+
* choice labels and help, file accept entries, and pattern sources. Identity and answer strings
|
|
468
|
+
* stay verbatim: schema, group, and field names, group references, choice values, and defaults.
|
|
469
|
+
* Rewriting those would sever the rendering copy from the authoritative form. Field metadata is
|
|
470
|
+
* removed because terminal neither renders nor interprets it. Pattern sources are sanitized as
|
|
471
|
+
* text only and are never compiled or executed here.
|
|
472
|
+
*
|
|
473
|
+
* @param schema - A schema already accepted by the Form package's `parseForm`
|
|
474
|
+
* @returns A new schema with terminal-readable strings sanitized and field metadata omitted
|
|
475
|
+
*
|
|
476
|
+
* @example
|
|
477
|
+
* ```ts
|
|
478
|
+
* sanitizeSchema({ fields: [{ control: 'text', name: 'na\u001bme', label: 'N\u0000ame' }] })
|
|
479
|
+
* // { fields: [{ control: 'text', name: 'na\u001bme', label: 'Name' }] }
|
|
480
|
+
* ```
|
|
481
|
+
*/
|
|
482
|
+
function sanitizeSchema(schema) {
|
|
483
|
+
const groups = schema.groups?.map((group) => ({
|
|
484
|
+
name: group.name,
|
|
485
|
+
label: sanitizeDisplayText(group.label),
|
|
486
|
+
...group.help !== void 0 ? { help: sanitizeDisplayText(group.help) } : {}
|
|
487
|
+
}));
|
|
488
|
+
const fields = [];
|
|
489
|
+
for (const source of schema.fields) {
|
|
490
|
+
const { meta: _meta, ...field } = source;
|
|
491
|
+
const rule = field.rule === void 0 ? void 0 : {
|
|
492
|
+
...field.rule,
|
|
493
|
+
...field.rule.pattern !== void 0 ? { pattern: sanitizeDisplayText(field.rule.pattern) } : {}
|
|
494
|
+
};
|
|
495
|
+
const shared = {
|
|
496
|
+
name: field.name,
|
|
497
|
+
...field.label !== void 0 ? { label: sanitizeDisplayText(field.label) } : {},
|
|
498
|
+
...field.help !== void 0 ? { help: sanitizeDisplayText(field.help) } : {},
|
|
499
|
+
...field.group !== void 0 ? { group: field.group } : {},
|
|
500
|
+
...rule !== void 0 ? { rule } : {}
|
|
501
|
+
};
|
|
502
|
+
switch (field.control) {
|
|
503
|
+
case "text":
|
|
504
|
+
case "editor":
|
|
505
|
+
fields.push({
|
|
506
|
+
...field,
|
|
507
|
+
...shared,
|
|
508
|
+
...field.placeholder !== void 0 ? { placeholder: sanitizeDisplayText(field.placeholder) } : {}
|
|
509
|
+
});
|
|
510
|
+
break;
|
|
511
|
+
case "password":
|
|
512
|
+
fields.push({
|
|
513
|
+
...field,
|
|
514
|
+
...shared,
|
|
515
|
+
...field.mask !== void 0 ? { mask: sanitizeDisplayText(field.mask) } : {}
|
|
516
|
+
});
|
|
517
|
+
break;
|
|
518
|
+
case "number":
|
|
519
|
+
fields.push({
|
|
520
|
+
...field,
|
|
521
|
+
...shared,
|
|
522
|
+
...field.placeholder !== void 0 ? { placeholder: sanitizeDisplayText(field.placeholder) } : {}
|
|
523
|
+
});
|
|
524
|
+
break;
|
|
525
|
+
case "date":
|
|
526
|
+
case "time":
|
|
527
|
+
case "datetime":
|
|
528
|
+
case "color":
|
|
529
|
+
fields.push({
|
|
530
|
+
...field,
|
|
531
|
+
...shared
|
|
532
|
+
});
|
|
533
|
+
break;
|
|
534
|
+
case "confirm":
|
|
535
|
+
fields.push({
|
|
536
|
+
...field,
|
|
537
|
+
...shared
|
|
538
|
+
});
|
|
539
|
+
break;
|
|
540
|
+
case "select":
|
|
541
|
+
fields.push({
|
|
542
|
+
...field,
|
|
543
|
+
...shared,
|
|
544
|
+
choices: field.choices.map((choice) => ({
|
|
545
|
+
...choice,
|
|
546
|
+
value: choice.value,
|
|
547
|
+
label: sanitizeDisplayText(choice.label),
|
|
548
|
+
...choice.help !== void 0 ? { help: sanitizeDisplayText(choice.help) } : {}
|
|
549
|
+
}))
|
|
550
|
+
});
|
|
551
|
+
break;
|
|
552
|
+
case "checkbox":
|
|
553
|
+
fields.push({
|
|
554
|
+
...field,
|
|
555
|
+
...shared,
|
|
556
|
+
choices: field.choices.map((choice) => ({
|
|
557
|
+
...choice,
|
|
558
|
+
value: choice.value,
|
|
559
|
+
label: sanitizeDisplayText(choice.label),
|
|
560
|
+
...choice.help !== void 0 ? { help: sanitizeDisplayText(choice.help) } : {}
|
|
561
|
+
}))
|
|
562
|
+
});
|
|
563
|
+
break;
|
|
564
|
+
case "file":
|
|
565
|
+
fields.push({
|
|
566
|
+
...field,
|
|
567
|
+
...shared,
|
|
568
|
+
...field.accept !== void 0 ? { accept: field.accept.map(sanitizeDisplayText) } : {}
|
|
569
|
+
});
|
|
570
|
+
break;
|
|
571
|
+
}
|
|
572
|
+
}
|
|
573
|
+
return {
|
|
574
|
+
...schema.name !== void 0 ? { name: schema.name } : {},
|
|
575
|
+
...schema.label !== void 0 ? { label: sanitizeDisplayText(schema.label) } : {},
|
|
576
|
+
...schema.help !== void 0 ? { help: sanitizeDisplayText(schema.help) } : {},
|
|
577
|
+
...groups !== void 0 ? { groups } : {},
|
|
578
|
+
fields
|
|
579
|
+
};
|
|
680
580
|
}
|
|
681
581
|
/**
|
|
682
582
|
* Sanitize every glyph a wire-supplied {@link PromptThemeOptions} carries for a single-line display
|
|
@@ -718,32 +618,38 @@ function hintedHeader(styler, theme, message, hint) {
|
|
|
718
618
|
function submitHeader(styler, theme, message) {
|
|
719
619
|
return `${styler.render(theme.roles.success, theme.icons.success)} ${styler.render(theme.roles.message, message)}`;
|
|
720
620
|
}
|
|
721
|
-
/** The styled error line (`✖ message`)
|
|
621
|
+
/** The styled error line (`✖ message`) a form driver appends for a field failure. */
|
|
722
622
|
function errorLine(styler, theme, message) {
|
|
723
623
|
return `${styler.render(theme.roles.error, theme.icons.error)} ${styler.render(theme.roles.error, message)}`;
|
|
724
624
|
}
|
|
725
|
-
/**
|
|
726
|
-
|
|
625
|
+
/**
|
|
626
|
+
* Build the initial text-field key state.
|
|
627
|
+
*
|
|
628
|
+
* @param field - The text field to render
|
|
629
|
+
* @param styler - The styler used to render the view
|
|
630
|
+
* @param theme - The optional terminal theme
|
|
631
|
+
* @returns The initial immutable key state
|
|
632
|
+
*/
|
|
633
|
+
function createInputState(field, styler = createStyler(), theme) {
|
|
727
634
|
return {
|
|
728
|
-
message:
|
|
729
|
-
default:
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
theme: createPromptTheme(options.theme),
|
|
635
|
+
message: sanitizeDisplayText(field.label ?? field.name),
|
|
636
|
+
default: field.default ?? "",
|
|
637
|
+
styler,
|
|
638
|
+
theme: createPromptTheme(theme),
|
|
733
639
|
value: ""
|
|
734
640
|
};
|
|
735
641
|
}
|
|
736
|
-
/** Render
|
|
642
|
+
/** Render a text-field key state as a styled view. */
|
|
737
643
|
function inputView(state) {
|
|
738
|
-
const
|
|
739
|
-
const
|
|
740
|
-
|
|
644
|
+
const content = state.value.length > 0 ? state.value : state.default;
|
|
645
|
+
const role = state.value.length > 0 ? state.theme.roles.content : state.theme.roles.hint;
|
|
646
|
+
const shown = state.styler.render(role, sanitizeDisplayText(content));
|
|
647
|
+
return `${promptHeader(state.styler, state.theme, state.message)} ${state.styler.render(state.theme.roles.pointer, state.theme.icons.pointer)} ${shown}`;
|
|
741
648
|
}
|
|
742
649
|
/**
|
|
743
650
|
* Advance an input prompt by one {@link KeyEvent} — the pure `(state, key) → PromptStep<string>`
|
|
744
651
|
* reducer. Printable characters extend the value; backspace shrinks it; ctrl-u clears it; ctrl-c
|
|
745
|
-
* cancels; return
|
|
746
|
-
* invalid submit stays active with the error in the view.
|
|
652
|
+
* cancels; return produces the candidate value, with an empty line falling back to the default.
|
|
747
653
|
*/
|
|
748
654
|
function inputReduce(state, key) {
|
|
749
655
|
if (key.ctrl && key.name === "c") return {
|
|
@@ -751,27 +657,14 @@ function inputReduce(state, key) {
|
|
|
751
657
|
view: inputView(state),
|
|
752
658
|
status: "cancel"
|
|
753
659
|
};
|
|
754
|
-
const { error: _error, ...clear } = state;
|
|
755
660
|
if (key.name === "return") {
|
|
756
661
|
const answer = state.value.length > 0 ? state.value : state.default;
|
|
757
|
-
const result = state.validator(answer);
|
|
758
|
-
if (result !== true) {
|
|
759
|
-
const next = {
|
|
760
|
-
...state,
|
|
761
|
-
error: result
|
|
762
|
-
};
|
|
763
|
-
return {
|
|
764
|
-
state: next,
|
|
765
|
-
view: inputView(next),
|
|
766
|
-
status: "active"
|
|
767
|
-
};
|
|
768
|
-
}
|
|
769
662
|
return {
|
|
770
663
|
state: {
|
|
771
|
-
...
|
|
664
|
+
...state,
|
|
772
665
|
value: answer
|
|
773
666
|
},
|
|
774
|
-
view: `${submitHeader(state.styler, state.theme, state.message)} ${state.styler.render(state.theme.roles.hint, answer)}`,
|
|
667
|
+
view: `${submitHeader(state.styler, state.theme, state.message)} ${state.styler.render(state.theme.roles.hint, sanitizeDisplayText(answer))}`,
|
|
775
668
|
status: "submit",
|
|
776
669
|
value: answer
|
|
777
670
|
};
|
|
@@ -783,7 +676,7 @@ function inputReduce(state, key) {
|
|
|
783
676
|
status: "active"
|
|
784
677
|
};
|
|
785
678
|
const next = {
|
|
786
|
-
...
|
|
679
|
+
...state,
|
|
787
680
|
value
|
|
788
681
|
};
|
|
789
682
|
return {
|
|
@@ -792,28 +685,32 @@ function inputReduce(state, key) {
|
|
|
792
685
|
status: "active"
|
|
793
686
|
};
|
|
794
687
|
}
|
|
795
|
-
/**
|
|
796
|
-
|
|
688
|
+
/**
|
|
689
|
+
* Build the initial password-field key state.
|
|
690
|
+
*
|
|
691
|
+
* @param field - The password field to render
|
|
692
|
+
* @param styler - The styler used to render the view
|
|
693
|
+
* @param theme - The optional terminal theme
|
|
694
|
+
* @returns The initial immutable key state
|
|
695
|
+
*/
|
|
696
|
+
function createPasswordState(field, styler = createStyler(), theme) {
|
|
797
697
|
return {
|
|
798
|
-
message:
|
|
799
|
-
mask:
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
theme: createPromptTheme(options.theme),
|
|
698
|
+
message: sanitizeDisplayText(field.label ?? field.name),
|
|
699
|
+
mask: sanitizeDisplayText(field.mask ?? "*"),
|
|
700
|
+
styler,
|
|
701
|
+
theme: createPromptTheme(theme),
|
|
803
702
|
value: ""
|
|
804
703
|
};
|
|
805
704
|
}
|
|
806
|
-
/** Render a
|
|
705
|
+
/** Render a password-field key state as a styled view. */
|
|
807
706
|
function passwordView(state) {
|
|
808
707
|
const masked = state.styler.render(state.theme.roles.content, state.mask.repeat(state.value.length));
|
|
809
|
-
|
|
810
|
-
return state.error === void 0 ? head : `${head}\n${errorLine(state.styler, state.theme, state.error)}`;
|
|
708
|
+
return `${promptHeader(state.styler, state.theme, state.message)} ${state.styler.render(state.theme.roles.pointer, state.theme.icons.pointer)} ${masked}`;
|
|
811
709
|
}
|
|
812
710
|
/**
|
|
813
711
|
* Advance a password prompt by one {@link KeyEvent} — the pure `(state, key) → PromptStep<string>`
|
|
814
712
|
* reducer. Identical line-editing to {@link inputReduce} (printable extends, backspace shrinks,
|
|
815
|
-
* ctrl-u clears, ctrl-c cancels) but the view masks the value
|
|
816
|
-
* validator (no default fallback — a password has no echoed default).
|
|
713
|
+
* ctrl-u clears, ctrl-c cancels) but the view masks the value. Return produces the candidate value.
|
|
817
714
|
*/
|
|
818
715
|
function passwordReduce(state, key) {
|
|
819
716
|
if (key.ctrl && key.name === "c") return {
|
|
@@ -821,27 +718,12 @@ function passwordReduce(state, key) {
|
|
|
821
718
|
view: passwordView(state),
|
|
822
719
|
status: "cancel"
|
|
823
720
|
};
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
error: result
|
|
831
|
-
};
|
|
832
|
-
return {
|
|
833
|
-
state: next,
|
|
834
|
-
view: passwordView(next),
|
|
835
|
-
status: "active"
|
|
836
|
-
};
|
|
837
|
-
}
|
|
838
|
-
return {
|
|
839
|
-
state: clear,
|
|
840
|
-
view: `${submitHeader(state.styler, state.theme, state.message)} ${state.styler.render(state.theme.roles.hint, state.mask.repeat(state.value.length))}`,
|
|
841
|
-
status: "submit",
|
|
842
|
-
value: state.value
|
|
843
|
-
};
|
|
844
|
-
}
|
|
721
|
+
if (key.name === "return") return {
|
|
722
|
+
state,
|
|
723
|
+
view: `${submitHeader(state.styler, state.theme, state.message)} ${state.styler.render(state.theme.roles.hint, state.mask.repeat(state.value.length))}`,
|
|
724
|
+
status: "submit",
|
|
725
|
+
value: state.value
|
|
726
|
+
};
|
|
845
727
|
const value = editLine(state.value, key);
|
|
846
728
|
if (value === void 0) return {
|
|
847
729
|
state,
|
|
@@ -849,7 +731,7 @@ function passwordReduce(state, key) {
|
|
|
849
731
|
status: "active"
|
|
850
732
|
};
|
|
851
733
|
const next = {
|
|
852
|
-
...
|
|
734
|
+
...state,
|
|
853
735
|
value
|
|
854
736
|
};
|
|
855
737
|
return {
|
|
@@ -858,23 +740,26 @@ function passwordReduce(state, key) {
|
|
|
858
740
|
status: "active"
|
|
859
741
|
};
|
|
860
742
|
}
|
|
861
|
-
/**
|
|
862
|
-
|
|
743
|
+
/**
|
|
744
|
+
* Build the initial confirm-field key state.
|
|
745
|
+
*
|
|
746
|
+
* @param field - The confirm field to render
|
|
747
|
+
* @param styler - The styler used to render the view
|
|
748
|
+
* @param theme - The optional terminal theme
|
|
749
|
+
* @returns The initial immutable key state
|
|
750
|
+
*/
|
|
751
|
+
function createConfirmState(field, styler = createStyler(), theme) {
|
|
863
752
|
return {
|
|
864
|
-
message:
|
|
865
|
-
default:
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
theme: createPromptTheme(options.theme)
|
|
753
|
+
message: sanitizeDisplayText(field.label ?? field.name),
|
|
754
|
+
default: field.default ?? false,
|
|
755
|
+
styler,
|
|
756
|
+
theme: createPromptTheme(theme)
|
|
869
757
|
};
|
|
870
758
|
}
|
|
871
759
|
/**
|
|
872
|
-
* Render a
|
|
873
|
-
* default letter painted by the `selected` role. A supplied `hint` replaces that whole group,
|
|
874
|
-
* parentheses included, painted by the `hint` role.
|
|
760
|
+
* Render a confirm-field key state as a styled view. The selected role paints the default letter.
|
|
875
761
|
*/
|
|
876
762
|
function confirmView(state) {
|
|
877
|
-
if (state.hint !== void 0) return hintedHeader(state.styler, state.theme, state.message, state.hint);
|
|
878
763
|
const head = promptHeader(state.styler, state.theme, state.message);
|
|
879
764
|
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")}`;
|
|
880
765
|
return `${head} ${state.styler.render(state.theme.roles.hint, "(")}${answer}${state.styler.render(state.theme.roles.hint, ")")}`;
|
|
@@ -907,26 +792,32 @@ function confirmReduce(state, key) {
|
|
|
907
792
|
value: answer
|
|
908
793
|
};
|
|
909
794
|
}
|
|
910
|
-
/**
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
|
|
795
|
+
/**
|
|
796
|
+
* Build the initial select-field key state.
|
|
797
|
+
*
|
|
798
|
+
* @param field - The select field to render
|
|
799
|
+
* @param styler - The styler used to render the view
|
|
800
|
+
* @param theme - The optional terminal theme
|
|
801
|
+
* @returns The initial immutable key state
|
|
802
|
+
*/
|
|
803
|
+
function createSelectState(field, styler = createStyler(), theme) {
|
|
804
|
+
const choices = [...field.choices];
|
|
805
|
+
const index = choices.findIndex((choice) => choice.value === field.default);
|
|
914
806
|
return {
|
|
915
|
-
message:
|
|
807
|
+
message: sanitizeDisplayText(field.label ?? field.name),
|
|
916
808
|
choices,
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
theme: createPromptTheme(options.theme),
|
|
809
|
+
styler,
|
|
810
|
+
theme: createPromptTheme(theme),
|
|
920
811
|
focused: index >= 0 ? index : 0
|
|
921
812
|
};
|
|
922
813
|
}
|
|
923
|
-
/** Render a
|
|
814
|
+
/** Render a select-field key state as a multi-line styled view. */
|
|
924
815
|
function selectView(state) {
|
|
925
816
|
const lines = state.choices.map((choice, index) => {
|
|
926
817
|
const active = index === state.focused;
|
|
927
|
-
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.
|
|
818
|
+
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)}`}`;
|
|
928
819
|
});
|
|
929
|
-
return [
|
|
820
|
+
return [promptHeader(state.styler, state.theme, state.message), ...lines].join("\n");
|
|
930
821
|
}
|
|
931
822
|
/**
|
|
932
823
|
* Advance a select prompt by one {@link KeyEvent} — the pure `(state, key) → PromptStep<string>`
|
|
@@ -973,7 +864,7 @@ function selectReduce(state, key) {
|
|
|
973
864
|
const value = choice?.value ?? "";
|
|
974
865
|
return {
|
|
975
866
|
state,
|
|
976
|
-
view: `${submitHeader(state.styler, state.theme, state.message)} ${state.styler.render(state.theme.roles.hint, choice?.
|
|
867
|
+
view: `${submitHeader(state.styler, state.theme, state.message)} ${state.styler.render(state.theme.roles.hint, choice?.label ?? "")}`,
|
|
977
868
|
status: "submit",
|
|
978
869
|
value
|
|
979
870
|
};
|
|
@@ -984,46 +875,48 @@ function selectReduce(state, key) {
|
|
|
984
875
|
status: "active"
|
|
985
876
|
};
|
|
986
877
|
}
|
|
987
|
-
/**
|
|
988
|
-
|
|
989
|
-
|
|
878
|
+
/**
|
|
879
|
+
* Build the initial checkbox-field key state.
|
|
880
|
+
*
|
|
881
|
+
* @param field - The checkbox field to render
|
|
882
|
+
* @param styler - The styler used to render the view
|
|
883
|
+
* @param theme - The optional terminal theme
|
|
884
|
+
* @returns The initial immutable key state
|
|
885
|
+
*/
|
|
886
|
+
function createCheckboxState(field, styler = createStyler(), theme) {
|
|
887
|
+
const choices = [...field.choices];
|
|
990
888
|
const checked = choices.reduce((indices, choice, index) => {
|
|
991
|
-
if (choice.
|
|
889
|
+
if (field.default?.includes(choice.value) === true) indices.push(index);
|
|
992
890
|
return indices;
|
|
993
891
|
}, []);
|
|
994
892
|
return {
|
|
995
|
-
message:
|
|
893
|
+
message: sanitizeDisplayText(field.label ?? field.name),
|
|
996
894
|
choices,
|
|
997
|
-
|
|
998
|
-
|
|
999
|
-
theme: createPromptTheme(options.theme),
|
|
895
|
+
styler,
|
|
896
|
+
theme: createPromptTheme(theme),
|
|
1000
897
|
focused: 0,
|
|
1001
|
-
checked
|
|
1002
|
-
...options.min !== void 0 ? { min: options.min } : {},
|
|
1003
|
-
...options.max !== void 0 ? { max: options.max } : {}
|
|
898
|
+
checked
|
|
1004
899
|
};
|
|
1005
900
|
}
|
|
1006
|
-
/** Render a
|
|
901
|
+
/** Render a checkbox-field key state as a multi-line styled view. */
|
|
1007
902
|
function checkboxView(state) {
|
|
1008
903
|
const lines = state.choices.map((choice, index) => {
|
|
1009
904
|
const active = index === state.focused;
|
|
1010
905
|
const ticked = state.checked.includes(index);
|
|
1011
|
-
return `${active ? state.styler.render(state.theme.roles.pointer, state.theme.icons.pointer) : " "} ${ticked ? state.styler.render(state.theme.roles.selected, state.theme.icons.checked) : state.styler.render(state.theme.roles.muted, state.theme.icons.unchecked)} ${active ? state.styler.render(state.theme.roles.focus, choice.
|
|
906
|
+
return `${active ? state.styler.render(state.theme.roles.pointer, state.theme.icons.pointer) : " "} ${ticked ? state.styler.render(state.theme.roles.selected, state.theme.icons.checked) : state.styler.render(state.theme.roles.muted, state.theme.icons.unchecked)} ${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)}`}`;
|
|
1012
907
|
});
|
|
1013
908
|
const summary = state.styler.render(state.theme.roles.hint, `${state.checked.length} selected`);
|
|
1014
|
-
|
|
1015
|
-
|
|
909
|
+
return [
|
|
910
|
+
promptHeader(state.styler, state.theme, state.message),
|
|
1016
911
|
...lines,
|
|
1017
912
|
summary
|
|
1018
913
|
].join("\n");
|
|
1019
|
-
return state.error === void 0 ? body : `${body}\n${errorLine(state.styler, state.theme, state.error)}`;
|
|
1020
914
|
}
|
|
1021
915
|
/**
|
|
1022
916
|
* Advance a checkbox prompt by one {@link KeyEvent} — the pure
|
|
1023
917
|
* `(state, key) → PromptStep<readonly string[]>` reducer. `up` / `down` (and `k` / `j`) move the
|
|
1024
918
|
* focus (wrapping); `space` toggles the focused index in the checked set; return submits the
|
|
1025
|
-
* checked values in
|
|
1026
|
-
* the reason in the view); ctrl-c cancels.
|
|
919
|
+
* checked values in choice order; ctrl-c cancels. The form applies selection-count rules.
|
|
1027
920
|
*/
|
|
1028
921
|
function checkboxReduce(state, key) {
|
|
1029
922
|
if (key.ctrl && key.name === "c") return {
|
|
@@ -1032,10 +925,9 @@ function checkboxReduce(state, key) {
|
|
|
1032
925
|
status: "cancel"
|
|
1033
926
|
};
|
|
1034
927
|
const count = state.choices.length;
|
|
1035
|
-
const { error: _error, ...clear } = state;
|
|
1036
928
|
if ((key.name === "up" || key.name === "k") && count > 0) {
|
|
1037
929
|
const next = {
|
|
1038
|
-
...
|
|
930
|
+
...state,
|
|
1039
931
|
focused: (state.focused - 1 + count) % count
|
|
1040
932
|
};
|
|
1041
933
|
return {
|
|
@@ -1046,7 +938,7 @@ function checkboxReduce(state, key) {
|
|
|
1046
938
|
}
|
|
1047
939
|
if ((key.name === "down" || key.name === "j") && count > 0) {
|
|
1048
940
|
const next = {
|
|
1049
|
-
...
|
|
941
|
+
...state,
|
|
1050
942
|
focused: (state.focused + 1) % count
|
|
1051
943
|
};
|
|
1052
944
|
return {
|
|
@@ -1058,7 +950,7 @@ function checkboxReduce(state, key) {
|
|
|
1058
950
|
if (key.name === "space" && count > 0) {
|
|
1059
951
|
const checked = toggleIndex(state.checked, state.focused);
|
|
1060
952
|
const next = {
|
|
1061
|
-
...
|
|
953
|
+
...state,
|
|
1062
954
|
checked
|
|
1063
955
|
};
|
|
1064
956
|
return {
|
|
@@ -1068,23 +960,11 @@ function checkboxReduce(state, key) {
|
|
|
1068
960
|
};
|
|
1069
961
|
}
|
|
1070
962
|
if (key.name === "return") {
|
|
1071
|
-
const error = gateSelection(state.checked.length, state.min, state.max);
|
|
1072
|
-
if (error !== void 0) {
|
|
1073
|
-
const next = {
|
|
1074
|
-
...state,
|
|
1075
|
-
error
|
|
1076
|
-
};
|
|
1077
|
-
return {
|
|
1078
|
-
state: next,
|
|
1079
|
-
view: checkboxView(next),
|
|
1080
|
-
status: "active"
|
|
1081
|
-
};
|
|
1082
|
-
}
|
|
1083
963
|
const ordered = [...state.checked].sort((a, b) => a - b);
|
|
1084
964
|
const values = ordered.map((index) => state.choices[index]?.value).filter((value) => value !== void 0);
|
|
1085
|
-
const summary = ordered.map((index) => state.choices[index]?.
|
|
965
|
+
const summary = ordered.map((index) => state.choices[index]?.label).filter((name) => name !== void 0).join(", ");
|
|
1086
966
|
return {
|
|
1087
|
-
state
|
|
967
|
+
state,
|
|
1088
968
|
view: `${submitHeader(state.styler, state.theme, state.message)} ${state.styler.render(state.theme.roles.hint, summary)}`,
|
|
1089
969
|
status: "submit",
|
|
1090
970
|
value: values
|
|
@@ -1100,41 +980,37 @@ function checkboxReduce(state, key) {
|
|
|
1100
980
|
function toggleIndex(indices, index) {
|
|
1101
981
|
return indices.includes(index) ? indices.filter((i) => i !== index) : [...indices, index];
|
|
1102
982
|
}
|
|
1103
|
-
/**
|
|
1104
|
-
|
|
1105
|
-
|
|
1106
|
-
|
|
1107
|
-
|
|
1108
|
-
|
|
1109
|
-
|
|
983
|
+
/**
|
|
984
|
+
* Build the initial editor-field key state.
|
|
985
|
+
*
|
|
986
|
+
* @param field - The editor field to render
|
|
987
|
+
* @param styler - The styler used to render the view
|
|
988
|
+
* @param theme - The optional terminal theme
|
|
989
|
+
* @returns The initial immutable key state
|
|
990
|
+
*/
|
|
991
|
+
function createEditorState(field, styler = createStyler(), theme) {
|
|
1110
992
|
return {
|
|
1111
|
-
message:
|
|
1112
|
-
default:
|
|
1113
|
-
|
|
1114
|
-
|
|
1115
|
-
styler: options.styler ?? createStyler(),
|
|
1116
|
-
theme: createPromptTheme(options.theme),
|
|
993
|
+
message: sanitizeDisplayText(field.label ?? field.name),
|
|
994
|
+
default: field.default ?? "",
|
|
995
|
+
styler,
|
|
996
|
+
theme: createPromptTheme(theme),
|
|
1117
997
|
lines: [],
|
|
1118
998
|
current: ""
|
|
1119
999
|
};
|
|
1120
1000
|
}
|
|
1121
1001
|
/**
|
|
1122
|
-
* Render an
|
|
1123
|
-
* committed and in-progress lines painted by the `content` role, and any error. A supplied `hint`
|
|
1124
|
-
* replaces `(Ctrl+D to finish)` in full; the finish key stays ctrl-d.
|
|
1002
|
+
* Render an editor-field key state as a multi-line styled view with its finish hint.
|
|
1125
1003
|
*/
|
|
1126
1004
|
function editorView(state) {
|
|
1127
|
-
const head = hintedHeader(state.styler, state.theme, state.message,
|
|
1005
|
+
const head = hintedHeader(state.styler, state.theme, state.message, "(Ctrl+D to finish)");
|
|
1128
1006
|
const pointer = state.styler.render(state.theme.roles.pointer, state.theme.icons.pointer);
|
|
1129
|
-
|
|
1130
|
-
return state.error === void 0 ? view : `${view}\n${errorLine(state.styler, state.theme, state.error)}`;
|
|
1007
|
+
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");
|
|
1131
1008
|
}
|
|
1132
1009
|
/**
|
|
1133
1010
|
* Advance an editor prompt by one {@link KeyEvent} — the pure `(state, key) → PromptStep<string>`
|
|
1134
1011
|
* reducer. Printable characters extend the current line; backspace shrinks it; return commits the
|
|
1135
1012
|
* current line and starts a fresh one; ctrl-d FINISHES (joining all lines, falling back to the
|
|
1136
|
-
* default when empty)
|
|
1137
|
-
* the error.
|
|
1013
|
+
* default when empty); ctrl-c cancels. The form validates the candidate after the driver fills it.
|
|
1138
1014
|
*/
|
|
1139
1015
|
function editorReduce(state, key) {
|
|
1140
1016
|
if (key.ctrl && key.name === "c") return {
|
|
@@ -1142,25 +1018,12 @@ function editorReduce(state, key) {
|
|
|
1142
1018
|
view: editorView(state),
|
|
1143
1019
|
status: "cancel"
|
|
1144
1020
|
};
|
|
1145
|
-
const { error: _error, ...clear } = state;
|
|
1146
1021
|
if (key.ctrl && key.name === "d") {
|
|
1147
1022
|
const lines = state.current.length > 0 ? [...state.lines, state.current] : state.lines;
|
|
1148
1023
|
const joined = lines.join("\n");
|
|
1149
1024
|
const answer = joined.length > 0 ? joined : state.default;
|
|
1150
|
-
const result = state.validator(answer);
|
|
1151
|
-
if (result !== true) {
|
|
1152
|
-
const next = {
|
|
1153
|
-
...state,
|
|
1154
|
-
error: result
|
|
1155
|
-
};
|
|
1156
|
-
return {
|
|
1157
|
-
state: next,
|
|
1158
|
-
view: editorView(next),
|
|
1159
|
-
status: "active"
|
|
1160
|
-
};
|
|
1161
|
-
}
|
|
1162
1025
|
return {
|
|
1163
|
-
state
|
|
1026
|
+
state,
|
|
1164
1027
|
view: `${submitHeader(state.styler, state.theme, state.message)} ${state.styler.render(state.theme.roles.hint, `${String(lines.length)} line${lines.length === 1 ? "" : "s"}`)}`,
|
|
1165
1028
|
status: "submit",
|
|
1166
1029
|
value: answer
|
|
@@ -1168,7 +1031,7 @@ function editorReduce(state, key) {
|
|
|
1168
1031
|
}
|
|
1169
1032
|
if (key.name === "return") {
|
|
1170
1033
|
const next = {
|
|
1171
|
-
...
|
|
1034
|
+
...state,
|
|
1172
1035
|
lines: [...state.lines, state.current],
|
|
1173
1036
|
current: ""
|
|
1174
1037
|
};
|
|
@@ -1185,7 +1048,7 @@ function editorReduce(state, key) {
|
|
|
1185
1048
|
status: "active"
|
|
1186
1049
|
};
|
|
1187
1050
|
const next = {
|
|
1188
|
-
...
|
|
1051
|
+
...state,
|
|
1189
1052
|
current
|
|
1190
1053
|
};
|
|
1191
1054
|
return {
|
|
@@ -1207,227 +1070,6 @@ function editLine(value, key) {
|
|
|
1207
1070
|
if (!key.ctrl && !key.meta && [...key.name].length === 1 && isPrintable(key.name)) return `${value}${key.sequence}`;
|
|
1208
1071
|
}
|
|
1209
1072
|
/**
|
|
1210
|
-
* Produce the WIRE-SAFE form of a prompt's options — the {@link PendingPrompt.options} a broker
|
|
1211
|
-
* serializes over SSE. Drops everything that cannot cross the wire (a `styler`, and any
|
|
1212
|
-
* function-valued `validate` rule), while KEEPING the declarative data a remote client needs to
|
|
1213
|
-
* reconstruct the prompt: the {@link ValidationRules} as data, plus `message` / `choices` /
|
|
1214
|
-
* `default` / `mask` / `min` / `max` / `hint` / `theme`. This is WHY T-a's validation is
|
|
1215
|
-
* rules-as-data, and why a theme is data too: a `Validator`
|
|
1216
|
-
* FUNCTION can't be serialized, but its declarative rules can — the client rebuilds the validator
|
|
1217
|
-
* from them via {@link resolveValidation}.
|
|
1218
|
-
*
|
|
1219
|
-
* @remarks
|
|
1220
|
-
* - **Functions are dropped.** A top-level function value (e.g. a bare-`Validator` `validate`) is
|
|
1221
|
-
* omitted entirely; the `styler` (a fluent function-bearing object) is dropped by key.
|
|
1222
|
-
* - **`validate` rules are flattened.** A {@link ValidationRules} bag is copied with each
|
|
1223
|
-
* function rule replaced by `true` (the rule's INTENT survives as the built-in check; its
|
|
1224
|
-
* custom function does not). A bare-function `validate` is dropped (no declarative form to keep).
|
|
1225
|
-
* - **`choices` are function-stripped.** Each choice keeps its plain fields (`name` / `value` /
|
|
1226
|
-
* `description` / `checked`); a bare string passes through.
|
|
1227
|
-
*
|
|
1228
|
-
* @param options - The raw prompt options bag (may hold functions / a styler)
|
|
1229
|
-
* @returns A JSON-safe options record — only serializable, declarative data
|
|
1230
|
-
*/
|
|
1231
|
-
function serializePromptOptions(options) {
|
|
1232
|
-
const result = {};
|
|
1233
|
-
for (const [key, value] of Object.entries(options)) {
|
|
1234
|
-
if (key === "styler" || typeof value === "function") continue;
|
|
1235
|
-
if (key === "validate") {
|
|
1236
|
-
const rules = serializeValidationRules(value);
|
|
1237
|
-
if (rules !== void 0) result[key] = rules;
|
|
1238
|
-
continue;
|
|
1239
|
-
}
|
|
1240
|
-
if (key === "choices") {
|
|
1241
|
-
result[key] = serializeChoices(value);
|
|
1242
|
-
continue;
|
|
1243
|
-
}
|
|
1244
|
-
result[key] = value;
|
|
1245
|
-
}
|
|
1246
|
-
return result;
|
|
1247
|
-
}
|
|
1248
|
-
/**
|
|
1249
|
-
* Flatten a `validate` option to its wire-safe {@link ValidationRules} DATA — a function rule
|
|
1250
|
-
* becomes `true` (its built-in check survives; its body cannot cross the wire). A bare-function
|
|
1251
|
-
* `validate` (no rules object) has no declarative form, so it yields `undefined` (dropped).
|
|
1252
|
-
*/
|
|
1253
|
-
function serializeValidationRules(validate) {
|
|
1254
|
-
if (!isRecord(validate)) return void 0;
|
|
1255
|
-
const rules = {};
|
|
1256
|
-
for (const [rule, value] of Object.entries(validate)) rules[rule] = typeof value === "function" ? true : value;
|
|
1257
|
-
return rules;
|
|
1258
|
-
}
|
|
1259
|
-
/** Strip functions from a `choices` option — each choice keeps its plain fields; a bare string passes through. */
|
|
1260
|
-
function serializeChoices(choices) {
|
|
1261
|
-
if (!Array.isArray(choices)) return [];
|
|
1262
|
-
return choices.map((choice) => {
|
|
1263
|
-
if (isString(choice)) return choice;
|
|
1264
|
-
if (!isRecord(choice)) return choice;
|
|
1265
|
-
const normalized = {};
|
|
1266
|
-
for (const [key, value] of Object.entries(choice)) if (typeof value !== "function") normalized[key] = value;
|
|
1267
|
-
return normalized;
|
|
1268
|
-
});
|
|
1269
|
-
}
|
|
1270
|
-
/**
|
|
1271
|
-
* Rebuild a wire-decoded `validate` payload into a {@link ValidationRules} bag — the inverse of
|
|
1272
|
-
* {@link serializeValidationRules}. Keeps only the primitive rule values (`boolean` / `number` /
|
|
1273
|
-
* `string`) a serialized prompt could carry; an empty / non-record / all-dropped payload yields
|
|
1274
|
-
* `undefined` (no rules to apply). The client feeds the result back through {@link resolveValidation}.
|
|
1275
|
-
*/
|
|
1276
|
-
function reconstructValidationRules(value) {
|
|
1277
|
-
if (!isRecord(value)) return void 0;
|
|
1278
|
-
const rules = {};
|
|
1279
|
-
let count = 0;
|
|
1280
|
-
for (const [rule, item] of Object.entries(value)) {
|
|
1281
|
-
if (rule === "pattern") continue;
|
|
1282
|
-
if (isBoolean(item) || isNumber(item) || isString(item)) {
|
|
1283
|
-
rules[rule] = item;
|
|
1284
|
-
count += 1;
|
|
1285
|
-
}
|
|
1286
|
-
}
|
|
1287
|
-
if (count === 0) return void 0;
|
|
1288
|
-
return rules;
|
|
1289
|
-
}
|
|
1290
|
-
/** Read one option by key, narrowed by `guard` — `undefined` when absent or off-shape (§14, never an `as`). */
|
|
1291
|
-
function resolveOption(options, key, guard) {
|
|
1292
|
-
const value = options[key];
|
|
1293
|
-
return guard(value) ? value : void 0;
|
|
1294
|
-
}
|
|
1295
|
-
/** Narrow an unknown value to a {@link PromptChoice} — the `recordOf` shape inlined so no non-exported member lingers (§5). */
|
|
1296
|
-
function isPromptChoice(value) {
|
|
1297
|
-
return recordOf({
|
|
1298
|
-
name: isString,
|
|
1299
|
-
value: isString,
|
|
1300
|
-
description: isString
|
|
1301
|
-
}, ["description"])(value);
|
|
1302
|
-
}
|
|
1303
|
-
/** Narrow an unknown value to a {@link CheckboxChoice} — the `recordOf` shape inlined so no non-exported member lingers (§5). */
|
|
1304
|
-
function isCheckboxChoice(value) {
|
|
1305
|
-
return recordOf({
|
|
1306
|
-
name: isString,
|
|
1307
|
-
value: isString,
|
|
1308
|
-
description: isString,
|
|
1309
|
-
checked: isBoolean
|
|
1310
|
-
}, ["description", "checked"])(value);
|
|
1311
|
-
}
|
|
1312
|
-
/** Read a `choices` option as a list of bare strings / full choices — each element narrowed by `guard`, off-shape elements stringified. */
|
|
1313
|
-
function resolveChoices(options, guard) {
|
|
1314
|
-
const choices = options.choices;
|
|
1315
|
-
if (!Array.isArray(choices)) return [];
|
|
1316
|
-
return choices.map((choice) => {
|
|
1317
|
-
if (isString(choice)) return choice;
|
|
1318
|
-
if (guard(choice)) return choice;
|
|
1319
|
-
return String(choice);
|
|
1320
|
-
});
|
|
1321
|
-
}
|
|
1322
|
-
/**
|
|
1323
|
-
* Sanitize a list of resolved choices' human-readable labels (`name` + `description`) with
|
|
1324
|
-
* {@link stripControls} — shared by the `select` and `checkbox` branches of
|
|
1325
|
-
* {@link dispatchPendingPrompt} so a remote-supplied choice can never inject raw control bytes
|
|
1326
|
-
* into the local terminal's rendered view.
|
|
1327
|
-
*
|
|
1328
|
-
* @param choices - The resolved choices (bare strings or full {@link PromptChoice} /
|
|
1329
|
-
* {@link CheckboxChoice} objects) as returned by {@link resolveChoices}
|
|
1330
|
-
* @returns The same choices with every `name` / `description` control-stripped
|
|
1331
|
-
*/
|
|
1332
|
-
function sanitizeChoiceLabels(choices) {
|
|
1333
|
-
return choices.map((choice) => {
|
|
1334
|
-
if (isString(choice)) return stripControls(choice);
|
|
1335
|
-
return {
|
|
1336
|
-
...choice,
|
|
1337
|
-
name: stripControls(choice.name),
|
|
1338
|
-
...choice.description !== void 0 ? { description: stripControls(choice.description) } : {}
|
|
1339
|
-
};
|
|
1340
|
-
});
|
|
1341
|
-
}
|
|
1342
|
-
/**
|
|
1343
|
-
* Dispatch a {@link PendingPrompt} to the matching {@link PromptFormInterface} method — the bridge
|
|
1344
|
-
* step a {@link PromptClient} runs to drive a LOCAL terminal with a prompt issued elsewhere.
|
|
1345
|
-
* Reconstructs typed options from the wire-safe {@link PendingPrompt.options} (every field
|
|
1346
|
-
* §14-narrowed, never an `as`; the validator rebuilt from rules via {@link reconstructValidationRules}),
|
|
1347
|
-
* then calls the matching prompt form and returns its resolved value.
|
|
1348
|
-
*
|
|
1349
|
-
* @remarks
|
|
1350
|
-
* `theme` and `hint` are plain data, so they cross the wire with the rest of the options bag and
|
|
1351
|
-
* are reconstructed here for every form that accepts them: the theme is narrowed by
|
|
1352
|
-
* {@link import('./validators.js').isPromptThemeOptions} (an off-shape theme is dropped, leaving
|
|
1353
|
-
* the defaults), then every glyph and hint is passed through {@link sanitizeDisplayText}. Choice
|
|
1354
|
-
* labels use the stream-safe {@link sanitizeChoiceLabels} treatment instead.
|
|
1355
|
-
*
|
|
1356
|
-
* @param terminal - The local {@link PromptFormInterface} to drive
|
|
1357
|
-
* @param pending - The decoded pending prompt to dispatch
|
|
1358
|
-
* @returns The prompt's resolved value (a `string` / `boolean` / `readonly string[]` per form)
|
|
1359
|
-
*/
|
|
1360
|
-
function dispatchPendingPrompt(terminal, pending) {
|
|
1361
|
-
const options = pending.options;
|
|
1362
|
-
const validate = reconstructValidationRules(options.validate);
|
|
1363
|
-
const message = stripControls(pending.message);
|
|
1364
|
-
const supplied = resolveOption(options, "theme", isPromptThemeOptions);
|
|
1365
|
-
const theme = supplied === void 0 ? void 0 : sanitizeThemeIcons(supplied);
|
|
1366
|
-
const raw = resolveOption(options, "hint", isString);
|
|
1367
|
-
const hint = raw === void 0 ? void 0 : sanitizeDisplayText(raw);
|
|
1368
|
-
switch (pending.form) {
|
|
1369
|
-
case "input": {
|
|
1370
|
-
const value = resolveOption(options, "default", isString);
|
|
1371
|
-
return terminal.input({
|
|
1372
|
-
message,
|
|
1373
|
-
...value !== void 0 ? { default: stripControls(value) } : {},
|
|
1374
|
-
...validate !== void 0 ? { validate } : {},
|
|
1375
|
-
...theme !== void 0 ? { theme } : {}
|
|
1376
|
-
});
|
|
1377
|
-
}
|
|
1378
|
-
case "password": {
|
|
1379
|
-
const value = resolveOption(options, "mask", isString);
|
|
1380
|
-
return terminal.password({
|
|
1381
|
-
message,
|
|
1382
|
-
...value !== void 0 ? { mask: stripControls(value) } : {},
|
|
1383
|
-
...validate !== void 0 ? { validate } : {},
|
|
1384
|
-
...theme !== void 0 ? { theme } : {}
|
|
1385
|
-
});
|
|
1386
|
-
}
|
|
1387
|
-
case "confirm": {
|
|
1388
|
-
const value = resolveOption(options, "default", isBoolean);
|
|
1389
|
-
return terminal.confirm({
|
|
1390
|
-
message,
|
|
1391
|
-
...value !== void 0 ? { default: value } : {},
|
|
1392
|
-
...hint !== void 0 ? { hint } : {},
|
|
1393
|
-
...theme !== void 0 ? { theme } : {}
|
|
1394
|
-
});
|
|
1395
|
-
}
|
|
1396
|
-
case "select": {
|
|
1397
|
-
const value = resolveOption(options, "default", isString);
|
|
1398
|
-
return terminal.select({
|
|
1399
|
-
message,
|
|
1400
|
-
choices: sanitizeChoiceLabels(resolveChoices(options, isPromptChoice)),
|
|
1401
|
-
...value !== void 0 ? { default: stripControls(value) } : {},
|
|
1402
|
-
...hint !== void 0 ? { hint } : {},
|
|
1403
|
-
...theme !== void 0 ? { theme } : {}
|
|
1404
|
-
});
|
|
1405
|
-
}
|
|
1406
|
-
case "checkbox": {
|
|
1407
|
-
const min = resolveOption(options, "min", isNumber);
|
|
1408
|
-
const max = resolveOption(options, "max", isNumber);
|
|
1409
|
-
return terminal.checkbox({
|
|
1410
|
-
message,
|
|
1411
|
-
choices: sanitizeChoiceLabels(resolveChoices(options, isCheckboxChoice)),
|
|
1412
|
-
...min !== void 0 ? { min } : {},
|
|
1413
|
-
...max !== void 0 ? { max } : {},
|
|
1414
|
-
...hint !== void 0 ? { hint } : {},
|
|
1415
|
-
...theme !== void 0 ? { theme } : {}
|
|
1416
|
-
});
|
|
1417
|
-
}
|
|
1418
|
-
case "editor": {
|
|
1419
|
-
const value = resolveOption(options, "default", isString);
|
|
1420
|
-
return terminal.editor({
|
|
1421
|
-
message,
|
|
1422
|
-
...value !== void 0 ? { default: stripControls(value) } : {},
|
|
1423
|
-
...validate !== void 0 ? { validate } : {},
|
|
1424
|
-
...hint !== void 0 ? { hint } : {},
|
|
1425
|
-
...theme !== void 0 ? { theme } : {}
|
|
1426
|
-
});
|
|
1427
|
-
}
|
|
1428
|
-
}
|
|
1429
|
-
}
|
|
1430
|
-
/**
|
|
1431
1073
|
* The default {@link import('./types.js').TimerHandler} — a thin host `setTimeout` / `clearTimeout`
|
|
1432
1074
|
* wrapper that arms `callback` after `ms` and returns a {@link TimerCancel}. The deadline seam
|
|
1433
1075
|
* behind both the {@link import('./Prompt.js').Prompt} broker (its expiry) and the
|
|
@@ -1478,12 +1120,12 @@ function isInsecureRemote(url) {
|
|
|
1478
1120
|
const hostname = host.startsWith("[") ? host.slice(0, host.indexOf("]") + 1) : host.split(":")[0] ?? "";
|
|
1479
1121
|
return hostname !== "localhost" && hostname !== "127.0.0.1" && hostname !== "[::1]";
|
|
1480
1122
|
}
|
|
1481
|
-
/** Serialize a parked {@link
|
|
1482
|
-
function serializePending(
|
|
1123
|
+
/** Serialize a parked {@link PendingForm} into a {@link WireEvent}. */
|
|
1124
|
+
function serializePending(form) {
|
|
1483
1125
|
return {
|
|
1484
1126
|
event: "pending",
|
|
1485
|
-
data: JSON.stringify(
|
|
1486
|
-
id:
|
|
1127
|
+
data: JSON.stringify(form),
|
|
1128
|
+
id: form.id
|
|
1487
1129
|
};
|
|
1488
1130
|
}
|
|
1489
1131
|
/** Serialize a parked prompt's expiry into a {@link WireEvent} — event `'expire'`, `data` the JSON-stringified `{ id }` payload. */
|
|
@@ -1500,44 +1142,24 @@ function serializeShutdown() {
|
|
|
1500
1142
|
data: ""
|
|
1501
1143
|
};
|
|
1502
1144
|
}
|
|
1503
|
-
/** Narrow an unknown wire payload to an answer POST body — a non-empty `id` string plus a `value` key (of any shape) present. */
|
|
1504
|
-
function isAnswerPayload(value) {
|
|
1505
|
-
return isRecord(value) && isNonEmptyString(value.id) && "value" in value;
|
|
1506
|
-
}
|
|
1507
1145
|
//#endregion
|
|
1508
1146
|
//#region src/core/Prompt.ts
|
|
1509
1147
|
/**
|
|
1510
|
-
* The headless
|
|
1511
|
-
*
|
|
1512
|
-
* rejects on timeout / teardown). The tri-surface's headless arm: there is no terminal here, so a
|
|
1513
|
-
* transport forwards each `pending` event to whoever can answer, and {@link answer} resolves the
|
|
1514
|
-
* parked Promise — for environments where direct user access is unavailable (a headless server on
|
|
1515
|
-
* stdio, a browser with no TTY, a prompt issued on one machine answered on another).
|
|
1148
|
+
* The headless form broker. It parks live forms, exposes their serialized schemas, and applies
|
|
1149
|
+
* remote answers to the authoritative form.
|
|
1516
1150
|
*
|
|
1517
1151
|
* @remarks
|
|
1518
|
-
*
|
|
1519
|
-
*
|
|
1520
|
-
*
|
|
1521
|
-
* ({@link serializePromptOptions}) so a transport can forward them as-is.
|
|
1522
|
-
* - **Answer validates + type-checks.** {@link answer} runs the prompt's per-form gate: it
|
|
1523
|
-
* type-checks `value` to the form (string / boolean / string[]) AND, for the text forms, runs
|
|
1524
|
-
* the validator resolved from the original `validate` rules. A rejected answer returns a failure
|
|
1525
|
-
* `Result` with error `'rejected'` and the prompt stays `pending`; an accepted answer resolves the
|
|
1526
|
-
* Promise, emits `answer`, and removes the prompt.
|
|
1527
|
-
* - **Timeout → expire → reject.** An unanswered prompt expires after `timeout` ms (via the
|
|
1528
|
-
* INJECTED timer): `expire` fires and the parked Promise rejects with a {@link TerminalError}
|
|
1529
|
-
* (`code: 'EXPIRE'`). {@link destroy} expires every still-pending prompt the same way.
|
|
1530
|
-
* - **Deterministic.** The timer is injectable ({@link import('./types.js').TimerHandler}); a test
|
|
1531
|
-
* supplies one that fires on demand, so expiry is driven without real time.
|
|
1152
|
+
* A parked record carries one call to `serializeForm`. A failed fill or submit leaves the record
|
|
1153
|
+
* parked for another answer. A successful submit settles the form once, emits `answer`, and removes
|
|
1154
|
+
* the record. Timeout and teardown abandon every unsettled form through its own `destroy` method.
|
|
1532
1155
|
*
|
|
1533
1156
|
* @example
|
|
1534
1157
|
* ```ts
|
|
1535
|
-
* const
|
|
1536
|
-
* prompt
|
|
1537
|
-
*
|
|
1538
|
-
*
|
|
1539
|
-
*
|
|
1540
|
-
* prompt.answer(id, 'Ada') // resolves the awaited input() above
|
|
1158
|
+
* const form = createForm({ fields: [{ control: 'text', name: 'name' }] })
|
|
1159
|
+
* const prompt = createPrompt()
|
|
1160
|
+
* const id = prompt.park(form)
|
|
1161
|
+
* prompt.answer(id, { name: 'Ada' })
|
|
1162
|
+
* await form.answer // { name: 'Ada' }
|
|
1541
1163
|
* ```
|
|
1542
1164
|
*/
|
|
1543
1165
|
var Prompt = class {
|
|
@@ -1563,211 +1185,164 @@ var Prompt = class {
|
|
|
1563
1185
|
return this.#parked.size;
|
|
1564
1186
|
}
|
|
1565
1187
|
pending(id) {
|
|
1566
|
-
if (id !== void 0) return this.#parked.get(id)?.
|
|
1567
|
-
const
|
|
1568
|
-
for (const parked of this.#parked.values())
|
|
1569
|
-
return
|
|
1570
|
-
}
|
|
1571
|
-
|
|
1572
|
-
|
|
1573
|
-
|
|
1574
|
-
|
|
1575
|
-
|
|
1576
|
-
|
|
1577
|
-
|
|
1578
|
-
|
|
1579
|
-
|
|
1580
|
-
|
|
1188
|
+
if (id !== void 0) return this.#parked.get(id)?.pending;
|
|
1189
|
+
const forms = [];
|
|
1190
|
+
for (const parked of this.#parked.values()) forms.push(parked.pending);
|
|
1191
|
+
return forms;
|
|
1192
|
+
}
|
|
1193
|
+
park(form, request) {
|
|
1194
|
+
if (this.#destroyed) {
|
|
1195
|
+
form.destroy();
|
|
1196
|
+
throw new TerminalError("EXPIRE", "The broker has been destroyed");
|
|
1197
|
+
}
|
|
1198
|
+
if (this.#cap !== void 0 && this.#parked.size >= this.#cap) {
|
|
1199
|
+
form.destroy();
|
|
1200
|
+
throw new TerminalError("LIMIT", `The parked-form cap (${String(this.#cap)}) was reached`, { cap: this.#cap });
|
|
1201
|
+
}
|
|
1202
|
+
const id = crypto.randomUUID();
|
|
1203
|
+
const pending = {
|
|
1204
|
+
id,
|
|
1205
|
+
schema: serializeForm(form.schema),
|
|
1206
|
+
status: "pending",
|
|
1207
|
+
time: Date.now(),
|
|
1208
|
+
...request?.from !== void 0 ? { from: request.from } : {},
|
|
1209
|
+
...request?.to !== void 0 ? { to: request.to } : {}
|
|
1581
1210
|
};
|
|
1582
|
-
|
|
1583
|
-
this.#
|
|
1584
|
-
|
|
1211
|
+
const cancel = this.#timer(() => this.#expire(id), this.#timeout);
|
|
1212
|
+
this.#parked.set(id, {
|
|
1213
|
+
form,
|
|
1214
|
+
pending,
|
|
1215
|
+
cancel
|
|
1216
|
+
});
|
|
1217
|
+
this.#emitter.emit("pending", pending);
|
|
1218
|
+
return id;
|
|
1219
|
+
}
|
|
1220
|
+
answer(id, values) {
|
|
1221
|
+
const outcome = attempt(() => this.#answer(id, values));
|
|
1222
|
+
if (outcome.success) return outcome.value;
|
|
1585
1223
|
return {
|
|
1586
|
-
success:
|
|
1587
|
-
|
|
1224
|
+
success: false,
|
|
1225
|
+
error: {
|
|
1226
|
+
reason: "rejected",
|
|
1227
|
+
errors: [{
|
|
1228
|
+
field: "form",
|
|
1229
|
+
message: "The form rejected the answer"
|
|
1230
|
+
}]
|
|
1231
|
+
}
|
|
1588
1232
|
};
|
|
1589
1233
|
}
|
|
1590
|
-
park(request) {
|
|
1591
|
-
const gate = this.#gate(request.form, request.options);
|
|
1592
|
-
return this.#park(request.form, request.options.message, request.options, gate, request.from, request.to);
|
|
1593
|
-
}
|
|
1594
|
-
input(options) {
|
|
1595
|
-
return this.#park("input", options.message, options, this.#gate("input", options)).value;
|
|
1596
|
-
}
|
|
1597
|
-
password(options) {
|
|
1598
|
-
return this.#park("password", options.message, options, this.#gate("password", options)).value;
|
|
1599
|
-
}
|
|
1600
|
-
confirm(options) {
|
|
1601
|
-
return this.#park("confirm", options.message, options, this.#gate("confirm", options)).value;
|
|
1602
|
-
}
|
|
1603
|
-
select(options) {
|
|
1604
|
-
return this.#park("select", options.message, options, this.#gate("select", options)).value;
|
|
1605
|
-
}
|
|
1606
|
-
checkbox(options) {
|
|
1607
|
-
return this.#park("checkbox", options.message, options, this.#gate("checkbox", options)).value;
|
|
1608
|
-
}
|
|
1609
|
-
editor(options) {
|
|
1610
|
-
return this.#park("editor", options.message, options, this.#gate("editor", options)).value;
|
|
1611
|
-
}
|
|
1612
1234
|
destroy() {
|
|
1613
1235
|
if (this.#destroyed) return;
|
|
1614
1236
|
this.#destroyed = true;
|
|
1615
1237
|
for (const id of [...this.#parked.keys()]) this.#expire(id);
|
|
1616
|
-
this.#parked.clear();
|
|
1617
1238
|
this.#emitter.destroy();
|
|
1618
1239
|
}
|
|
1619
|
-
#
|
|
1620
|
-
|
|
1621
|
-
|
|
1622
|
-
|
|
1623
|
-
|
|
1624
|
-
if (this.#cap !== void 0 && this.#parked.size >= this.#cap) return {
|
|
1625
|
-
id: crypto.randomUUID(),
|
|
1626
|
-
value: Promise.reject(new TerminalError("LIMIT", `parked-prompt cap (${this.#cap}) reached`, { cap: this.#cap }))
|
|
1627
|
-
};
|
|
1628
|
-
const id = crypto.randomUUID();
|
|
1629
|
-
const prompt = {
|
|
1630
|
-
id,
|
|
1631
|
-
form,
|
|
1632
|
-
message,
|
|
1633
|
-
options: serializePromptOptions(options),
|
|
1634
|
-
status: "pending",
|
|
1635
|
-
time: Date.now(),
|
|
1636
|
-
...from !== void 0 ? { from } : {},
|
|
1637
|
-
...to !== void 0 ? { to } : {}
|
|
1638
|
-
};
|
|
1639
|
-
return {
|
|
1640
|
-
id,
|
|
1641
|
-
value: new Promise((resolve, reject) => {
|
|
1642
|
-
const cancel = this.#timer(() => this.#expire(id), this.#timeout);
|
|
1643
|
-
const respond = this.#createRespond(id, gate, resolve);
|
|
1644
|
-
const expire = this.#createExpire(id, reject);
|
|
1645
|
-
this.#parked.set(id, {
|
|
1646
|
-
prompt,
|
|
1647
|
-
respond,
|
|
1648
|
-
expire,
|
|
1649
|
-
cancel
|
|
1650
|
-
});
|
|
1651
|
-
this.#emitter.emit("pending", prompt);
|
|
1652
|
-
})
|
|
1653
|
-
};
|
|
1654
|
-
}
|
|
1655
|
-
#createRespond(id, gate, resolve) {
|
|
1656
|
-
return (answer) => {
|
|
1657
|
-
const accepted = gate(answer);
|
|
1658
|
-
if (accepted === void 0) return void 0;
|
|
1659
|
-
const current = this.#parked.get(id);
|
|
1660
|
-
if (current !== void 0) this.#parked.set(id, {
|
|
1661
|
-
...current,
|
|
1662
|
-
prompt: {
|
|
1663
|
-
...current.prompt,
|
|
1664
|
-
status: "answered"
|
|
1665
|
-
}
|
|
1666
|
-
});
|
|
1667
|
-
resolve(accepted);
|
|
1668
|
-
return accepted;
|
|
1240
|
+
#answer(id, values) {
|
|
1241
|
+
const parked = this.#parked.get(id);
|
|
1242
|
+
if (parked === void 0 || parked.pending.status !== "pending") return {
|
|
1243
|
+
success: false,
|
|
1244
|
+
error: { reason: "unknown" }
|
|
1669
1245
|
};
|
|
1670
|
-
|
|
1671
|
-
|
|
1672
|
-
|
|
1673
|
-
|
|
1674
|
-
|
|
1675
|
-
|
|
1676
|
-
case "input":
|
|
1677
|
-
case "password":
|
|
1678
|
-
case "editor": {
|
|
1679
|
-
if (!this.#isTextOptions(form, options)) return () => void 0;
|
|
1680
|
-
const validator = resolveValidation(options.validate);
|
|
1681
|
-
return (value) => isString(value) && validator(value) === true ? value : void 0;
|
|
1246
|
+
const outcome = attempt(() => this.#submit(parked.form, values));
|
|
1247
|
+
if (!outcome.success) return {
|
|
1248
|
+
success: false,
|
|
1249
|
+
error: {
|
|
1250
|
+
reason: "rejected",
|
|
1251
|
+
errors: this.#errors(outcome.error, parked.form)
|
|
1682
1252
|
}
|
|
1683
|
-
|
|
1684
|
-
|
|
1685
|
-
|
|
1686
|
-
|
|
1687
|
-
|
|
1688
|
-
|
|
1689
|
-
return (value) => isString(value) && values.has(value) ? value : void 0;
|
|
1253
|
+
};
|
|
1254
|
+
if (!outcome.value.success) return {
|
|
1255
|
+
success: false,
|
|
1256
|
+
error: {
|
|
1257
|
+
reason: "rejected",
|
|
1258
|
+
errors: outcome.value.error
|
|
1690
1259
|
}
|
|
1691
|
-
|
|
1692
|
-
|
|
1693
|
-
|
|
1694
|
-
|
|
1695
|
-
|
|
1696
|
-
|
|
1697
|
-
|
|
1698
|
-
if (min !== void 0 && value.length < min) return void 0;
|
|
1699
|
-
if (max !== void 0 && value.length > max) return void 0;
|
|
1700
|
-
return value;
|
|
1701
|
-
};
|
|
1260
|
+
};
|
|
1261
|
+
parked.cancel();
|
|
1262
|
+
const answered = {
|
|
1263
|
+
...parked,
|
|
1264
|
+
pending: {
|
|
1265
|
+
...parked.pending,
|
|
1266
|
+
status: "answered"
|
|
1702
1267
|
}
|
|
1268
|
+
};
|
|
1269
|
+
this.#parked.set(id, answered);
|
|
1270
|
+
this.#emitter.emit("answer", id, outcome.value.value);
|
|
1271
|
+
this.#parked.delete(id);
|
|
1272
|
+
return outcome.value;
|
|
1273
|
+
}
|
|
1274
|
+
#submit(form, values) {
|
|
1275
|
+
for (const [field, value] of Object.entries(values)) {
|
|
1276
|
+
const outcome = attempt(() => form.fill(field, value));
|
|
1277
|
+
if (!outcome.success) return {
|
|
1278
|
+
success: false,
|
|
1279
|
+
error: this.#errors(outcome.error, form, field)
|
|
1280
|
+
};
|
|
1703
1281
|
}
|
|
1282
|
+
return form.submit();
|
|
1283
|
+
}
|
|
1284
|
+
#errors(error, form, field) {
|
|
1285
|
+
if (isFormError(error)) {
|
|
1286
|
+
if (isFieldError(error.context)) return [error.context];
|
|
1287
|
+
const named = error.context?.field;
|
|
1288
|
+
return [{
|
|
1289
|
+
field: typeof named === "string" ? named : this.#field(form, field),
|
|
1290
|
+
message: error.message
|
|
1291
|
+
}];
|
|
1292
|
+
}
|
|
1293
|
+
return [{
|
|
1294
|
+
field: this.#field(form, field),
|
|
1295
|
+
message: "The form rejected the answer"
|
|
1296
|
+
}];
|
|
1704
1297
|
}
|
|
1705
|
-
#
|
|
1706
|
-
return
|
|
1707
|
-
}
|
|
1708
|
-
#isConfirmOptions(form, _options) {
|
|
1709
|
-
return form === "confirm";
|
|
1710
|
-
}
|
|
1711
|
-
#isSelectOptions(form, _options) {
|
|
1712
|
-
return form === "select";
|
|
1713
|
-
}
|
|
1714
|
-
#isCheckboxOptions(form, _options) {
|
|
1715
|
-
return form === "checkbox";
|
|
1298
|
+
#field(form, field) {
|
|
1299
|
+
return field ?? form.schema.fields[0]?.name ?? form.schema.name ?? "form";
|
|
1716
1300
|
}
|
|
1717
1301
|
#expire(id) {
|
|
1718
1302
|
const parked = this.#parked.get(id);
|
|
1719
|
-
if (parked === void 0) return;
|
|
1303
|
+
if (parked === void 0 || parked.pending.status !== "pending") return;
|
|
1720
1304
|
parked.cancel();
|
|
1721
|
-
|
|
1305
|
+
const expired = {
|
|
1722
1306
|
...parked,
|
|
1723
|
-
|
|
1724
|
-
...parked.
|
|
1307
|
+
pending: {
|
|
1308
|
+
...parked.pending,
|
|
1725
1309
|
status: "expired"
|
|
1726
1310
|
}
|
|
1727
|
-
}
|
|
1311
|
+
};
|
|
1312
|
+
this.#parked.set(id, expired);
|
|
1313
|
+
parked.form.destroy();
|
|
1728
1314
|
this.#emitter.emit("expire", id);
|
|
1729
1315
|
this.#parked.delete(id);
|
|
1730
|
-
parked.expire();
|
|
1731
1316
|
}
|
|
1732
1317
|
};
|
|
1733
1318
|
//#endregion
|
|
1734
1319
|
//#region src/core/PromptClient.ts
|
|
1735
1320
|
/**
|
|
1736
|
-
* The SSE
|
|
1737
|
-
*
|
|
1738
|
-
* serialized pending prompts, dispatches EACH to a LOCAL {@link PromptFormInterface} terminal (so
|
|
1739
|
-
* a human at THIS machine answers a prompt parked elsewhere), and POSTs the answer back.
|
|
1740
|
-
* Universal — `fetch` + SSE are web-standard, so it runs in a browser or on a server; the
|
|
1741
|
-
* injected `fetch` / timer make it fully deterministic in tests.
|
|
1321
|
+
* The SSE form bridge. It ingests serialized forms from a remote broker, renders them through a
|
|
1322
|
+
* local terminal, and posts answers back without blocking the event stream.
|
|
1742
1323
|
*
|
|
1743
1324
|
* @remarks
|
|
1744
|
-
* - **Connect + reconnect.** {@link connect} opens the SSE stream
|
|
1745
|
-
*
|
|
1746
|
-
*
|
|
1747
|
-
*
|
|
1748
|
-
*
|
|
1749
|
-
*
|
|
1750
|
-
*
|
|
1751
|
-
*
|
|
1752
|
-
*
|
|
1753
|
-
*
|
|
1754
|
-
*
|
|
1755
|
-
*
|
|
1756
|
-
*
|
|
1757
|
-
* `shutdown` event calls {@link disconnect} (not {@link destroy}) — the client stops streaming
|
|
1758
|
-
* without auto-reconnect but stays reusable; a later {@link connect} recovers it.
|
|
1759
|
-
* - **Lean events (§13).** `connect` / `disconnect` / `expire` / `error` — errors are `unknown`.
|
|
1760
|
-
* `disconnect` fires exactly once per connected-to-disconnected transition, whether triggered by
|
|
1761
|
-
* the server ending the stream cleanly or by a deliberate {@link disconnect} / {@link destroy}.
|
|
1325
|
+
* - **Connect + reconnect.** {@link connect} opens the SSE stream and reconnects after a transport
|
|
1326
|
+
* drop with the injected backoff unless reconnect is disabled, the client was destroyed, or
|
|
1327
|
+
* {@link disconnect} deliberately stopped it.
|
|
1328
|
+
* - **Ingest + render.** Each `pending` envelope passes through `isPendingForm`, Form's `parseForm`,
|
|
1329
|
+
* and terminal's `sanitizeSchema`, then enters a serial render queue. The SSE reader never awaits
|
|
1330
|
+
* that queue, so `expire` and `shutdown` remain live while a person is answering.
|
|
1331
|
+
* - **Safe local form.** The rendering copy omits every wire `pattern`, because Form compiles a
|
|
1332
|
+
* pattern during local evaluation. The broker's parked form retains it and remains authoritative.
|
|
1333
|
+
* - **Refusal retry.** A structured `rejected` response seeds a new rendering form with the values
|
|
1334
|
+
* just submitted, applies every {@link FieldError} through `invalidate`, and asks again. No retry
|
|
1335
|
+
* counter truncates the loop; acceptance, expiry, and shutdown are its bounds.
|
|
1336
|
+
* - **Replay safety.** A replayed id is skipped while it is queued, rendering, or posting. Once an
|
|
1337
|
+
* attempt ends, a later delivery of that id may be rendered again.
|
|
1762
1338
|
*
|
|
1763
1339
|
* @example
|
|
1764
1340
|
* ```ts
|
|
1765
1341
|
* const client = createPromptClient({
|
|
1766
1342
|
* url: 'http://localhost:3001/prompts',
|
|
1767
|
-
* terminal: createTerminal(),
|
|
1768
|
-
* on: { connect: () => log('connected') },
|
|
1343
|
+
* terminal: createTerminal(),
|
|
1769
1344
|
* })
|
|
1770
|
-
* await client.connect()
|
|
1345
|
+
* await client.connect()
|
|
1771
1346
|
* ```
|
|
1772
1347
|
*/
|
|
1773
1348
|
var PromptClient = class {
|
|
@@ -1785,8 +1360,11 @@ var PromptClient = class {
|
|
|
1785
1360
|
#connecting = false;
|
|
1786
1361
|
#connected = false;
|
|
1787
1362
|
#destroyed = false;
|
|
1363
|
+
#draining = false;
|
|
1788
1364
|
#warnedInsecureToken = false;
|
|
1789
1365
|
#seen = /* @__PURE__ */ new Set();
|
|
1366
|
+
#queue = /* @__PURE__ */ new Map();
|
|
1367
|
+
#active;
|
|
1790
1368
|
constructor(options) {
|
|
1791
1369
|
this.url = options.url;
|
|
1792
1370
|
this.#terminal = options.terminal;
|
|
@@ -1807,8 +1385,7 @@ var PromptClient = class {
|
|
|
1807
1385
|
return this.#connected;
|
|
1808
1386
|
}
|
|
1809
1387
|
async connect() {
|
|
1810
|
-
if (this.#destroyed) return;
|
|
1811
|
-
if (this.#connecting) return;
|
|
1388
|
+
if (this.#destroyed || this.#connecting) return;
|
|
1812
1389
|
this.#connecting = true;
|
|
1813
1390
|
while (this.#connecting && !this.#destroyed) {
|
|
1814
1391
|
try {
|
|
@@ -1837,7 +1414,7 @@ var PromptClient = class {
|
|
|
1837
1414
|
if (this.#destroyed) return;
|
|
1838
1415
|
this.#destroyed = true;
|
|
1839
1416
|
this.disconnect();
|
|
1840
|
-
this.#
|
|
1417
|
+
this.#interrupt();
|
|
1841
1418
|
this.#emitter.destroy();
|
|
1842
1419
|
}
|
|
1843
1420
|
async #stream() {
|
|
@@ -1863,49 +1440,158 @@ var PromptClient = class {
|
|
|
1863
1440
|
for (;;) {
|
|
1864
1441
|
const { done, value } = await reader.read();
|
|
1865
1442
|
if (done) break;
|
|
1866
|
-
for (const event of parser.parse(decoder.decode(value, { stream: true })))
|
|
1443
|
+
for (const event of parser.parse(decoder.decode(value, { stream: true }))) {
|
|
1444
|
+
this.#handle(event);
|
|
1445
|
+
if (!this.#connecting) break;
|
|
1446
|
+
}
|
|
1447
|
+
if (!this.#connecting) break;
|
|
1867
1448
|
}
|
|
1868
1449
|
} finally {
|
|
1869
1450
|
reader.releaseLock();
|
|
1870
1451
|
}
|
|
1871
1452
|
this.#markDisconnected();
|
|
1872
1453
|
}
|
|
1873
|
-
|
|
1454
|
+
#handle(event) {
|
|
1874
1455
|
if (event.event === SSE_EVENTS.pending) {
|
|
1875
1456
|
const parsed = parseJSON(event.data);
|
|
1876
|
-
if (
|
|
1457
|
+
if (!isPendingForm(parsed) || this.#seen.has(parsed.id)) return;
|
|
1458
|
+
const schema = parseForm(parsed.schema);
|
|
1459
|
+
if (schema === void 0) {
|
|
1460
|
+
this.#emitter.emit("error", /* @__PURE__ */ new Error(`broker sent an invalid form schema for ${parsed.id}`));
|
|
1461
|
+
return;
|
|
1462
|
+
}
|
|
1463
|
+
this.#seen.add(parsed.id);
|
|
1464
|
+
this.#queue.set(parsed.id, sanitizeSchema(schema));
|
|
1465
|
+
this.#drain();
|
|
1877
1466
|
return;
|
|
1878
1467
|
}
|
|
1879
1468
|
if (event.event === SSE_EVENTS.expire) {
|
|
1880
1469
|
const parsed = parseJSON(event.data);
|
|
1881
|
-
if (isRecord(parsed) && isString(parsed.id))
|
|
1882
|
-
this.#seen.delete(parsed.id);
|
|
1883
|
-
this.#emitter.emit("expire", parsed.id);
|
|
1884
|
-
}
|
|
1470
|
+
if (isRecord(parsed) && isString(parsed.id)) this.#expire(parsed.id);
|
|
1885
1471
|
return;
|
|
1886
1472
|
}
|
|
1887
|
-
if (event.event === SSE_EVENTS.shutdown)
|
|
1473
|
+
if (event.event === SSE_EVENTS.shutdown) {
|
|
1474
|
+
this.disconnect();
|
|
1475
|
+
this.#interrupt();
|
|
1476
|
+
}
|
|
1888
1477
|
}
|
|
1889
|
-
async #
|
|
1890
|
-
this.#
|
|
1478
|
+
async #drain() {
|
|
1479
|
+
if (this.#draining) return;
|
|
1480
|
+
this.#draining = true;
|
|
1891
1481
|
try {
|
|
1892
|
-
|
|
1893
|
-
|
|
1894
|
-
|
|
1895
|
-
|
|
1482
|
+
while (!this.#destroyed) {
|
|
1483
|
+
let queued;
|
|
1484
|
+
for (const entry of this.#queue) {
|
|
1485
|
+
queued = entry;
|
|
1486
|
+
break;
|
|
1487
|
+
}
|
|
1488
|
+
if (queued === void 0) return;
|
|
1489
|
+
const [id, schema] = queued;
|
|
1490
|
+
this.#queue.delete(id);
|
|
1491
|
+
try {
|
|
1492
|
+
await this.#render(id, schema);
|
|
1493
|
+
} catch (error) {
|
|
1494
|
+
const active = this.#active;
|
|
1495
|
+
if (active?.id !== id || !active.stopped) this.#emitter.emit("error", error);
|
|
1496
|
+
} finally {
|
|
1497
|
+
const active = this.#active;
|
|
1498
|
+
if (active?.id === id) {
|
|
1499
|
+
active.form.destroy();
|
|
1500
|
+
this.#active = void 0;
|
|
1501
|
+
}
|
|
1502
|
+
if (!this.#queue.has(id)) this.#seen.delete(id);
|
|
1503
|
+
}
|
|
1504
|
+
}
|
|
1896
1505
|
} finally {
|
|
1897
|
-
this.#
|
|
1506
|
+
this.#draining = false;
|
|
1507
|
+
}
|
|
1508
|
+
}
|
|
1509
|
+
async #render(id, schema) {
|
|
1510
|
+
let values;
|
|
1511
|
+
let errors = [];
|
|
1512
|
+
while (this.#seen.has(id) && !this.#destroyed) {
|
|
1513
|
+
const form = this.#createRenderingForm(schema, values);
|
|
1514
|
+
this.#active = {
|
|
1515
|
+
id,
|
|
1516
|
+
form,
|
|
1517
|
+
stopped: false
|
|
1518
|
+
};
|
|
1519
|
+
for (const error of errors) form.invalidate(error.field, sanitizeDisplayText(error.message));
|
|
1520
|
+
const submitted = await this.#terminal.ask(form);
|
|
1521
|
+
const active = this.#active;
|
|
1522
|
+
if (active?.id !== id || active.stopped) return;
|
|
1523
|
+
const rejected = await this.#post(id, submitted);
|
|
1524
|
+
const posted = this.#active;
|
|
1525
|
+
if (posted?.id !== id || posted.stopped || rejected === void 0) return;
|
|
1526
|
+
values = submitted;
|
|
1527
|
+
errors = rejected;
|
|
1528
|
+
form.destroy();
|
|
1529
|
+
this.#active = void 0;
|
|
1898
1530
|
}
|
|
1899
1531
|
}
|
|
1900
|
-
|
|
1901
|
-
|
|
1532
|
+
#createRenderingForm(schema, values) {
|
|
1533
|
+
const fields = schema.fields.map((field) => {
|
|
1534
|
+
if (field.rule?.pattern === void 0) return field;
|
|
1535
|
+
const { pattern: _pattern, ...rule } = field.rule;
|
|
1536
|
+
return {
|
|
1537
|
+
...field,
|
|
1538
|
+
rule
|
|
1539
|
+
};
|
|
1540
|
+
});
|
|
1541
|
+
const form = createForm({
|
|
1542
|
+
...schema,
|
|
1543
|
+
fields
|
|
1544
|
+
}, values === void 0 ? void 0 : { values });
|
|
1545
|
+
form.answer.catch(() => void 0);
|
|
1546
|
+
return form;
|
|
1547
|
+
}
|
|
1548
|
+
async #post(id, values) {
|
|
1549
|
+
const response = await this.#fetch(this.url, {
|
|
1902
1550
|
method: "POST",
|
|
1903
1551
|
headers: this.#headers({ "Content-Type": "application/json" }),
|
|
1904
1552
|
body: JSON.stringify({
|
|
1905
1553
|
id,
|
|
1906
|
-
|
|
1554
|
+
values
|
|
1907
1555
|
})
|
|
1908
|
-
})
|
|
1556
|
+
});
|
|
1557
|
+
if (!response.ok) this.#emitter.emit("error", /* @__PURE__ */ new Error(`broker rejected answer ${id}`));
|
|
1558
|
+
const parsed = parseJSON(await response.text());
|
|
1559
|
+
if (!isRecord(parsed) || !isBoolean(parsed.success)) {
|
|
1560
|
+
this.#emitter.emit("error", /* @__PURE__ */ new Error(`broker returned an invalid answer result for ${id}`));
|
|
1561
|
+
return;
|
|
1562
|
+
}
|
|
1563
|
+
if (parsed.success) return void 0;
|
|
1564
|
+
const error = parsed.error;
|
|
1565
|
+
if (!isRecord(error) || !isString(error.reason)) {
|
|
1566
|
+
this.#emitter.emit("error", /* @__PURE__ */ new Error(`broker returned an invalid answer error for ${id}`));
|
|
1567
|
+
return;
|
|
1568
|
+
}
|
|
1569
|
+
if (error.reason === "unknown") return void 0;
|
|
1570
|
+
if (error.reason === "rejected" && arrayOf(isFieldError)(error.errors)) return error.errors;
|
|
1571
|
+
this.#emitter.emit("error", /* @__PURE__ */ new Error(`broker returned an invalid answer refusal for ${id}`));
|
|
1572
|
+
}
|
|
1573
|
+
#expire(id) {
|
|
1574
|
+
const active = this.#active;
|
|
1575
|
+
if (active?.id === id) {
|
|
1576
|
+
this.#active = {
|
|
1577
|
+
...active,
|
|
1578
|
+
stopped: true
|
|
1579
|
+
};
|
|
1580
|
+
this.#seen.delete(id);
|
|
1581
|
+
active.form.destroy();
|
|
1582
|
+
} else if (this.#queue.delete(id)) this.#seen.delete(id);
|
|
1583
|
+
this.#emitter.emit("expire", id);
|
|
1584
|
+
}
|
|
1585
|
+
#interrupt() {
|
|
1586
|
+
this.#queue.clear();
|
|
1587
|
+
this.#seen.clear();
|
|
1588
|
+
const active = this.#active;
|
|
1589
|
+
if (active === void 0) return;
|
|
1590
|
+
this.#active = {
|
|
1591
|
+
...active,
|
|
1592
|
+
stopped: true
|
|
1593
|
+
};
|
|
1594
|
+
active.form.destroy();
|
|
1909
1595
|
}
|
|
1910
1596
|
#markDisconnected() {
|
|
1911
1597
|
if (!this.#connected) return;
|
|
@@ -1938,8 +1624,8 @@ var PromptClient = class {
|
|
|
1938
1624
|
//#region src/core/TerminalManager.ts
|
|
1939
1625
|
/**
|
|
1940
1626
|
* The multi-endpoint terminal MANAGER — a registry of named {@link PromptInterface} brokers (one
|
|
1941
|
-
* per endpoint), so several parties can `ask`
|
|
1942
|
-
* attribution edge on every parked
|
|
1627
|
+
* per endpoint), so several parties can `ask` forms of each other by NAME with a `from` → `to`
|
|
1628
|
+
* attribution edge on every parked form, and a transitive DEADLOCK check across all in-flight
|
|
1943
1629
|
* asks.
|
|
1944
1630
|
*
|
|
1945
1631
|
* @remarks
|
|
@@ -1950,22 +1636,23 @@ var PromptClient = class {
|
|
|
1950
1636
|
* - **`ask`.** The target must already be mounted via {@link add} — `ask` never auto-adds it;
|
|
1951
1637
|
* rejects `TARGET` for an unknown `to` (listing the known names). Rejects `DEADLOCK` when parking
|
|
1952
1638
|
* `from → to` would close a cycle over the CURRENT in-flight edge set (walked transitively);
|
|
1953
|
-
* otherwise parks through the target's broker and
|
|
1954
|
-
*
|
|
1639
|
+
* otherwise parks the caller's live form through the target's broker and returns that form's own
|
|
1640
|
+
* `answer` promise. Edge cleanup never alters the value or rejection the caller observes.
|
|
1955
1641
|
* - **Durable open / save.** `open(name)` restores an EMPTY broker from the `store` (parked
|
|
1956
1642
|
* Promises are process-bound and never resurrected); `save(name)` persists the endpoint's
|
|
1957
1643
|
* configured `timeout`.
|
|
1958
1644
|
* - **Removal.** `remove` drops one endpoint, a batch (§9.2, array overload FIRST), or every
|
|
1959
|
-
* endpoint when called without an argument. It destroys each broker, which expires every
|
|
1645
|
+
* endpoint when called without an argument. It destroys each broker, which expires every form
|
|
1960
1646
|
* still parked on it. `destroy` is idempotent.
|
|
1961
1647
|
*
|
|
1962
1648
|
* @example
|
|
1963
1649
|
* ```ts
|
|
1650
|
+
* const form = createForm({ fields: [{ control: 'text', name: 'name' }] })
|
|
1964
1651
|
* const manager = new TerminalManager()
|
|
1965
1652
|
* manager.add('agent')
|
|
1966
|
-
* const
|
|
1967
|
-
* manager.answer('agent', manager.pending('agent')[0].id, 'Ada')
|
|
1968
|
-
* await
|
|
1653
|
+
* const answer = manager.ask('user', 'agent', form)
|
|
1654
|
+
* manager.answer('agent', manager.pending('agent')[0].id, { name: 'Ada' })
|
|
1655
|
+
* await answer // { name: 'Ada' }
|
|
1969
1656
|
* ```
|
|
1970
1657
|
*/
|
|
1971
1658
|
var TerminalManager = class {
|
|
@@ -2009,6 +1696,8 @@ var TerminalManager = class {
|
|
|
2009
1696
|
const timer = options?.timer ?? this.#timer;
|
|
2010
1697
|
const cap = options?.cap ?? this.#cap;
|
|
2011
1698
|
const broker = createPrompt({
|
|
1699
|
+
...options?.on !== void 0 ? { on: options.on } : {},
|
|
1700
|
+
...options?.error !== void 0 ? { error: options.error } : {},
|
|
2012
1701
|
...timeout !== void 0 ? { timeout } : {},
|
|
2013
1702
|
...timer !== void 0 ? { timer } : {},
|
|
2014
1703
|
...cap !== void 0 ? { cap } : {}
|
|
@@ -2022,11 +1711,11 @@ var TerminalManager = class {
|
|
|
2022
1711
|
broker.emitter.on("answer", listeners.answer);
|
|
2023
1712
|
broker.emitter.on("expire", listeners.expire);
|
|
2024
1713
|
this.#terminals.set(name, broker);
|
|
2025
|
-
this.#config.set(name, options
|
|
1714
|
+
this.#config.set(name, { ...options });
|
|
2026
1715
|
this.#listeners.set(name, listeners);
|
|
2027
1716
|
return broker;
|
|
2028
1717
|
}
|
|
2029
|
-
ask(from, to, form
|
|
1718
|
+
ask(from, to, form) {
|
|
2030
1719
|
const broker = this.#terminals.get(to);
|
|
2031
1720
|
if (broker === void 0) {
|
|
2032
1721
|
const known = this.terminals();
|
|
@@ -2041,21 +1730,18 @@ var TerminalManager = class {
|
|
|
2041
1730
|
to,
|
|
2042
1731
|
path: cycle
|
|
2043
1732
|
}));
|
|
2044
|
-
const
|
|
2045
|
-
form,
|
|
2046
|
-
options,
|
|
1733
|
+
const id = broker.park(form, {
|
|
2047
1734
|
from,
|
|
2048
1735
|
to
|
|
2049
1736
|
});
|
|
2050
|
-
if (broker.pending(
|
|
2051
|
-
this.#edges.set(
|
|
1737
|
+
if (broker.pending(id) !== void 0) {
|
|
1738
|
+
this.#edges.set(id, {
|
|
2052
1739
|
from,
|
|
2053
1740
|
to
|
|
2054
1741
|
});
|
|
2055
|
-
|
|
2056
|
-
ticket.value.then(clear, clear);
|
|
1742
|
+
form.answer.then(this.#createEdgeClear(id), this.#createEdgeClear(id));
|
|
2057
1743
|
}
|
|
2058
|
-
return
|
|
1744
|
+
return form.answer;
|
|
2059
1745
|
}
|
|
2060
1746
|
pending(to) {
|
|
2061
1747
|
if (to !== void 0) {
|
|
@@ -2066,13 +1752,15 @@ var TerminalManager = class {
|
|
|
2066
1752
|
for (const broker of this.#terminals.values()) result.push(...broker.pending());
|
|
2067
1753
|
return result;
|
|
2068
1754
|
}
|
|
2069
|
-
answer(to, id,
|
|
1755
|
+
answer(to, id, values) {
|
|
2070
1756
|
const broker = this.#terminals.get(to);
|
|
2071
1757
|
if (broker === void 0) return {
|
|
2072
1758
|
success: false,
|
|
2073
|
-
error: "terminal"
|
|
1759
|
+
error: { reason: "terminal" }
|
|
2074
1760
|
};
|
|
2075
|
-
|
|
1761
|
+
const result = broker.answer(id, values);
|
|
1762
|
+
if (result.success) this.#edges.delete(id);
|
|
1763
|
+
return result;
|
|
2076
1764
|
}
|
|
2077
1765
|
async open(name) {
|
|
2078
1766
|
if (this.#destroyed) throw new TerminalError("DESTROYED", "manager destroyed");
|
|
@@ -2111,16 +1799,23 @@ var TerminalManager = class {
|
|
|
2111
1799
|
if (this.#destroyed) return;
|
|
2112
1800
|
this.#destroyed = true;
|
|
2113
1801
|
this.remove();
|
|
1802
|
+
this.#edges.clear();
|
|
2114
1803
|
this.#emitter.destroy();
|
|
2115
1804
|
}
|
|
2116
1805
|
#createPendingListener() {
|
|
2117
|
-
return (
|
|
1806
|
+
return (form) => this.#emitter.emit("pending", form);
|
|
2118
1807
|
}
|
|
2119
1808
|
#createAnswerListener(name) {
|
|
2120
|
-
return (id,
|
|
1809
|
+
return (id, values) => {
|
|
1810
|
+
this.#edges.delete(id);
|
|
1811
|
+
this.#emitter.emit("answer", name, id, values);
|
|
1812
|
+
};
|
|
2121
1813
|
}
|
|
2122
1814
|
#createExpireListener(name) {
|
|
2123
|
-
return (id) =>
|
|
1815
|
+
return (id) => {
|
|
1816
|
+
this.#edges.delete(id);
|
|
1817
|
+
this.#emitter.emit("expire", name, id);
|
|
1818
|
+
};
|
|
2124
1819
|
}
|
|
2125
1820
|
#createEdgeClear(id) {
|
|
2126
1821
|
return () => {
|
|
@@ -2140,8 +1835,12 @@ var TerminalManager = class {
|
|
|
2140
1835
|
this.#terminals.delete(name);
|
|
2141
1836
|
this.#config.delete(name);
|
|
2142
1837
|
this.#listeners.delete(name);
|
|
1838
|
+
this.#clearEdges(name);
|
|
2143
1839
|
return true;
|
|
2144
1840
|
}
|
|
1841
|
+
#clearEdges(to) {
|
|
1842
|
+
for (const [id, edge] of this.#edges) if (edge.to === to) this.#edges.delete(id);
|
|
1843
|
+
}
|
|
2145
1844
|
#findCycle(from, to) {
|
|
2146
1845
|
if (from === to) return [from, to];
|
|
2147
1846
|
const visited = /* @__PURE__ */ new Set([to]);
|
|
@@ -2289,30 +1988,26 @@ var DatabaseTerminalStore = class {
|
|
|
2289
1988
|
//#endregion
|
|
2290
1989
|
//#region src/core/factories.ts
|
|
2291
1990
|
/**
|
|
2292
|
-
* Create the headless
|
|
2293
|
-
*
|
|
2294
|
-
* timeout). The tri-surface's headless arm: subscribe `emitter.on('pending', …)` to forward each
|
|
2295
|
-
* prompt to whoever can answer (an SSE transport, a remote terminal), then route the answer back
|
|
2296
|
-
* through `answer(id, value)`.
|
|
1991
|
+
* Create the headless {@link PromptInterface} broker. It parks live forms and applies remote
|
|
1992
|
+
* answers to the authoritative instances.
|
|
2297
1993
|
*
|
|
2298
1994
|
* @param options - See {@link PromptOptions}
|
|
2299
1995
|
* @returns A {@link PromptInterface}
|
|
2300
1996
|
*
|
|
2301
1997
|
* @remarks
|
|
2302
|
-
*
|
|
2303
|
-
*
|
|
2304
|
-
*
|
|
2305
|
-
* `options.timeout` (default {@link import('./constants.js').DEFAULT_PROMPT_TIMEOUT_MS}) and its
|
|
2306
|
-
* Promise rejects with a {@link import('./errors.js').TerminalError}; inject `options.timer` to
|
|
2307
|
-
* drive expiry without real time.
|
|
1998
|
+
* The caller awaits the parked form's own `answer`. Timeout or teardown destroys the form, so that
|
|
1999
|
+
* promise rejects with the Form package's `ABANDONED` error. Inject `options.timer` to drive
|
|
2000
|
+
* expiry without real time.
|
|
2308
2001
|
*
|
|
2309
2002
|
* @example
|
|
2310
2003
|
* ```ts
|
|
2311
2004
|
* import { createPrompt } from '@src/core'
|
|
2005
|
+
* import { createForm } from '@orkestrel/form'
|
|
2312
2006
|
*
|
|
2313
2007
|
* const prompt = createPrompt()
|
|
2314
|
-
*
|
|
2315
|
-
* const
|
|
2008
|
+
* const form = createForm({ fields: [{ control: 'text', name: 'name' }] })
|
|
2009
|
+
* const id = prompt.park(form)
|
|
2010
|
+
* prompt.answer(id, { name: 'Ada' })
|
|
2316
2011
|
* ```
|
|
2317
2012
|
*/
|
|
2318
2013
|
function createPrompt(options) {
|
|
@@ -2320,9 +2015,8 @@ function createPrompt(options) {
|
|
|
2320
2015
|
}
|
|
2321
2016
|
/**
|
|
2322
2017
|
* Create the SSE prompt {@link PromptClientInterface} BRIDGE — it connects to a remote broker's SSE
|
|
2323
|
-
* endpoint, dispatches each received
|
|
2324
|
-
*
|
|
2325
|
-
* back. Universal — `fetch` / SSE are web-standard.
|
|
2018
|
+
* endpoint, dispatches each received form to a local {@link import('./types.js').TerminalInterface},
|
|
2019
|
+
* and POSTs the answer back. Universal — `fetch` / SSE are web-standard.
|
|
2326
2020
|
*
|
|
2327
2021
|
* @param options - See {@link PromptClientOptions} (`url` + `terminal` required)
|
|
2328
2022
|
* @returns A {@link PromptClientInterface}
|
|
@@ -2346,12 +2040,12 @@ function createPromptClient(options) {
|
|
|
2346
2040
|
return new PromptClient(options);
|
|
2347
2041
|
}
|
|
2348
2042
|
/**
|
|
2349
|
-
* Create the multi-endpoint {@link
|
|
2043
|
+
* Create the multi-endpoint {@link TerminalManager} — a named registry of
|
|
2350
2044
|
* {@link PromptInterface} brokers so several parties can `ask` prompts of each other by name,
|
|
2351
2045
|
* with a transitive DEADLOCK check across every in-flight ask.
|
|
2352
2046
|
*
|
|
2353
2047
|
* @param options - See {@link TerminalManagerOptions}
|
|
2354
|
-
* @returns A {@link
|
|
2048
|
+
* @returns A {@link TerminalManager}
|
|
2355
2049
|
*
|
|
2356
2050
|
* @example
|
|
2357
2051
|
* ```ts
|
|
@@ -2405,6 +2099,6 @@ function createDatabaseTerminalStore(driver = createMemoryDriver()) {
|
|
|
2405
2099
|
}).table("terminals"));
|
|
2406
2100
|
}
|
|
2407
2101
|
//#endregion
|
|
2408
|
-
export { ACCEPT_EVENT_STREAM,
|
|
2102
|
+
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 };
|
|
2409
2103
|
|
|
2410
2104
|
//# sourceMappingURL=index.js.map
|