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