@orkestrel/terminal 0.0.7 → 0.0.9
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 +785 -869
- package/dist/src/core/index.cjs.map +1 -1
- package/dist/src/core/index.d.cts +627 -862
- package/dist/src/core/index.d.ts +627 -862
- package/dist/src/core/index.js +777 -841
- package/dist/src/core/index.js.map +1 -1
- package/dist/src/server/index.cjs +555 -205
- package/dist/src/server/index.cjs.map +1 -1
- package/dist/src/server/index.d.cts +232 -129
- package/dist/src/server/index.d.ts +232 -129
- package/dist/src/server/index.js +540 -206
- package/dist/src/server/index.js.map +1 -1
- package/package.json +8 -6
package/dist/src/core/index.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
1
|
+
import { STATUS_ICONS, createStyler, freezeStyle, strip, stripControls } from "@orkestrel/console";
|
|
2
|
+
import { arrayOf, attempt, isArray, isBoolean, isNonEmptyString, isNumber, isRecord, isString, literalOf, parseJSON, rawShape, recordOf, stringShape } from "@orkestrel/contract";
|
|
3
3
|
import { Emitter } from "@orkestrel/emitter";
|
|
4
|
+
import { createForm, isFieldError, isFormError, parseForm, serializeForm } from "@orkestrel/form";
|
|
4
5
|
import { createSSEParser } from "@orkestrel/sse";
|
|
5
6
|
import { createDatabase, createMemoryDriver } from "@orkestrel/database";
|
|
6
7
|
//#region src/core/constants.ts
|
|
@@ -127,42 +128,11 @@ var CONTROL_NAMES = Object.freeze({
|
|
|
127
128
|
ctrl: true
|
|
128
129
|
})
|
|
129
130
|
});
|
|
130
|
-
/** The default mask glyph
|
|
131
|
+
/** The default mask glyph {@link import('./helpers.js').createPasswordState} uses — `*`. */
|
|
131
132
|
var DEFAULT_MASK = "*";
|
|
132
|
-
/** Matches an email address — a non-trivial `local@domain.tld` shape. The `email` rule tests against this. */
|
|
133
|
-
var EMAIL_PATTERN = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
|
|
134
|
-
/** Matches an HTTP(S) URL. The `url` rule tests against this. */
|
|
135
|
-
var URL_PATTERN = /^https?:\/\/.+/;
|
|
136
|
-
/** Matches a numeric value (integer or decimal, optional sign). The `numeric` rule tests against this. */
|
|
137
|
-
var NUMERIC_PATTERN = /^-?\d+(\.\d+)?$/;
|
|
138
|
-
/** Matches an integer (optional sign). The `integer` rule tests against this. */
|
|
139
|
-
var INTEGER_PATTERN = /^-?\d+$/;
|
|
140
|
-
/** Matches an alphanumeric string (letters and digits only). The `alphanumeric` rule tests against this. */
|
|
141
|
-
var ALPHANUMERIC_PATTERN = /^[a-zA-Z0-9]+$/;
|
|
142
133
|
/**
|
|
143
|
-
*
|
|
144
|
-
*
|
|
145
|
-
* messages are interpolated with the configured length at build time). Frozen; the source of
|
|
146
|
-
* truth for the rule-failure copy.
|
|
147
|
-
*/
|
|
148
|
-
var RULE_MESSAGES = Object.freeze({
|
|
149
|
-
required: "This field is required",
|
|
150
|
-
minimum: "Must be at least {count} characters",
|
|
151
|
-
maximum: "Must be at most {count} characters",
|
|
152
|
-
pattern: "Must match pattern: {pattern}",
|
|
153
|
-
email: "Must be a valid email address",
|
|
154
|
-
url: "Must be a valid URL",
|
|
155
|
-
numeric: "Must be a numeric value",
|
|
156
|
-
integer: "Must be an integer",
|
|
157
|
-
alphanumeric: "Must contain only letters and digits",
|
|
158
|
-
invalid: "Invalid input"
|
|
159
|
-
});
|
|
160
|
-
/**
|
|
161
|
-
* The prompt-view icon glyphs the reducers render the prompt line and choice rows with. PLAIN
|
|
162
|
-
* glyphs — color is applied by the {@link import('./types.js').PromptState}'s
|
|
163
|
-
* {@link import('@orkestrel/console').StylerInterface} at render time, never baked into the
|
|
164
|
-
* constant (AGENTS — styling orthogonal to data; the rework fixes the terminal's ANSI-in-the-icon).
|
|
165
|
-
* Frozen.
|
|
134
|
+
* The six default glyphs {@link DEFAULT_PROMPT_THEME} assembles its `icons` from. Read only when
|
|
135
|
+
* the default theme is assembled; a view reads its resolved theme and never this constant. Frozen.
|
|
166
136
|
*
|
|
167
137
|
* @remarks
|
|
168
138
|
* - `question` — the leading mark on a prompt's message line.
|
|
@@ -178,16 +148,89 @@ var PROMPT_ICONS = Object.freeze({
|
|
|
178
148
|
checked: "☑",
|
|
179
149
|
unchecked: "☐"
|
|
180
150
|
});
|
|
151
|
+
/**
|
|
152
|
+
* Every {@link import('./types.js').PromptRole}, in one frozen list — the role axis's source of
|
|
153
|
+
* truth. {@link import('./helpers.js').createPromptTheme} walks it to merge a partial theme, and
|
|
154
|
+
* a consumer building a complete role map reads it rather than retyping every name.
|
|
155
|
+
*/
|
|
156
|
+
var PROMPT_ROLES = Object.freeze([
|
|
157
|
+
"question",
|
|
158
|
+
"pointer",
|
|
159
|
+
"message",
|
|
160
|
+
"content",
|
|
161
|
+
"success",
|
|
162
|
+
"error",
|
|
163
|
+
"selected",
|
|
164
|
+
"focus",
|
|
165
|
+
"hint",
|
|
166
|
+
"muted",
|
|
167
|
+
"description"
|
|
168
|
+
]);
|
|
169
|
+
/**
|
|
170
|
+
* The {@link import('./types.js').PromptTheme} every prompt renders with unless its options supply
|
|
171
|
+
* another — the glyph set assembled from {@link PROMPT_ICONS} plus the console
|
|
172
|
+
* {@link import('@orkestrel/console').STATUS_ICONS} `success` / `error` marks, and the console
|
|
173
|
+
* {@link import('@orkestrel/console').Style} each role is painted with. Deeply frozen through the
|
|
174
|
+
* console module's own {@link import('@orkestrel/console').freezeStyle}; the baseline
|
|
175
|
+
* {@link import('./helpers.js').createPromptTheme} merges a partial theme over.
|
|
176
|
+
*
|
|
177
|
+
* @remarks
|
|
178
|
+
* The default roles reproduce the views' historical coloring exactly: `question` / `pointer` cyan,
|
|
179
|
+
* `message` / `focus` bold, `success` / `selected` green, `error` red, `hint` / `muted` /
|
|
180
|
+
* `description` dim, and `content` the EMPTY style — an empty style renders bare text, so primary
|
|
181
|
+
* content keeps the bytes it had before it became themeable.
|
|
182
|
+
* Two roles sharing a style today are still two roles — re-mapping one leaves the other alone.
|
|
183
|
+
*/
|
|
184
|
+
var DEFAULT_PROMPT_THEME = Object.freeze({
|
|
185
|
+
icons: Object.freeze({
|
|
186
|
+
question: PROMPT_ICONS.question,
|
|
187
|
+
pointer: PROMPT_ICONS.pointer,
|
|
188
|
+
dot: PROMPT_ICONS.dot,
|
|
189
|
+
selected: PROMPT_ICONS.selected,
|
|
190
|
+
checked: PROMPT_ICONS.checked,
|
|
191
|
+
unchecked: PROMPT_ICONS.unchecked,
|
|
192
|
+
success: STATUS_ICONS.success,
|
|
193
|
+
error: STATUS_ICONS.error
|
|
194
|
+
}),
|
|
195
|
+
roles: Object.freeze({
|
|
196
|
+
question: freezeStyle({
|
|
197
|
+
foreground: "cyan",
|
|
198
|
+
attributes: []
|
|
199
|
+
}),
|
|
200
|
+
pointer: freezeStyle({
|
|
201
|
+
foreground: "cyan",
|
|
202
|
+
attributes: []
|
|
203
|
+
}),
|
|
204
|
+
message: freezeStyle({ attributes: ["bold"] }),
|
|
205
|
+
content: freezeStyle({ attributes: [] }),
|
|
206
|
+
success: freezeStyle({
|
|
207
|
+
foreground: "green",
|
|
208
|
+
attributes: []
|
|
209
|
+
}),
|
|
210
|
+
error: freezeStyle({
|
|
211
|
+
foreground: "red",
|
|
212
|
+
attributes: []
|
|
213
|
+
}),
|
|
214
|
+
selected: freezeStyle({
|
|
215
|
+
foreground: "green",
|
|
216
|
+
attributes: []
|
|
217
|
+
}),
|
|
218
|
+
focus: freezeStyle({ attributes: ["bold"] }),
|
|
219
|
+
hint: freezeStyle({ attributes: ["dim"] }),
|
|
220
|
+
muted: freezeStyle({ attributes: ["dim"] }),
|
|
221
|
+
description: freezeStyle({ attributes: ["dim"] })
|
|
222
|
+
})
|
|
223
|
+
});
|
|
181
224
|
/** How long (ms) the {@link import('./types.js').PromptInterface} broker parks an unanswered prompt before it expires — 5 minutes. */
|
|
182
225
|
var DEFAULT_PROMPT_TIMEOUT_MS = 3e5;
|
|
183
226
|
/** How long (ms) the {@link import('./types.js').PromptClientInterface} waits before each reconnect attempt — 2 seconds. */
|
|
184
227
|
var DEFAULT_RECONNECT_DELAY_MS = 2e3;
|
|
185
228
|
/**
|
|
186
|
-
* The SSE `event:` names the broker emits and the {@link import('./types.js').
|
|
229
|
+
* The SSE `event:` names the broker emits and the {@link import('./types.js').PromptClientInterface}
|
|
187
230
|
* dispatches on. Frozen; the source of truth for the wire event vocabulary.
|
|
188
231
|
*
|
|
189
232
|
* @remarks
|
|
190
|
-
* - `pending` — a serialized {@link import('./types.js').
|
|
233
|
+
* - `pending` — a serialized {@link import('./types.js').PendingForm} to dispatch and answer.
|
|
191
234
|
* - `expire` — an `{ id }` payload: the broker expired a parked prompt (the client drops it).
|
|
192
235
|
* - `shutdown` — the broker is going away; the client disconnects (no auto-reconnect) but stays reusable.
|
|
193
236
|
*/
|
|
@@ -196,12 +239,12 @@ var SSE_EVENTS = Object.freeze({
|
|
|
196
239
|
expire: "expire",
|
|
197
240
|
shutdown: "shutdown"
|
|
198
241
|
});
|
|
199
|
-
/** The auth-token request header the {@link import('./types.js').
|
|
242
|
+
/** The auth-token request header the {@link import('./types.js').PromptClientInterface} sends. */
|
|
200
243
|
var HEADER_TOKEN = "x-orkestrel-token";
|
|
201
244
|
/** The `Accept` header value that opens the broker's SSE stream. */
|
|
202
245
|
var ACCEPT_EVENT_STREAM = "text/event-stream";
|
|
203
246
|
/**
|
|
204
|
-
* The maximum number of characters the {@link import('./types.js').
|
|
247
|
+
* The maximum number of characters the {@link import('./types.js').PromptClientInterface} lets its
|
|
205
248
|
* SSE parser buffer before treating the stream as hostile — 1 MiB, comfortably above any
|
|
206
249
|
* legitimate prompt payload. Passed as the `limit` to `createSSEParser` so an unterminated
|
|
207
250
|
* or oversized `data:` field cannot grow the buffer without bound (a memory-exhaustion guard).
|
|
@@ -210,19 +253,20 @@ var SSE_BUFFER_LIMIT = 1048576;
|
|
|
210
253
|
//#endregion
|
|
211
254
|
//#region src/core/errors.ts
|
|
212
255
|
/**
|
|
213
|
-
*
|
|
256
|
+
* The error the terminal surfaces for its own refusals: parking on a destroyed or full broker, an
|
|
257
|
+
* unusable driver stream, a manager routing fault, or a ctrl-c cancellation. A parked form's own
|
|
258
|
+
* lifecycle failures reject through the form's `answer` with the form package's error, never with
|
|
259
|
+
* this one.
|
|
214
260
|
*
|
|
215
261
|
* @remarks
|
|
216
|
-
* Carries a {@link TerminalErrorCode} and an optional `context` bag
|
|
217
|
-
*
|
|
218
|
-
*
|
|
219
|
-
* `EXPIRE`); or when the user aborts an interactive server-`Terminal` prompt with ctrl-c
|
|
220
|
-
* (`CANCEL`). Narrow a caught value with {@link isTerminalError} and branch on `error.code`.
|
|
262
|
+
* Carries a {@link TerminalErrorCode} and an optional `context` bag naming the offending values:
|
|
263
|
+
* `{ cap }` on `LIMIT`, `{ to, known }` on `TARGET`, and `{ from, to, path }` on `DEADLOCK`. Narrow
|
|
264
|
+
* a caught value with {@link isTerminalError} and branch on `error.code`.
|
|
221
265
|
*/
|
|
222
266
|
var TerminalError = class extends Error {
|
|
223
267
|
/** The machine-readable condition — see {@link TerminalErrorCode}. */
|
|
224
268
|
code;
|
|
225
|
-
/** An optional context bag naming the offending
|
|
269
|
+
/** An optional context bag naming the offending values — see the class {@link TerminalError remarks}. */
|
|
226
270
|
context;
|
|
227
271
|
constructor(code, message, context) {
|
|
228
272
|
super(message);
|
|
@@ -240,9 +284,11 @@ var TerminalError = class extends Error {
|
|
|
240
284
|
* @example
|
|
241
285
|
* ```ts
|
|
242
286
|
* try {
|
|
243
|
-
*
|
|
287
|
+
* await terminal.ask(form)
|
|
244
288
|
* } catch (error) {
|
|
245
|
-
* if (isTerminalError(error) && error.code === '
|
|
289
|
+
* if (isTerminalError(error) && error.code === 'CANCEL') {
|
|
290
|
+
* // the person aborted
|
|
291
|
+
* }
|
|
246
292
|
* }
|
|
247
293
|
* ```
|
|
248
294
|
*/
|
|
@@ -251,26 +297,39 @@ function isTerminalError(value) {
|
|
|
251
297
|
}
|
|
252
298
|
//#endregion
|
|
253
299
|
//#region src/core/validators.ts
|
|
254
|
-
/** Narrow an unknown value to a {@link
|
|
255
|
-
var
|
|
256
|
-
/** Narrow an unknown value to a {@link PendingPromptStatus}. */
|
|
257
|
-
var isPendingPromptStatus = literalOf("pending", "answered", "expired");
|
|
300
|
+
/** Narrow an unknown value to a {@link PendingFormStatus}. */
|
|
301
|
+
var isPendingFormStatus = literalOf("pending", "answered", "expired");
|
|
258
302
|
/**
|
|
259
|
-
* Narrow an unknown wire value to a {@link
|
|
303
|
+
* Narrow an unknown wire value to a {@link PendingForm} envelope.
|
|
304
|
+
*
|
|
305
|
+
* @remarks
|
|
306
|
+
* This guard checks the transport record and proves only that `schema` is a record. The Form
|
|
307
|
+
* package's `parseForm` owns the schema payload and its semantic audit.
|
|
260
308
|
*
|
|
261
309
|
* @param value - The decoded wire value to inspect
|
|
262
|
-
* @returns Whether
|
|
310
|
+
* @returns Whether the value is a complete pending-form envelope
|
|
263
311
|
*/
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
to
|
|
273
|
-
}
|
|
312
|
+
function isPendingForm(value) {
|
|
313
|
+
return recordOf({
|
|
314
|
+
id: isNonEmptyString,
|
|
315
|
+
schema: isRecord,
|
|
316
|
+
status: isPendingFormStatus,
|
|
317
|
+
time: isNumber,
|
|
318
|
+
from: isString,
|
|
319
|
+
to: isString
|
|
320
|
+
}, ["from", "to"])(value);
|
|
321
|
+
}
|
|
322
|
+
/**
|
|
323
|
+
* Narrow an unknown value to a transport-neutral {@link WireEvent}.
|
|
324
|
+
*
|
|
325
|
+
* @param value - The candidate wire event
|
|
326
|
+
* @returns Whether the value carries an event name, serialized data, and an optional id
|
|
327
|
+
*/
|
|
328
|
+
var isWireEvent = recordOf({
|
|
329
|
+
event: isString,
|
|
330
|
+
data: isString,
|
|
331
|
+
id: isString
|
|
332
|
+
}, ["id"]);
|
|
274
333
|
/** Narrow an unknown value to a {@link TerminalSnapshot}. */
|
|
275
334
|
var isTerminalSnapshot = recordOf({
|
|
276
335
|
id: isNonEmptyString,
|
|
@@ -351,188 +410,246 @@ function isPrintable(character) {
|
|
|
351
410
|
return code >= 32 && code !== 127;
|
|
352
411
|
}
|
|
353
412
|
/**
|
|
354
|
-
*
|
|
355
|
-
*
|
|
356
|
-
*
|
|
413
|
+
* Build a complete {@link PromptTheme} by merging a partial one over
|
|
414
|
+
* {@link DEFAULT_PROMPT_THEME}, leaf by leaf — each supplied icon replaces that glyph, each
|
|
415
|
+
* supplied role replaces that {@link Style}, and everything else keeps its default. Each supplied
|
|
416
|
+
* style is snapshotted through the console module's own
|
|
417
|
+
* {@link import('@orkestrel/console').freezeStyle}, so the result is deeply frozen and a caller
|
|
418
|
+
* mutating its own attribute list afterwards cannot reach into a built theme.
|
|
357
419
|
*
|
|
358
|
-
* @
|
|
359
|
-
*
|
|
360
|
-
*
|
|
361
|
-
*
|
|
362
|
-
*
|
|
363
|
-
*
|
|
364
|
-
*
|
|
365
|
-
*
|
|
366
|
-
*
|
|
367
|
-
*
|
|
368
|
-
*
|
|
369
|
-
*/
|
|
370
|
-
function evaluateRule(rule, check, input) {
|
|
371
|
-
if (typeof check === "function") {
|
|
372
|
-
const result = check(input);
|
|
373
|
-
if (result === true) return void 0;
|
|
374
|
-
return isString(result) ? result : RULE_MESSAGES.invalid;
|
|
375
|
-
}
|
|
376
|
-
switch (rule) {
|
|
377
|
-
case "required":
|
|
378
|
-
if (check === true && input.trim().length === 0) return RULE_MESSAGES.required;
|
|
379
|
-
break;
|
|
380
|
-
case "minimum":
|
|
381
|
-
if (isNumber(check) && [...input].length < check) return RULE_MESSAGES.minimum.replace("{count}", String(check));
|
|
382
|
-
break;
|
|
383
|
-
case "maximum":
|
|
384
|
-
if (isNumber(check) && [...input].length > check) return RULE_MESSAGES.maximum.replace("{count}", String(check));
|
|
385
|
-
break;
|
|
386
|
-
case "pattern":
|
|
387
|
-
if (isString(check)) {
|
|
388
|
-
let compiled;
|
|
389
|
-
try {
|
|
390
|
-
compiled = new RegExp(check);
|
|
391
|
-
} catch {
|
|
392
|
-
return RULE_MESSAGES.pattern.replace("{pattern}", check);
|
|
393
|
-
}
|
|
394
|
-
if (!compiled.test(input)) return RULE_MESSAGES.pattern.replace("{pattern}", check);
|
|
395
|
-
}
|
|
396
|
-
break;
|
|
397
|
-
case "email":
|
|
398
|
-
if (check === true && !EMAIL_PATTERN.test(input)) return RULE_MESSAGES.email;
|
|
399
|
-
break;
|
|
400
|
-
case "url":
|
|
401
|
-
if (check === true && !URL_PATTERN.test(input)) return RULE_MESSAGES.url;
|
|
402
|
-
break;
|
|
403
|
-
case "numeric":
|
|
404
|
-
if (check === true && !NUMERIC_PATTERN.test(input)) return RULE_MESSAGES.numeric;
|
|
405
|
-
break;
|
|
406
|
-
case "integer":
|
|
407
|
-
if (check === true && !INTEGER_PATTERN.test(input)) return RULE_MESSAGES.integer;
|
|
408
|
-
break;
|
|
409
|
-
case "alphanumeric":
|
|
410
|
-
if (check === true && !ALPHANUMERIC_PATTERN.test(input)) return RULE_MESSAGES.alphanumeric;
|
|
411
|
-
break;
|
|
412
|
-
}
|
|
413
|
-
}
|
|
414
|
-
/** Wrap a named rule + its primitive check into a {@link Validator} (returns `true` or the rule's message). */
|
|
415
|
-
function buildRuleValidator(rule, check) {
|
|
416
|
-
return (input) => evaluateRule(rule, check, input) ?? true;
|
|
417
|
-
}
|
|
418
|
-
/**
|
|
419
|
-
* Append a rule-backed {@link Validator} to `validators` when the rule is enabled — a `false` /
|
|
420
|
-
* `undefined` `check` is skipped, a function `check` is added verbatim (the custom override), and
|
|
421
|
-
* a primitive `check` is wrapped via {@link buildRuleValidator}. Mutates `validators` in place.
|
|
420
|
+
* @param options - The partial theme to merge, or `undefined` for the defaults
|
|
421
|
+
* @returns The resolved, deeply frozen theme every prompt state carries
|
|
422
|
+
*
|
|
423
|
+
* @example
|
|
424
|
+
* ```ts
|
|
425
|
+
* createPromptTheme() // the defaults
|
|
426
|
+
* createPromptTheme({
|
|
427
|
+
* icons: { pointer: '=>' },
|
|
428
|
+
* roles: { message: { foreground: 'magenta', attributes: ['bold'] } },
|
|
429
|
+
* })
|
|
430
|
+
* ```
|
|
422
431
|
*/
|
|
423
|
-
function
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
}
|
|
429
|
-
|
|
432
|
+
function createPromptTheme(options) {
|
|
433
|
+
const icons = {
|
|
434
|
+
...DEFAULT_PROMPT_THEME.icons,
|
|
435
|
+
...options?.icons
|
|
436
|
+
};
|
|
437
|
+
const roles = { ...DEFAULT_PROMPT_THEME.roles };
|
|
438
|
+
for (const role of PROMPT_ROLES) {
|
|
439
|
+
const style = options?.roles?.[role];
|
|
440
|
+
if (style !== void 0) roles[role] = freezeStyle(style);
|
|
441
|
+
}
|
|
442
|
+
return Object.freeze({
|
|
443
|
+
icons: Object.freeze(icons),
|
|
444
|
+
roles: Object.freeze(roles)
|
|
445
|
+
});
|
|
430
446
|
}
|
|
431
447
|
/**
|
|
432
|
-
*
|
|
433
|
-
*
|
|
434
|
-
*
|
|
448
|
+
* Sanitize text for one single-line display slot. Composes console's ANSI {@link strip} and C0
|
|
449
|
+
* {@link stripControls} passes with removal of tab, line feed, and carriage return.
|
|
450
|
+
*
|
|
451
|
+
* @param text - The text to sanitize for a glyph or hint slot
|
|
452
|
+
* @returns The text with ANSI sequences, every C0 control character, and DEL removed
|
|
453
|
+
*
|
|
454
|
+
* @example
|
|
455
|
+
* ```ts
|
|
456
|
+
* sanitizeDisplayText('Q\rOVERWRITE\nNEXT\tX') // 'QOVERWRITENEXTX'
|
|
457
|
+
* ```
|
|
435
458
|
*/
|
|
436
|
-
function
|
|
437
|
-
return (
|
|
438
|
-
for (const validator of validators) {
|
|
439
|
-
const result = validator(input);
|
|
440
|
-
if (result !== true) return result;
|
|
441
|
-
}
|
|
442
|
-
return true;
|
|
443
|
-
};
|
|
459
|
+
function sanitizeDisplayText(text) {
|
|
460
|
+
return stripControls(strip(text)).replaceAll(" ", "").replaceAll("\n", "").replaceAll("\r", "");
|
|
444
461
|
}
|
|
445
462
|
/**
|
|
446
|
-
*
|
|
447
|
-
* composed {@link Validator}. A bare validator passes through; rules are appended in the fixed
|
|
448
|
-
* order (required → minimum → maximum → pattern → email → url → numeric → integer → alphanumeric
|
|
449
|
-
* → custom) and composed; absent / empty input yields an always-passing validator. Pure.
|
|
463
|
+
* Sanitize every terminal-readable string in a parsed form schema.
|
|
450
464
|
*
|
|
451
465
|
* @remarks
|
|
452
|
-
*
|
|
453
|
-
*
|
|
454
|
-
*
|
|
455
|
-
*
|
|
466
|
+
* Display strings pass through {@link sanitizeDisplayText}: labels, help, placeholders, masks,
|
|
467
|
+
* choice labels and help, file accept entries, and pattern sources. Identity and answer strings
|
|
468
|
+
* stay verbatim: schema, group, and field names, group references, choice values, and defaults.
|
|
469
|
+
* Rewriting those would sever the rendering copy from the authoritative form. Field metadata is
|
|
470
|
+
* removed because terminal neither renders nor interprets it. Pattern sources are sanitized as
|
|
471
|
+
* text only and are never compiled or executed here.
|
|
456
472
|
*
|
|
457
|
-
* @param
|
|
458
|
-
* @returns
|
|
473
|
+
* @param schema - A schema already accepted by the Form package's `parseForm`
|
|
474
|
+
* @returns A new schema with terminal-readable strings sanitized and field metadata omitted
|
|
459
475
|
*
|
|
460
476
|
* @example
|
|
461
477
|
* ```ts
|
|
462
|
-
*
|
|
463
|
-
*
|
|
464
|
-
* v('ab') // 'Must be at least 3 characters'
|
|
465
|
-
* v('abc') // true
|
|
478
|
+
* sanitizeSchema({ fields: [{ control: 'text', name: 'na\u001bme', label: 'N\u0000ame' }] })
|
|
479
|
+
* // { fields: [{ control: 'text', name: 'na\u001bme', label: 'Name' }] }
|
|
466
480
|
* ```
|
|
467
481
|
*/
|
|
468
|
-
function
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
}
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
482
|
+
function sanitizeSchema(schema) {
|
|
483
|
+
const groups = schema.groups?.map((group) => ({
|
|
484
|
+
name: group.name,
|
|
485
|
+
label: sanitizeDisplayText(group.label),
|
|
486
|
+
...group.help !== void 0 ? { help: sanitizeDisplayText(group.help) } : {}
|
|
487
|
+
}));
|
|
488
|
+
const fields = [];
|
|
489
|
+
for (const source of schema.fields) {
|
|
490
|
+
const { meta: _meta, ...field } = source;
|
|
491
|
+
const rule = field.rule === void 0 ? void 0 : {
|
|
492
|
+
...field.rule,
|
|
493
|
+
...field.rule.pattern !== void 0 ? { pattern: sanitizeDisplayText(field.rule.pattern) } : {}
|
|
494
|
+
};
|
|
495
|
+
const shared = {
|
|
496
|
+
name: field.name,
|
|
497
|
+
...field.label !== void 0 ? { label: sanitizeDisplayText(field.label) } : {},
|
|
498
|
+
...field.help !== void 0 ? { help: sanitizeDisplayText(field.help) } : {},
|
|
499
|
+
...field.group !== void 0 ? { group: field.group } : {},
|
|
500
|
+
...rule !== void 0 ? { rule } : {}
|
|
501
|
+
};
|
|
502
|
+
switch (field.control) {
|
|
503
|
+
case "text":
|
|
504
|
+
case "editor":
|
|
505
|
+
fields.push({
|
|
506
|
+
...field,
|
|
507
|
+
...shared,
|
|
508
|
+
...field.placeholder !== void 0 ? { placeholder: sanitizeDisplayText(field.placeholder) } : {}
|
|
509
|
+
});
|
|
510
|
+
break;
|
|
511
|
+
case "password":
|
|
512
|
+
fields.push({
|
|
513
|
+
...field,
|
|
514
|
+
...shared,
|
|
515
|
+
...field.mask !== void 0 ? { mask: sanitizeDisplayText(field.mask) } : {}
|
|
516
|
+
});
|
|
517
|
+
break;
|
|
518
|
+
case "number":
|
|
519
|
+
fields.push({
|
|
520
|
+
...field,
|
|
521
|
+
...shared,
|
|
522
|
+
...field.placeholder !== void 0 ? { placeholder: sanitizeDisplayText(field.placeholder) } : {}
|
|
523
|
+
});
|
|
524
|
+
break;
|
|
525
|
+
case "date":
|
|
526
|
+
case "time":
|
|
527
|
+
case "datetime":
|
|
528
|
+
case "color":
|
|
529
|
+
fields.push({
|
|
530
|
+
...field,
|
|
531
|
+
...shared
|
|
532
|
+
});
|
|
533
|
+
break;
|
|
534
|
+
case "confirm":
|
|
535
|
+
fields.push({
|
|
536
|
+
...field,
|
|
537
|
+
...shared
|
|
538
|
+
});
|
|
539
|
+
break;
|
|
540
|
+
case "select":
|
|
541
|
+
fields.push({
|
|
542
|
+
...field,
|
|
543
|
+
...shared,
|
|
544
|
+
choices: field.choices.map((choice) => ({
|
|
545
|
+
...choice,
|
|
546
|
+
value: choice.value,
|
|
547
|
+
label: sanitizeDisplayText(choice.label),
|
|
548
|
+
...choice.help !== void 0 ? { help: sanitizeDisplayText(choice.help) } : {}
|
|
549
|
+
}))
|
|
550
|
+
});
|
|
551
|
+
break;
|
|
552
|
+
case "checkbox":
|
|
553
|
+
fields.push({
|
|
554
|
+
...field,
|
|
555
|
+
...shared,
|
|
556
|
+
choices: field.choices.map((choice) => ({
|
|
557
|
+
...choice,
|
|
558
|
+
value: choice.value,
|
|
559
|
+
label: sanitizeDisplayText(choice.label),
|
|
560
|
+
...choice.help !== void 0 ? { help: sanitizeDisplayText(choice.help) } : {}
|
|
561
|
+
}))
|
|
562
|
+
});
|
|
563
|
+
break;
|
|
564
|
+
case "file":
|
|
565
|
+
fields.push({
|
|
566
|
+
...field,
|
|
567
|
+
...shared,
|
|
568
|
+
...field.accept !== void 0 ? { accept: field.accept.map(sanitizeDisplayText) } : {}
|
|
569
|
+
});
|
|
570
|
+
break;
|
|
571
|
+
}
|
|
572
|
+
}
|
|
573
|
+
return {
|
|
574
|
+
...schema.name !== void 0 ? { name: schema.name } : {},
|
|
575
|
+
...schema.label !== void 0 ? { label: sanitizeDisplayText(schema.label) } : {},
|
|
576
|
+
...schema.help !== void 0 ? { help: sanitizeDisplayText(schema.help) } : {},
|
|
577
|
+
...groups !== void 0 ? { groups } : {},
|
|
578
|
+
fields
|
|
579
|
+
};
|
|
488
580
|
}
|
|
489
|
-
/**
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
581
|
+
/**
|
|
582
|
+
* Sanitize every glyph a wire-supplied {@link PromptThemeOptions} carries for a single-line display
|
|
583
|
+
* slot. Only the icons need it: a role is guard-narrowed to a console {@link Style}, whose colors
|
|
584
|
+
* and attributes are fixed name sets, so no role can carry a byte a terminal would act on.
|
|
585
|
+
*
|
|
586
|
+
* @param theme - The narrowed theme options a remote prompt supplied
|
|
587
|
+
* @returns The same theme with every supplied glyph sanitized for a single-line display slot
|
|
588
|
+
*/
|
|
589
|
+
function sanitizeThemeIcons(theme) {
|
|
590
|
+
const icons = theme.icons;
|
|
591
|
+
if (icons === void 0) return theme;
|
|
592
|
+
const sanitized = {};
|
|
593
|
+
for (const [icon, glyph] of Object.entries(icons)) sanitized[icon] = sanitizeDisplayText(glyph);
|
|
594
|
+
return {
|
|
595
|
+
...theme,
|
|
596
|
+
icons: sanitized
|
|
597
|
+
};
|
|
495
598
|
}
|
|
496
|
-
/**
|
|
497
|
-
function
|
|
498
|
-
return
|
|
499
|
-
name: choice,
|
|
500
|
-
value: choice
|
|
501
|
-
} : choice;
|
|
599
|
+
/** The styled prompt-message header (`? message`) — the leading line every active prompt view shares, themed by the `question` + `message` roles. */
|
|
600
|
+
function promptHeader(styler, theme, message) {
|
|
601
|
+
return `${styler.render(theme.roles.question, theme.icons.question)} ${styler.render(theme.roles.message, message)}`;
|
|
502
602
|
}
|
|
503
|
-
/**
|
|
504
|
-
|
|
505
|
-
|
|
603
|
+
/**
|
|
604
|
+
* Render a prompt header followed by a key hint painted with the `hint` role, or the header alone
|
|
605
|
+
* when no hint is supplied.
|
|
606
|
+
*
|
|
607
|
+
* @param styler - The console styler that renders each role
|
|
608
|
+
* @param theme - The resolved prompt theme
|
|
609
|
+
* @param message - The prompt's question text
|
|
610
|
+
* @param hint - The optional key hint to append
|
|
611
|
+
* @returns The rendered header with the optional hint
|
|
612
|
+
*/
|
|
613
|
+
function hintedHeader(styler, theme, message, hint) {
|
|
614
|
+
const head = promptHeader(styler, theme, message);
|
|
615
|
+
return hint === void 0 ? head : `${head} ${styler.render(theme.roles.hint, hint)}`;
|
|
506
616
|
}
|
|
507
|
-
/** The styled submit line (`✔ message`) — the committed header an interactive prompt shows once resolved. */
|
|
508
|
-
function submitHeader(styler, message) {
|
|
509
|
-
return `${styler.
|
|
617
|
+
/** The styled submit line (`✔ message`) — the committed header an interactive prompt shows once resolved, themed by the `success` + `message` roles. */
|
|
618
|
+
function submitHeader(styler, theme, message) {
|
|
619
|
+
return `${styler.render(theme.roles.success, theme.icons.success)} ${styler.render(theme.roles.message, message)}`;
|
|
510
620
|
}
|
|
511
|
-
/** The styled error line (`✖ message`)
|
|
512
|
-
function errorLine(styler, message) {
|
|
513
|
-
return `${styler.
|
|
621
|
+
/** The styled error line (`✖ message`) a form driver appends for a field failure. */
|
|
622
|
+
function errorLine(styler, theme, message) {
|
|
623
|
+
return `${styler.render(theme.roles.error, theme.icons.error)} ${styler.render(theme.roles.error, message)}`;
|
|
514
624
|
}
|
|
515
|
-
/**
|
|
516
|
-
|
|
625
|
+
/**
|
|
626
|
+
* Build the initial text-field key state.
|
|
627
|
+
*
|
|
628
|
+
* @param field - The text field to render
|
|
629
|
+
* @param styler - The styler used to render the view
|
|
630
|
+
* @param theme - The optional terminal theme
|
|
631
|
+
* @returns The initial immutable key state
|
|
632
|
+
*/
|
|
633
|
+
function createInputState(field, styler = createStyler(), theme) {
|
|
517
634
|
return {
|
|
518
|
-
message:
|
|
519
|
-
default:
|
|
520
|
-
|
|
521
|
-
|
|
635
|
+
message: sanitizeDisplayText(field.label ?? field.name),
|
|
636
|
+
default: field.default ?? "",
|
|
637
|
+
styler,
|
|
638
|
+
theme: createPromptTheme(theme),
|
|
522
639
|
value: ""
|
|
523
640
|
};
|
|
524
641
|
}
|
|
525
|
-
/** Render
|
|
642
|
+
/** Render a text-field key state as a styled view. */
|
|
526
643
|
function inputView(state) {
|
|
527
|
-
const
|
|
528
|
-
const
|
|
529
|
-
|
|
644
|
+
const content = state.value.length > 0 ? state.value : state.default;
|
|
645
|
+
const role = state.value.length > 0 ? state.theme.roles.content : state.theme.roles.hint;
|
|
646
|
+
const shown = state.styler.render(role, sanitizeDisplayText(content));
|
|
647
|
+
return `${promptHeader(state.styler, state.theme, state.message)} ${state.styler.render(state.theme.roles.pointer, state.theme.icons.pointer)} ${shown}`;
|
|
530
648
|
}
|
|
531
649
|
/**
|
|
532
650
|
* Advance an input prompt by one {@link KeyEvent} — the pure `(state, key) → PromptStep<string>`
|
|
533
651
|
* reducer. Printable characters extend the value; backspace shrinks it; ctrl-u clears it; ctrl-c
|
|
534
|
-
* cancels; return
|
|
535
|
-
* invalid submit stays active with the error in the view.
|
|
652
|
+
* cancels; return produces the candidate value, with an empty line falling back to the default.
|
|
536
653
|
*/
|
|
537
654
|
function inputReduce(state, key) {
|
|
538
655
|
if (key.ctrl && key.name === "c") return {
|
|
@@ -540,27 +657,14 @@ function inputReduce(state, key) {
|
|
|
540
657
|
view: inputView(state),
|
|
541
658
|
status: "cancel"
|
|
542
659
|
};
|
|
543
|
-
const { error: _error, ...clear } = state;
|
|
544
660
|
if (key.name === "return") {
|
|
545
661
|
const answer = state.value.length > 0 ? state.value : state.default;
|
|
546
|
-
const result = state.validator(answer);
|
|
547
|
-
if (result !== true) {
|
|
548
|
-
const next = {
|
|
549
|
-
...state,
|
|
550
|
-
error: result
|
|
551
|
-
};
|
|
552
|
-
return {
|
|
553
|
-
state: next,
|
|
554
|
-
view: inputView(next),
|
|
555
|
-
status: "active"
|
|
556
|
-
};
|
|
557
|
-
}
|
|
558
662
|
return {
|
|
559
663
|
state: {
|
|
560
|
-
...
|
|
664
|
+
...state,
|
|
561
665
|
value: answer
|
|
562
666
|
},
|
|
563
|
-
view: `${submitHeader(state.styler, state.message)} ${state.styler.
|
|
667
|
+
view: `${submitHeader(state.styler, state.theme, state.message)} ${state.styler.render(state.theme.roles.hint, sanitizeDisplayText(answer))}`,
|
|
564
668
|
status: "submit",
|
|
565
669
|
value: answer
|
|
566
670
|
};
|
|
@@ -572,7 +676,7 @@ function inputReduce(state, key) {
|
|
|
572
676
|
status: "active"
|
|
573
677
|
};
|
|
574
678
|
const next = {
|
|
575
|
-
...
|
|
679
|
+
...state,
|
|
576
680
|
value
|
|
577
681
|
};
|
|
578
682
|
return {
|
|
@@ -581,27 +685,32 @@ function inputReduce(state, key) {
|
|
|
581
685
|
status: "active"
|
|
582
686
|
};
|
|
583
687
|
}
|
|
584
|
-
/**
|
|
585
|
-
|
|
688
|
+
/**
|
|
689
|
+
* Build the initial password-field key state.
|
|
690
|
+
*
|
|
691
|
+
* @param field - The password field to render
|
|
692
|
+
* @param styler - The styler used to render the view
|
|
693
|
+
* @param theme - The optional terminal theme
|
|
694
|
+
* @returns The initial immutable key state
|
|
695
|
+
*/
|
|
696
|
+
function createPasswordState(field, styler = createStyler(), theme) {
|
|
586
697
|
return {
|
|
587
|
-
message:
|
|
588
|
-
mask:
|
|
589
|
-
|
|
590
|
-
|
|
698
|
+
message: sanitizeDisplayText(field.label ?? field.name),
|
|
699
|
+
mask: sanitizeDisplayText(field.mask ?? "*"),
|
|
700
|
+
styler,
|
|
701
|
+
theme: createPromptTheme(theme),
|
|
591
702
|
value: ""
|
|
592
703
|
};
|
|
593
704
|
}
|
|
594
|
-
/** Render a
|
|
705
|
+
/** Render a password-field key state as a styled view. */
|
|
595
706
|
function passwordView(state) {
|
|
596
|
-
const masked = state.mask.repeat(state.value.length);
|
|
597
|
-
|
|
598
|
-
return state.error === void 0 ? head : `${head}\n${errorLine(state.styler, state.error)}`;
|
|
707
|
+
const masked = state.styler.render(state.theme.roles.content, state.mask.repeat(state.value.length));
|
|
708
|
+
return `${promptHeader(state.styler, state.theme, state.message)} ${state.styler.render(state.theme.roles.pointer, state.theme.icons.pointer)} ${masked}`;
|
|
599
709
|
}
|
|
600
710
|
/**
|
|
601
711
|
* Advance a password prompt by one {@link KeyEvent} — the pure `(state, key) → PromptStep<string>`
|
|
602
712
|
* reducer. Identical line-editing to {@link inputReduce} (printable extends, backspace shrinks,
|
|
603
|
-
* ctrl-u clears, ctrl-c cancels) but the view masks the value
|
|
604
|
-
* validator (no default fallback — a password has no echoed default).
|
|
713
|
+
* ctrl-u clears, ctrl-c cancels) but the view masks the value. Return produces the candidate value.
|
|
605
714
|
*/
|
|
606
715
|
function passwordReduce(state, key) {
|
|
607
716
|
if (key.ctrl && key.name === "c") return {
|
|
@@ -609,27 +718,12 @@ function passwordReduce(state, key) {
|
|
|
609
718
|
view: passwordView(state),
|
|
610
719
|
status: "cancel"
|
|
611
720
|
};
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
error: result
|
|
619
|
-
};
|
|
620
|
-
return {
|
|
621
|
-
state: next,
|
|
622
|
-
view: passwordView(next),
|
|
623
|
-
status: "active"
|
|
624
|
-
};
|
|
625
|
-
}
|
|
626
|
-
return {
|
|
627
|
-
state: clear,
|
|
628
|
-
view: `${submitHeader(state.styler, state.message)} ${state.styler.dim(state.mask.repeat(state.value.length))}`,
|
|
629
|
-
status: "submit",
|
|
630
|
-
value: state.value
|
|
631
|
-
};
|
|
632
|
-
}
|
|
721
|
+
if (key.name === "return") return {
|
|
722
|
+
state,
|
|
723
|
+
view: `${submitHeader(state.styler, state.theme, state.message)} ${state.styler.render(state.theme.roles.hint, state.mask.repeat(state.value.length))}`,
|
|
724
|
+
status: "submit",
|
|
725
|
+
value: state.value
|
|
726
|
+
};
|
|
633
727
|
const value = editLine(state.value, key);
|
|
634
728
|
if (value === void 0) return {
|
|
635
729
|
state,
|
|
@@ -637,7 +731,7 @@ function passwordReduce(state, key) {
|
|
|
637
731
|
status: "active"
|
|
638
732
|
};
|
|
639
733
|
const next = {
|
|
640
|
-
...
|
|
734
|
+
...state,
|
|
641
735
|
value
|
|
642
736
|
};
|
|
643
737
|
return {
|
|
@@ -646,18 +740,29 @@ function passwordReduce(state, key) {
|
|
|
646
740
|
status: "active"
|
|
647
741
|
};
|
|
648
742
|
}
|
|
649
|
-
/**
|
|
650
|
-
|
|
743
|
+
/**
|
|
744
|
+
* Build the initial confirm-field key state.
|
|
745
|
+
*
|
|
746
|
+
* @param field - The confirm field to render
|
|
747
|
+
* @param styler - The styler used to render the view
|
|
748
|
+
* @param theme - The optional terminal theme
|
|
749
|
+
* @returns The initial immutable key state
|
|
750
|
+
*/
|
|
751
|
+
function createConfirmState(field, styler = createStyler(), theme) {
|
|
651
752
|
return {
|
|
652
|
-
message:
|
|
653
|
-
default:
|
|
654
|
-
styler
|
|
753
|
+
message: sanitizeDisplayText(field.label ?? field.name),
|
|
754
|
+
default: field.default ?? false,
|
|
755
|
+
styler,
|
|
756
|
+
theme: createPromptTheme(theme)
|
|
655
757
|
};
|
|
656
758
|
}
|
|
657
|
-
/**
|
|
759
|
+
/**
|
|
760
|
+
* Render a confirm-field key state as a styled view. The selected role paints the default letter.
|
|
761
|
+
*/
|
|
658
762
|
function confirmView(state) {
|
|
659
|
-
const
|
|
660
|
-
|
|
763
|
+
const head = promptHeader(state.styler, state.theme, state.message);
|
|
764
|
+
const answer = state.default ? `${state.styler.render(state.theme.roles.selected, "Y")}${state.styler.render(state.theme.roles.hint, "/n")}` : `${state.styler.render(state.theme.roles.hint, "y/")}${state.styler.render(state.theme.roles.selected, "N")}`;
|
|
765
|
+
return `${head} ${state.styler.render(state.theme.roles.hint, "(")}${answer}${state.styler.render(state.theme.roles.hint, ")")}`;
|
|
661
766
|
}
|
|
662
767
|
/**
|
|
663
768
|
* Advance a confirm prompt by one {@link KeyEvent} — the pure `(state, key) → PromptStep<boolean>`
|
|
@@ -682,29 +787,37 @@ function confirmReduce(state, key) {
|
|
|
682
787
|
};
|
|
683
788
|
return {
|
|
684
789
|
state,
|
|
685
|
-
view: `${submitHeader(state.styler, state.message)} ${state.styler.
|
|
790
|
+
view: `${submitHeader(state.styler, state.theme, state.message)} ${state.styler.render(state.theme.roles.hint, answer ? "yes" : "no")}`,
|
|
686
791
|
status: "submit",
|
|
687
792
|
value: answer
|
|
688
793
|
};
|
|
689
794
|
}
|
|
690
|
-
/**
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
795
|
+
/**
|
|
796
|
+
* Build the initial select-field key state.
|
|
797
|
+
*
|
|
798
|
+
* @param field - The select field to render
|
|
799
|
+
* @param styler - The styler used to render the view
|
|
800
|
+
* @param theme - The optional terminal theme
|
|
801
|
+
* @returns The initial immutable key state
|
|
802
|
+
*/
|
|
803
|
+
function createSelectState(field, styler = createStyler(), theme) {
|
|
804
|
+
const choices = [...field.choices];
|
|
805
|
+
const index = choices.findIndex((choice) => choice.value === field.default);
|
|
694
806
|
return {
|
|
695
|
-
message:
|
|
807
|
+
message: sanitizeDisplayText(field.label ?? field.name),
|
|
696
808
|
choices,
|
|
697
|
-
styler
|
|
809
|
+
styler,
|
|
810
|
+
theme: createPromptTheme(theme),
|
|
698
811
|
focused: index >= 0 ? index : 0
|
|
699
812
|
};
|
|
700
813
|
}
|
|
701
|
-
/** Render a
|
|
814
|
+
/** Render a select-field key state as a multi-line styled view. */
|
|
702
815
|
function selectView(state) {
|
|
703
816
|
const lines = state.choices.map((choice, index) => {
|
|
704
817
|
const active = index === state.focused;
|
|
705
|
-
return `${active ? state.styler.
|
|
818
|
+
return `${active ? state.styler.render(state.theme.roles.pointer, state.theme.icons.pointer) : " "} ${active ? state.styler.render(state.theme.roles.selected, state.theme.icons.selected) : state.styler.render(state.theme.roles.muted, state.theme.icons.dot)} ${active ? state.styler.render(state.theme.roles.focus, choice.label) : state.styler.render(state.theme.roles.content, choice.label)}${choice.help === void 0 ? "" : ` ${state.styler.render(state.theme.roles.description, choice.help)}`}`;
|
|
706
819
|
});
|
|
707
|
-
return [promptHeader(state.styler, state.message), ...lines].join("\n");
|
|
820
|
+
return [promptHeader(state.styler, state.theme, state.message), ...lines].join("\n");
|
|
708
821
|
}
|
|
709
822
|
/**
|
|
710
823
|
* Advance a select prompt by one {@link KeyEvent} — the pure `(state, key) → PromptStep<string>`
|
|
@@ -751,7 +864,7 @@ function selectReduce(state, key) {
|
|
|
751
864
|
const value = choice?.value ?? "";
|
|
752
865
|
return {
|
|
753
866
|
state,
|
|
754
|
-
view: `${submitHeader(state.styler, state.message)} ${state.styler.
|
|
867
|
+
view: `${submitHeader(state.styler, state.theme, state.message)} ${state.styler.render(state.theme.roles.hint, choice?.label ?? "")}`,
|
|
755
868
|
status: "submit",
|
|
756
869
|
value
|
|
757
870
|
};
|
|
@@ -762,44 +875,48 @@ function selectReduce(state, key) {
|
|
|
762
875
|
status: "active"
|
|
763
876
|
};
|
|
764
877
|
}
|
|
765
|
-
/**
|
|
766
|
-
|
|
767
|
-
|
|
878
|
+
/**
|
|
879
|
+
* Build the initial checkbox-field key state.
|
|
880
|
+
*
|
|
881
|
+
* @param field - The checkbox field to render
|
|
882
|
+
* @param styler - The styler used to render the view
|
|
883
|
+
* @param theme - The optional terminal theme
|
|
884
|
+
* @returns The initial immutable key state
|
|
885
|
+
*/
|
|
886
|
+
function createCheckboxState(field, styler = createStyler(), theme) {
|
|
887
|
+
const choices = [...field.choices];
|
|
768
888
|
const checked = choices.reduce((indices, choice, index) => {
|
|
769
|
-
if (choice.
|
|
889
|
+
if (field.default?.includes(choice.value) === true) indices.push(index);
|
|
770
890
|
return indices;
|
|
771
891
|
}, []);
|
|
772
892
|
return {
|
|
773
|
-
message:
|
|
893
|
+
message: sanitizeDisplayText(field.label ?? field.name),
|
|
774
894
|
choices,
|
|
775
|
-
styler
|
|
895
|
+
styler,
|
|
896
|
+
theme: createPromptTheme(theme),
|
|
776
897
|
focused: 0,
|
|
777
|
-
checked
|
|
778
|
-
...options.min !== void 0 ? { min: options.min } : {},
|
|
779
|
-
...options.max !== void 0 ? { max: options.max } : {}
|
|
898
|
+
checked
|
|
780
899
|
};
|
|
781
900
|
}
|
|
782
|
-
/** Render a
|
|
901
|
+
/** Render a checkbox-field key state as a multi-line styled view. */
|
|
783
902
|
function checkboxView(state) {
|
|
784
903
|
const lines = state.choices.map((choice, index) => {
|
|
785
904
|
const active = index === state.focused;
|
|
786
905
|
const ticked = state.checked.includes(index);
|
|
787
|
-
return `${active ? state.styler.
|
|
906
|
+
return `${active ? state.styler.render(state.theme.roles.pointer, state.theme.icons.pointer) : " "} ${ticked ? state.styler.render(state.theme.roles.selected, state.theme.icons.checked) : state.styler.render(state.theme.roles.muted, state.theme.icons.unchecked)} ${active ? state.styler.render(state.theme.roles.focus, choice.label) : state.styler.render(state.theme.roles.content, choice.label)}${choice.help === void 0 ? "" : ` ${state.styler.render(state.theme.roles.description, choice.help)}`}`;
|
|
788
907
|
});
|
|
789
|
-
const summary = state.styler.
|
|
790
|
-
|
|
791
|
-
promptHeader(state.styler, state.message),
|
|
908
|
+
const summary = state.styler.render(state.theme.roles.hint, `${state.checked.length} selected`);
|
|
909
|
+
return [
|
|
910
|
+
promptHeader(state.styler, state.theme, state.message),
|
|
792
911
|
...lines,
|
|
793
912
|
summary
|
|
794
913
|
].join("\n");
|
|
795
|
-
return state.error === void 0 ? body : `${body}\n${errorLine(state.styler, state.error)}`;
|
|
796
914
|
}
|
|
797
915
|
/**
|
|
798
916
|
* Advance a checkbox prompt by one {@link KeyEvent} — the pure
|
|
799
917
|
* `(state, key) → PromptStep<readonly string[]>` reducer. `up` / `down` (and `k` / `j`) move the
|
|
800
918
|
* focus (wrapping); `space` toggles the focused index in the checked set; return submits the
|
|
801
|
-
* checked values in
|
|
802
|
-
* the reason in the view); ctrl-c cancels.
|
|
919
|
+
* checked values in choice order; ctrl-c cancels. The form applies selection-count rules.
|
|
803
920
|
*/
|
|
804
921
|
function checkboxReduce(state, key) {
|
|
805
922
|
if (key.ctrl && key.name === "c") return {
|
|
@@ -808,10 +925,9 @@ function checkboxReduce(state, key) {
|
|
|
808
925
|
status: "cancel"
|
|
809
926
|
};
|
|
810
927
|
const count = state.choices.length;
|
|
811
|
-
const { error: _error, ...clear } = state;
|
|
812
928
|
if ((key.name === "up" || key.name === "k") && count > 0) {
|
|
813
929
|
const next = {
|
|
814
|
-
...
|
|
930
|
+
...state,
|
|
815
931
|
focused: (state.focused - 1 + count) % count
|
|
816
932
|
};
|
|
817
933
|
return {
|
|
@@ -822,7 +938,7 @@ function checkboxReduce(state, key) {
|
|
|
822
938
|
}
|
|
823
939
|
if ((key.name === "down" || key.name === "j") && count > 0) {
|
|
824
940
|
const next = {
|
|
825
|
-
...
|
|
941
|
+
...state,
|
|
826
942
|
focused: (state.focused + 1) % count
|
|
827
943
|
};
|
|
828
944
|
return {
|
|
@@ -834,7 +950,7 @@ function checkboxReduce(state, key) {
|
|
|
834
950
|
if (key.name === "space" && count > 0) {
|
|
835
951
|
const checked = toggleIndex(state.checked, state.focused);
|
|
836
952
|
const next = {
|
|
837
|
-
...
|
|
953
|
+
...state,
|
|
838
954
|
checked
|
|
839
955
|
};
|
|
840
956
|
return {
|
|
@@ -844,24 +960,12 @@ function checkboxReduce(state, key) {
|
|
|
844
960
|
};
|
|
845
961
|
}
|
|
846
962
|
if (key.name === "return") {
|
|
847
|
-
const error = gateSelection(state.checked.length, state.min, state.max);
|
|
848
|
-
if (error !== void 0) {
|
|
849
|
-
const next = {
|
|
850
|
-
...state,
|
|
851
|
-
error
|
|
852
|
-
};
|
|
853
|
-
return {
|
|
854
|
-
state: next,
|
|
855
|
-
view: checkboxView(next),
|
|
856
|
-
status: "active"
|
|
857
|
-
};
|
|
858
|
-
}
|
|
859
963
|
const ordered = [...state.checked].sort((a, b) => a - b);
|
|
860
964
|
const values = ordered.map((index) => state.choices[index]?.value).filter((value) => value !== void 0);
|
|
861
|
-
const summary = ordered.map((index) => state.choices[index]?.
|
|
965
|
+
const summary = ordered.map((index) => state.choices[index]?.label).filter((name) => name !== void 0).join(", ");
|
|
862
966
|
return {
|
|
863
|
-
state
|
|
864
|
-
view: `${submitHeader(state.styler, state.message)} ${state.styler.
|
|
967
|
+
state,
|
|
968
|
+
view: `${submitHeader(state.styler, state.theme, state.message)} ${state.styler.render(state.theme.roles.hint, summary)}`,
|
|
865
969
|
status: "submit",
|
|
866
970
|
value: values
|
|
867
971
|
};
|
|
@@ -876,33 +980,37 @@ function checkboxReduce(state, key) {
|
|
|
876
980
|
function toggleIndex(indices, index) {
|
|
877
981
|
return indices.includes(index) ? indices.filter((i) => i !== index) : [...indices, index];
|
|
878
982
|
}
|
|
879
|
-
/**
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
983
|
+
/**
|
|
984
|
+
* Build the initial editor-field key state.
|
|
985
|
+
*
|
|
986
|
+
* @param field - The editor field to render
|
|
987
|
+
* @param styler - The styler used to render the view
|
|
988
|
+
* @param theme - The optional terminal theme
|
|
989
|
+
* @returns The initial immutable key state
|
|
990
|
+
*/
|
|
991
|
+
function createEditorState(field, styler = createStyler(), theme) {
|
|
886
992
|
return {
|
|
887
|
-
message:
|
|
888
|
-
default:
|
|
889
|
-
|
|
890
|
-
|
|
993
|
+
message: sanitizeDisplayText(field.label ?? field.name),
|
|
994
|
+
default: field.default ?? "",
|
|
995
|
+
styler,
|
|
996
|
+
theme: createPromptTheme(theme),
|
|
891
997
|
lines: [],
|
|
892
998
|
current: ""
|
|
893
999
|
};
|
|
894
1000
|
}
|
|
895
|
-
/**
|
|
1001
|
+
/**
|
|
1002
|
+
* Render an editor-field key state as a multi-line styled view with its finish hint.
|
|
1003
|
+
*/
|
|
896
1004
|
function editorView(state) {
|
|
897
|
-
const
|
|
898
|
-
|
|
1005
|
+
const head = hintedHeader(state.styler, state.theme, state.message, "(Ctrl+D to finish)");
|
|
1006
|
+
const pointer = state.styler.render(state.theme.roles.pointer, state.theme.icons.pointer);
|
|
1007
|
+
return [head, ...[...state.lines.map((line) => state.styler.render(state.theme.roles.content, line)), `${pointer} ${state.styler.render(state.theme.roles.content, state.current)}`]].join("\n");
|
|
899
1008
|
}
|
|
900
1009
|
/**
|
|
901
1010
|
* Advance an editor prompt by one {@link KeyEvent} — the pure `(state, key) → PromptStep<string>`
|
|
902
1011
|
* reducer. Printable characters extend the current line; backspace shrinks it; return commits the
|
|
903
1012
|
* current line and starts a fresh one; ctrl-d FINISHES (joining all lines, falling back to the
|
|
904
|
-
* default when empty)
|
|
905
|
-
* the error.
|
|
1013
|
+
* default when empty); ctrl-c cancels. The form validates the candidate after the driver fills it.
|
|
906
1014
|
*/
|
|
907
1015
|
function editorReduce(state, key) {
|
|
908
1016
|
if (key.ctrl && key.name === "c") return {
|
|
@@ -910,33 +1018,20 @@ function editorReduce(state, key) {
|
|
|
910
1018
|
view: editorView(state),
|
|
911
1019
|
status: "cancel"
|
|
912
1020
|
};
|
|
913
|
-
const { error: _error, ...clear } = state;
|
|
914
1021
|
if (key.ctrl && key.name === "d") {
|
|
915
1022
|
const lines = state.current.length > 0 ? [...state.lines, state.current] : state.lines;
|
|
916
1023
|
const joined = lines.join("\n");
|
|
917
1024
|
const answer = joined.length > 0 ? joined : state.default;
|
|
918
|
-
const result = state.validator(answer);
|
|
919
|
-
if (result !== true) {
|
|
920
|
-
const next = {
|
|
921
|
-
...state,
|
|
922
|
-
error: result
|
|
923
|
-
};
|
|
924
|
-
return {
|
|
925
|
-
state: next,
|
|
926
|
-
view: editorView(next),
|
|
927
|
-
status: "active"
|
|
928
|
-
};
|
|
929
|
-
}
|
|
930
1025
|
return {
|
|
931
|
-
state
|
|
932
|
-
view: `${submitHeader(state.styler, state.message)} ${state.styler.
|
|
1026
|
+
state,
|
|
1027
|
+
view: `${submitHeader(state.styler, state.theme, state.message)} ${state.styler.render(state.theme.roles.hint, `${String(lines.length)} line${lines.length === 1 ? "" : "s"}`)}`,
|
|
933
1028
|
status: "submit",
|
|
934
1029
|
value: answer
|
|
935
1030
|
};
|
|
936
1031
|
}
|
|
937
1032
|
if (key.name === "return") {
|
|
938
1033
|
const next = {
|
|
939
|
-
...
|
|
1034
|
+
...state,
|
|
940
1035
|
lines: [...state.lines, state.current],
|
|
941
1036
|
current: ""
|
|
942
1037
|
};
|
|
@@ -953,7 +1048,7 @@ function editorReduce(state, key) {
|
|
|
953
1048
|
status: "active"
|
|
954
1049
|
};
|
|
955
1050
|
const next = {
|
|
956
|
-
...
|
|
1051
|
+
...state,
|
|
957
1052
|
current
|
|
958
1053
|
};
|
|
959
1054
|
return {
|
|
@@ -975,205 +1070,6 @@ function editLine(value, key) {
|
|
|
975
1070
|
if (!key.ctrl && !key.meta && [...key.name].length === 1 && isPrintable(key.name)) return `${value}${key.sequence}`;
|
|
976
1071
|
}
|
|
977
1072
|
/**
|
|
978
|
-
* Produce the WIRE-SAFE form of a prompt's options — the {@link PendingPrompt.options} a broker
|
|
979
|
-
* serializes over SSE. Drops everything that cannot cross the wire (a `styler`, and any
|
|
980
|
-
* function-valued `validate` rule), while KEEPING the declarative data a remote client needs to
|
|
981
|
-
* reconstruct the prompt: the {@link ValidationRules} as data, plus `message` / `choices` /
|
|
982
|
-
* `default` / `mask` / `min` / `max`. This is WHY T-a's validation is rules-as-data: a `Validator`
|
|
983
|
-
* FUNCTION can't be serialized, but its declarative rules can — the client rebuilds the validator
|
|
984
|
-
* from them via {@link resolveValidation}.
|
|
985
|
-
*
|
|
986
|
-
* @remarks
|
|
987
|
-
* - **Functions are dropped.** A top-level function value (e.g. a bare-`Validator` `validate`) is
|
|
988
|
-
* omitted entirely; the `styler` (a fluent function-bearing object) is dropped by key.
|
|
989
|
-
* - **`validate` rules are flattened.** A {@link ValidationRules} bag is copied with each
|
|
990
|
-
* function rule replaced by `true` (the rule's INTENT survives as the built-in check; its
|
|
991
|
-
* custom function does not). A bare-function `validate` is dropped (no declarative form to keep).
|
|
992
|
-
* - **`choices` are function-stripped.** Each choice keeps its plain fields (`name` / `value` /
|
|
993
|
-
* `description` / `checked`); a bare string passes through.
|
|
994
|
-
*
|
|
995
|
-
* @param options - The raw prompt options bag (may hold functions / a styler)
|
|
996
|
-
* @returns A JSON-safe options record — only serializable, declarative data
|
|
997
|
-
*/
|
|
998
|
-
function serializePromptOptions(options) {
|
|
999
|
-
const result = {};
|
|
1000
|
-
for (const [key, value] of Object.entries(options)) {
|
|
1001
|
-
if (key === "styler" || typeof value === "function") continue;
|
|
1002
|
-
if (key === "validate") {
|
|
1003
|
-
const rules = serializeValidationRules(value);
|
|
1004
|
-
if (rules !== void 0) result[key] = rules;
|
|
1005
|
-
continue;
|
|
1006
|
-
}
|
|
1007
|
-
if (key === "choices") {
|
|
1008
|
-
result[key] = serializeChoices(value);
|
|
1009
|
-
continue;
|
|
1010
|
-
}
|
|
1011
|
-
result[key] = value;
|
|
1012
|
-
}
|
|
1013
|
-
return result;
|
|
1014
|
-
}
|
|
1015
|
-
/**
|
|
1016
|
-
* Flatten a `validate` option to its wire-safe {@link ValidationRules} DATA — a function rule
|
|
1017
|
-
* becomes `true` (its built-in check survives; its body cannot cross the wire). A bare-function
|
|
1018
|
-
* `validate` (no rules object) has no declarative form, so it yields `undefined` (dropped).
|
|
1019
|
-
*/
|
|
1020
|
-
function serializeValidationRules(validate) {
|
|
1021
|
-
if (!isRecord(validate)) return void 0;
|
|
1022
|
-
const rules = {};
|
|
1023
|
-
for (const [rule, value] of Object.entries(validate)) rules[rule] = typeof value === "function" ? true : value;
|
|
1024
|
-
return rules;
|
|
1025
|
-
}
|
|
1026
|
-
/** Strip functions from a `choices` option — each choice keeps its plain fields; a bare string passes through. */
|
|
1027
|
-
function serializeChoices(choices) {
|
|
1028
|
-
if (!Array.isArray(choices)) return [];
|
|
1029
|
-
return choices.map((choice) => {
|
|
1030
|
-
if (isString(choice)) return choice;
|
|
1031
|
-
if (!isRecord(choice)) return choice;
|
|
1032
|
-
const normalized = {};
|
|
1033
|
-
for (const [key, value] of Object.entries(choice)) if (typeof value !== "function") normalized[key] = value;
|
|
1034
|
-
return normalized;
|
|
1035
|
-
});
|
|
1036
|
-
}
|
|
1037
|
-
/**
|
|
1038
|
-
* Rebuild a wire-decoded `validate` payload into a {@link ValidationRules} bag — the inverse of
|
|
1039
|
-
* {@link serializeValidationRules}. Keeps only the primitive rule values (`boolean` / `number` /
|
|
1040
|
-
* `string`) a serialized prompt could carry; an empty / non-record / all-dropped payload yields
|
|
1041
|
-
* `undefined` (no rules to apply). The client feeds the result back through {@link resolveValidation}.
|
|
1042
|
-
*/
|
|
1043
|
-
function reconstructValidationRules(value) {
|
|
1044
|
-
if (!isRecord(value)) return void 0;
|
|
1045
|
-
const rules = {};
|
|
1046
|
-
let count = 0;
|
|
1047
|
-
for (const [rule, item] of Object.entries(value)) {
|
|
1048
|
-
if (rule === "pattern") continue;
|
|
1049
|
-
if (isBoolean(item) || isNumber(item) || isString(item)) {
|
|
1050
|
-
rules[rule] = item;
|
|
1051
|
-
count += 1;
|
|
1052
|
-
}
|
|
1053
|
-
}
|
|
1054
|
-
if (count === 0) return void 0;
|
|
1055
|
-
return rules;
|
|
1056
|
-
}
|
|
1057
|
-
/** Read one option by key, narrowed by `guard` — `undefined` when absent or off-shape (§14, never an `as`). */
|
|
1058
|
-
function resolveOption(options, key, guard) {
|
|
1059
|
-
const value = options[key];
|
|
1060
|
-
return guard(value) ? value : void 0;
|
|
1061
|
-
}
|
|
1062
|
-
/** Narrow an unknown value to a {@link PromptChoice} — the `recordOf` shape inlined so no non-exported member lingers (§5). */
|
|
1063
|
-
function isPromptChoice(value) {
|
|
1064
|
-
return recordOf({
|
|
1065
|
-
name: isString,
|
|
1066
|
-
value: isString,
|
|
1067
|
-
description: isString
|
|
1068
|
-
}, ["description"])(value);
|
|
1069
|
-
}
|
|
1070
|
-
/** Narrow an unknown value to a {@link CheckboxChoice} — the `recordOf` shape inlined so no non-exported member lingers (§5). */
|
|
1071
|
-
function isCheckboxChoice(value) {
|
|
1072
|
-
return recordOf({
|
|
1073
|
-
name: isString,
|
|
1074
|
-
value: isString,
|
|
1075
|
-
description: isString,
|
|
1076
|
-
checked: isBoolean
|
|
1077
|
-
}, ["description", "checked"])(value);
|
|
1078
|
-
}
|
|
1079
|
-
/** Read a `choices` option as a list of bare strings / full choices — each element narrowed by `guard`, off-shape elements stringified. */
|
|
1080
|
-
function resolveChoices(options, guard) {
|
|
1081
|
-
const choices = options.choices;
|
|
1082
|
-
if (!Array.isArray(choices)) return [];
|
|
1083
|
-
return choices.map((choice) => {
|
|
1084
|
-
if (isString(choice)) return choice;
|
|
1085
|
-
if (guard(choice)) return choice;
|
|
1086
|
-
return String(choice);
|
|
1087
|
-
});
|
|
1088
|
-
}
|
|
1089
|
-
/**
|
|
1090
|
-
* Sanitize a list of resolved choices' human-readable labels (`name` + `description`) with
|
|
1091
|
-
* {@link stripControls} — shared by the `select` and `checkbox` branches of
|
|
1092
|
-
* {@link dispatchPendingPrompt} so a remote-supplied choice can never inject raw control bytes
|
|
1093
|
-
* into the local terminal's rendered view.
|
|
1094
|
-
*
|
|
1095
|
-
* @param choices - The resolved choices (bare strings or full {@link PromptChoice} /
|
|
1096
|
-
* {@link CheckboxChoice} objects) as returned by {@link resolveChoices}
|
|
1097
|
-
* @returns The same choices with every `name` / `description` control-stripped
|
|
1098
|
-
*/
|
|
1099
|
-
function sanitizeChoiceLabels(choices) {
|
|
1100
|
-
return choices.map((choice) => {
|
|
1101
|
-
if (isString(choice)) return stripControls(choice);
|
|
1102
|
-
return {
|
|
1103
|
-
...choice,
|
|
1104
|
-
name: stripControls(choice.name),
|
|
1105
|
-
...choice.description !== void 0 ? { description: stripControls(choice.description) } : {}
|
|
1106
|
-
};
|
|
1107
|
-
});
|
|
1108
|
-
}
|
|
1109
|
-
/**
|
|
1110
|
-
* Dispatch a {@link PendingPrompt} to the matching {@link PromptFormInterface} method — the bridge
|
|
1111
|
-
* step a {@link PromptClient} runs to drive a LOCAL terminal with a prompt issued elsewhere.
|
|
1112
|
-
* Reconstructs typed options from the wire-safe {@link PendingPrompt.options} (every field
|
|
1113
|
-
* §14-narrowed, never an `as`; the validator rebuilt from rules via {@link reconstructValidationRules}),
|
|
1114
|
-
* then calls the matching prompt form and returns its resolved value.
|
|
1115
|
-
*
|
|
1116
|
-
* @param terminal - The local {@link PromptFormInterface} to drive
|
|
1117
|
-
* @param pending - The decoded pending prompt to dispatch
|
|
1118
|
-
* @returns The prompt's resolved value (a `string` / `boolean` / `readonly string[]` per form)
|
|
1119
|
-
*/
|
|
1120
|
-
function dispatchPendingPrompt(terminal, pending) {
|
|
1121
|
-
const options = pending.options;
|
|
1122
|
-
const validate = reconstructValidationRules(options.validate);
|
|
1123
|
-
const message = stripControls(pending.message);
|
|
1124
|
-
switch (pending.form) {
|
|
1125
|
-
case "input": {
|
|
1126
|
-
const value = resolveOption(options, "default", isString);
|
|
1127
|
-
return terminal.input({
|
|
1128
|
-
message,
|
|
1129
|
-
...value !== void 0 ? { default: stripControls(value) } : {},
|
|
1130
|
-
...validate !== void 0 ? { validate } : {}
|
|
1131
|
-
});
|
|
1132
|
-
}
|
|
1133
|
-
case "password": {
|
|
1134
|
-
const value = resolveOption(options, "mask", isString);
|
|
1135
|
-
return terminal.password({
|
|
1136
|
-
message,
|
|
1137
|
-
...value !== void 0 ? { mask: stripControls(value) } : {},
|
|
1138
|
-
...validate !== void 0 ? { validate } : {}
|
|
1139
|
-
});
|
|
1140
|
-
}
|
|
1141
|
-
case "confirm": {
|
|
1142
|
-
const value = resolveOption(options, "default", isBoolean);
|
|
1143
|
-
return terminal.confirm({
|
|
1144
|
-
message,
|
|
1145
|
-
...value !== void 0 ? { default: value } : {}
|
|
1146
|
-
});
|
|
1147
|
-
}
|
|
1148
|
-
case "select": {
|
|
1149
|
-
const value = resolveOption(options, "default", isString);
|
|
1150
|
-
return terminal.select({
|
|
1151
|
-
message,
|
|
1152
|
-
choices: sanitizeChoiceLabels(resolveChoices(options, isPromptChoice)),
|
|
1153
|
-
...value !== void 0 ? { default: stripControls(value) } : {}
|
|
1154
|
-
});
|
|
1155
|
-
}
|
|
1156
|
-
case "checkbox": {
|
|
1157
|
-
const min = resolveOption(options, "min", isNumber);
|
|
1158
|
-
const max = resolveOption(options, "max", isNumber);
|
|
1159
|
-
return terminal.checkbox({
|
|
1160
|
-
message,
|
|
1161
|
-
choices: sanitizeChoiceLabels(resolveChoices(options, isCheckboxChoice)),
|
|
1162
|
-
...min !== void 0 ? { min } : {},
|
|
1163
|
-
...max !== void 0 ? { max } : {}
|
|
1164
|
-
});
|
|
1165
|
-
}
|
|
1166
|
-
case "editor": {
|
|
1167
|
-
const value = resolveOption(options, "default", isString);
|
|
1168
|
-
return terminal.editor({
|
|
1169
|
-
message,
|
|
1170
|
-
...value !== void 0 ? { default: stripControls(value) } : {},
|
|
1171
|
-
...validate !== void 0 ? { validate } : {}
|
|
1172
|
-
});
|
|
1173
|
-
}
|
|
1174
|
-
}
|
|
1175
|
-
}
|
|
1176
|
-
/**
|
|
1177
1073
|
* The default {@link import('./types.js').TimerHandler} — a thin host `setTimeout` / `clearTimeout`
|
|
1178
1074
|
* wrapper that arms `callback` after `ms` and returns a {@link TimerCancel}. The deadline seam
|
|
1179
1075
|
* behind both the {@link import('./Prompt.js').Prompt} broker (its expiry) and the
|
|
@@ -1197,19 +1093,6 @@ function isAbortError(error) {
|
|
|
1197
1093
|
return (error instanceof DOMException || error instanceof Error) && error.name === "AbortError";
|
|
1198
1094
|
}
|
|
1199
1095
|
/**
|
|
1200
|
-
* Parse a JSON wire string TOTAL — a malformed / empty payload yields `undefined` (the caller's
|
|
1201
|
-
* guard then rejects it), never a throw. The {@link import('./PromptClient.js').PromptClient}
|
|
1202
|
-
* decodes every SSE `data` field through this before §14-narrowing it.
|
|
1203
|
-
*/
|
|
1204
|
-
function parseWireJSON(text) {
|
|
1205
|
-
if (text.length === 0) return void 0;
|
|
1206
|
-
try {
|
|
1207
|
-
return JSON.parse(text);
|
|
1208
|
-
} catch {
|
|
1209
|
-
return;
|
|
1210
|
-
}
|
|
1211
|
-
}
|
|
1212
|
-
/**
|
|
1213
1096
|
* Whether `url` is an INSECURE remote endpoint — a plain `http://` URL whose host is NOT a
|
|
1214
1097
|
* loopback address. Pure string parsing (no `URL` global), so it stays total on malformed input.
|
|
1215
1098
|
*
|
|
@@ -1237,12 +1120,12 @@ function isInsecureRemote(url) {
|
|
|
1237
1120
|
const hostname = host.startsWith("[") ? host.slice(0, host.indexOf("]") + 1) : host.split(":")[0] ?? "";
|
|
1238
1121
|
return hostname !== "localhost" && hostname !== "127.0.0.1" && hostname !== "[::1]";
|
|
1239
1122
|
}
|
|
1240
|
-
/** Serialize a parked {@link
|
|
1241
|
-
function serializePending(
|
|
1123
|
+
/** Serialize a parked {@link PendingForm} into a {@link WireEvent}. */
|
|
1124
|
+
function serializePending(form) {
|
|
1242
1125
|
return {
|
|
1243
1126
|
event: "pending",
|
|
1244
|
-
data: JSON.stringify(
|
|
1245
|
-
id:
|
|
1127
|
+
data: JSON.stringify(form),
|
|
1128
|
+
id: form.id
|
|
1246
1129
|
};
|
|
1247
1130
|
}
|
|
1248
1131
|
/** Serialize a parked prompt's expiry into a {@link WireEvent} — event `'expire'`, `data` the JSON-stringified `{ id }` payload. */
|
|
@@ -1259,44 +1142,24 @@ function serializeShutdown() {
|
|
|
1259
1142
|
data: ""
|
|
1260
1143
|
};
|
|
1261
1144
|
}
|
|
1262
|
-
/** Narrow an unknown wire payload to an answer POST body — a non-empty `id` string plus a `value` key (of any shape) present. */
|
|
1263
|
-
function isAnswerPayload(value) {
|
|
1264
|
-
return isRecord(value) && isNonEmptyString(value.id) && "value" in value;
|
|
1265
|
-
}
|
|
1266
1145
|
//#endregion
|
|
1267
1146
|
//#region src/core/Prompt.ts
|
|
1268
1147
|
/**
|
|
1269
|
-
* The headless
|
|
1270
|
-
*
|
|
1271
|
-
* rejects on timeout / teardown). The tri-surface's headless arm: there is no terminal here, so a
|
|
1272
|
-
* transport forwards each `pending` event to whoever can answer, and {@link answer} resolves the
|
|
1273
|
-
* parked Promise — for environments where direct user access is unavailable (a headless server on
|
|
1274
|
-
* stdio, a browser with no TTY, a prompt issued on one machine answered on another).
|
|
1148
|
+
* The headless form broker. It parks live forms, exposes their serialized schemas, and applies
|
|
1149
|
+
* remote answers to the authoritative form.
|
|
1275
1150
|
*
|
|
1276
1151
|
* @remarks
|
|
1277
|
-
*
|
|
1278
|
-
*
|
|
1279
|
-
*
|
|
1280
|
-
* ({@link serializePromptOptions}) so a transport can forward them as-is.
|
|
1281
|
-
* - **Answer validates + type-checks.** {@link answer} runs the prompt's per-form gate: it
|
|
1282
|
-
* type-checks `value` to the form (string / boolean / string[]) AND, for the text forms, runs
|
|
1283
|
-
* the validator resolved from the original `validate` rules. A rejected answer returns `false`
|
|
1284
|
-
* and the prompt stays `pending`; an accepted answer resolves the Promise, emits `answer`, and
|
|
1285
|
-
* removes the prompt.
|
|
1286
|
-
* - **Timeout → expire → reject.** An unanswered prompt expires after `timeout` ms (via the
|
|
1287
|
-
* INJECTED timer): `expire` fires and the parked Promise rejects with a {@link TerminalError}
|
|
1288
|
-
* (`code: 'EXPIRE'`). {@link destroy} expires every still-pending prompt the same way.
|
|
1289
|
-
* - **Deterministic.** The timer is injectable ({@link import('./types.js').TimerHandler}); a test
|
|
1290
|
-
* supplies one that fires on demand, so expiry is driven without real time.
|
|
1152
|
+
* A parked record carries one call to `serializeForm`. A failed fill or submit leaves the record
|
|
1153
|
+
* parked for another answer. A successful submit settles the form once, emits `answer`, and removes
|
|
1154
|
+
* the record. Timeout and teardown abandon every unsettled form through its own `destroy` method.
|
|
1291
1155
|
*
|
|
1292
1156
|
* @example
|
|
1293
1157
|
* ```ts
|
|
1294
|
-
* const
|
|
1295
|
-
* prompt
|
|
1296
|
-
*
|
|
1297
|
-
*
|
|
1298
|
-
*
|
|
1299
|
-
* prompt.answer(id, 'Ada') // resolves the awaited input() above
|
|
1158
|
+
* const form = createForm({ fields: [{ control: 'text', name: 'name' }] })
|
|
1159
|
+
* const prompt = createPrompt()
|
|
1160
|
+
* const id = prompt.park(form)
|
|
1161
|
+
* prompt.answer(id, { name: 'Ada' })
|
|
1162
|
+
* await form.answer // { name: 'Ada' }
|
|
1300
1163
|
* ```
|
|
1301
1164
|
*/
|
|
1302
1165
|
var Prompt = class {
|
|
@@ -1322,211 +1185,164 @@ var Prompt = class {
|
|
|
1322
1185
|
return this.#parked.size;
|
|
1323
1186
|
}
|
|
1324
1187
|
pending(id) {
|
|
1325
|
-
if (id !== void 0) return this.#parked.get(id)?.
|
|
1326
|
-
const
|
|
1327
|
-
for (const parked of this.#parked.values())
|
|
1328
|
-
return
|
|
1329
|
-
}
|
|
1330
|
-
|
|
1331
|
-
|
|
1332
|
-
|
|
1333
|
-
|
|
1334
|
-
|
|
1335
|
-
|
|
1336
|
-
|
|
1337
|
-
|
|
1338
|
-
|
|
1339
|
-
|
|
1188
|
+
if (id !== void 0) return this.#parked.get(id)?.pending;
|
|
1189
|
+
const forms = [];
|
|
1190
|
+
for (const parked of this.#parked.values()) forms.push(parked.pending);
|
|
1191
|
+
return forms;
|
|
1192
|
+
}
|
|
1193
|
+
park(form, request) {
|
|
1194
|
+
if (this.#destroyed) {
|
|
1195
|
+
form.destroy();
|
|
1196
|
+
throw new TerminalError("EXPIRE", "The broker has been destroyed");
|
|
1197
|
+
}
|
|
1198
|
+
if (this.#cap !== void 0 && this.#parked.size >= this.#cap) {
|
|
1199
|
+
form.destroy();
|
|
1200
|
+
throw new TerminalError("LIMIT", `The parked-form cap (${String(this.#cap)}) was reached`, { cap: this.#cap });
|
|
1201
|
+
}
|
|
1202
|
+
const id = crypto.randomUUID();
|
|
1203
|
+
const pending = {
|
|
1204
|
+
id,
|
|
1205
|
+
schema: serializeForm(form.schema),
|
|
1206
|
+
status: "pending",
|
|
1207
|
+
time: Date.now(),
|
|
1208
|
+
...request?.from !== void 0 ? { from: request.from } : {},
|
|
1209
|
+
...request?.to !== void 0 ? { to: request.to } : {}
|
|
1340
1210
|
};
|
|
1341
|
-
|
|
1342
|
-
this.#
|
|
1343
|
-
|
|
1211
|
+
const cancel = this.#timer(() => this.#expire(id), this.#timeout);
|
|
1212
|
+
this.#parked.set(id, {
|
|
1213
|
+
form,
|
|
1214
|
+
pending,
|
|
1215
|
+
cancel
|
|
1216
|
+
});
|
|
1217
|
+
this.#emitter.emit("pending", pending);
|
|
1218
|
+
return id;
|
|
1219
|
+
}
|
|
1220
|
+
answer(id, values) {
|
|
1221
|
+
const outcome = attempt(() => this.#answer(id, values));
|
|
1222
|
+
if (outcome.success) return outcome.value;
|
|
1344
1223
|
return {
|
|
1345
|
-
success:
|
|
1346
|
-
|
|
1224
|
+
success: false,
|
|
1225
|
+
error: {
|
|
1226
|
+
reason: "rejected",
|
|
1227
|
+
errors: [{
|
|
1228
|
+
field: "form",
|
|
1229
|
+
message: "The form rejected the answer"
|
|
1230
|
+
}]
|
|
1231
|
+
}
|
|
1347
1232
|
};
|
|
1348
1233
|
}
|
|
1349
|
-
park(request) {
|
|
1350
|
-
const gate = this.#gate(request.form, request.options);
|
|
1351
|
-
return this.#park(request.form, request.options.message, request.options, gate, request.from, request.to);
|
|
1352
|
-
}
|
|
1353
|
-
input(options) {
|
|
1354
|
-
return this.#park("input", options.message, options, this.#gate("input", options)).value;
|
|
1355
|
-
}
|
|
1356
|
-
password(options) {
|
|
1357
|
-
return this.#park("password", options.message, options, this.#gate("password", options)).value;
|
|
1358
|
-
}
|
|
1359
|
-
confirm(options) {
|
|
1360
|
-
return this.#park("confirm", options.message, options, this.#gate("confirm", options)).value;
|
|
1361
|
-
}
|
|
1362
|
-
select(options) {
|
|
1363
|
-
return this.#park("select", options.message, options, this.#gate("select", options)).value;
|
|
1364
|
-
}
|
|
1365
|
-
checkbox(options) {
|
|
1366
|
-
return this.#park("checkbox", options.message, options, this.#gate("checkbox", options)).value;
|
|
1367
|
-
}
|
|
1368
|
-
editor(options) {
|
|
1369
|
-
return this.#park("editor", options.message, options, this.#gate("editor", options)).value;
|
|
1370
|
-
}
|
|
1371
1234
|
destroy() {
|
|
1372
1235
|
if (this.#destroyed) return;
|
|
1373
1236
|
this.#destroyed = true;
|
|
1374
1237
|
for (const id of [...this.#parked.keys()]) this.#expire(id);
|
|
1375
|
-
this.#parked.clear();
|
|
1376
1238
|
this.#emitter.destroy();
|
|
1377
1239
|
}
|
|
1378
|
-
#
|
|
1379
|
-
|
|
1380
|
-
|
|
1381
|
-
|
|
1382
|
-
|
|
1383
|
-
if (this.#cap !== void 0 && this.#parked.size >= this.#cap) return {
|
|
1384
|
-
id: crypto.randomUUID(),
|
|
1385
|
-
value: Promise.reject(new TerminalError("LIMIT", `parked-prompt cap (${this.#cap}) reached`, { cap: this.#cap }))
|
|
1386
|
-
};
|
|
1387
|
-
const id = crypto.randomUUID();
|
|
1388
|
-
const prompt = {
|
|
1389
|
-
id,
|
|
1390
|
-
form,
|
|
1391
|
-
message,
|
|
1392
|
-
options: serializePromptOptions(options),
|
|
1393
|
-
status: "pending",
|
|
1394
|
-
time: Date.now(),
|
|
1395
|
-
...from !== void 0 ? { from } : {},
|
|
1396
|
-
...to !== void 0 ? { to } : {}
|
|
1397
|
-
};
|
|
1398
|
-
return {
|
|
1399
|
-
id,
|
|
1400
|
-
value: new Promise((resolve, reject) => {
|
|
1401
|
-
const cancel = this.#timer(() => this.#expire(id), this.#timeout);
|
|
1402
|
-
const respond = this.#createRespond(id, gate, resolve);
|
|
1403
|
-
const expire = this.#createExpire(id, reject);
|
|
1404
|
-
this.#parked.set(id, {
|
|
1405
|
-
prompt,
|
|
1406
|
-
respond,
|
|
1407
|
-
expire,
|
|
1408
|
-
cancel
|
|
1409
|
-
});
|
|
1410
|
-
this.#emitter.emit("pending", prompt);
|
|
1411
|
-
})
|
|
1412
|
-
};
|
|
1413
|
-
}
|
|
1414
|
-
#createRespond(id, gate, resolve) {
|
|
1415
|
-
return (answer) => {
|
|
1416
|
-
const accepted = gate(answer);
|
|
1417
|
-
if (accepted === void 0) return void 0;
|
|
1418
|
-
const current = this.#parked.get(id);
|
|
1419
|
-
if (current !== void 0) this.#parked.set(id, {
|
|
1420
|
-
...current,
|
|
1421
|
-
prompt: {
|
|
1422
|
-
...current.prompt,
|
|
1423
|
-
status: "answered"
|
|
1424
|
-
}
|
|
1425
|
-
});
|
|
1426
|
-
resolve(accepted);
|
|
1427
|
-
return accepted;
|
|
1240
|
+
#answer(id, values) {
|
|
1241
|
+
const parked = this.#parked.get(id);
|
|
1242
|
+
if (parked === void 0 || parked.pending.status !== "pending") return {
|
|
1243
|
+
success: false,
|
|
1244
|
+
error: { reason: "unknown" }
|
|
1428
1245
|
};
|
|
1429
|
-
|
|
1430
|
-
|
|
1431
|
-
|
|
1432
|
-
|
|
1433
|
-
|
|
1434
|
-
|
|
1435
|
-
case "input":
|
|
1436
|
-
case "password":
|
|
1437
|
-
case "editor": {
|
|
1438
|
-
if (!this.#isTextOptions(form, options)) return () => void 0;
|
|
1439
|
-
const validator = resolveValidation(options.validate);
|
|
1440
|
-
return (value) => isString(value) && validator(value) === true ? value : void 0;
|
|
1246
|
+
const outcome = attempt(() => this.#submit(parked.form, values));
|
|
1247
|
+
if (!outcome.success) return {
|
|
1248
|
+
success: false,
|
|
1249
|
+
error: {
|
|
1250
|
+
reason: "rejected",
|
|
1251
|
+
errors: this.#errors(outcome.error, parked.form)
|
|
1441
1252
|
}
|
|
1442
|
-
|
|
1443
|
-
|
|
1444
|
-
|
|
1445
|
-
|
|
1446
|
-
|
|
1447
|
-
|
|
1448
|
-
return (value) => isString(value) && values.has(value) ? value : void 0;
|
|
1253
|
+
};
|
|
1254
|
+
if (!outcome.value.success) return {
|
|
1255
|
+
success: false,
|
|
1256
|
+
error: {
|
|
1257
|
+
reason: "rejected",
|
|
1258
|
+
errors: outcome.value.error
|
|
1449
1259
|
}
|
|
1450
|
-
|
|
1451
|
-
|
|
1452
|
-
|
|
1453
|
-
|
|
1454
|
-
|
|
1455
|
-
|
|
1456
|
-
|
|
1457
|
-
if (min !== void 0 && value.length < min) return void 0;
|
|
1458
|
-
if (max !== void 0 && value.length > max) return void 0;
|
|
1459
|
-
return value;
|
|
1460
|
-
};
|
|
1260
|
+
};
|
|
1261
|
+
parked.cancel();
|
|
1262
|
+
const answered = {
|
|
1263
|
+
...parked,
|
|
1264
|
+
pending: {
|
|
1265
|
+
...parked.pending,
|
|
1266
|
+
status: "answered"
|
|
1461
1267
|
}
|
|
1268
|
+
};
|
|
1269
|
+
this.#parked.set(id, answered);
|
|
1270
|
+
this.#emitter.emit("answer", id, outcome.value.value);
|
|
1271
|
+
this.#parked.delete(id);
|
|
1272
|
+
return outcome.value;
|
|
1273
|
+
}
|
|
1274
|
+
#submit(form, values) {
|
|
1275
|
+
for (const [field, value] of Object.entries(values)) {
|
|
1276
|
+
const outcome = attempt(() => form.fill(field, value));
|
|
1277
|
+
if (!outcome.success) return {
|
|
1278
|
+
success: false,
|
|
1279
|
+
error: this.#errors(outcome.error, form, field)
|
|
1280
|
+
};
|
|
1462
1281
|
}
|
|
1282
|
+
return form.submit();
|
|
1283
|
+
}
|
|
1284
|
+
#errors(error, form, field) {
|
|
1285
|
+
if (isFormError(error)) {
|
|
1286
|
+
if (isFieldError(error.context)) return [error.context];
|
|
1287
|
+
const named = error.context?.field;
|
|
1288
|
+
return [{
|
|
1289
|
+
field: typeof named === "string" ? named : this.#field(form, field),
|
|
1290
|
+
message: error.message
|
|
1291
|
+
}];
|
|
1292
|
+
}
|
|
1293
|
+
return [{
|
|
1294
|
+
field: this.#field(form, field),
|
|
1295
|
+
message: "The form rejected the answer"
|
|
1296
|
+
}];
|
|
1463
1297
|
}
|
|
1464
|
-
#
|
|
1465
|
-
return
|
|
1466
|
-
}
|
|
1467
|
-
#isConfirmOptions(form, _options) {
|
|
1468
|
-
return form === "confirm";
|
|
1469
|
-
}
|
|
1470
|
-
#isSelectOptions(form, _options) {
|
|
1471
|
-
return form === "select";
|
|
1472
|
-
}
|
|
1473
|
-
#isCheckboxOptions(form, _options) {
|
|
1474
|
-
return form === "checkbox";
|
|
1298
|
+
#field(form, field) {
|
|
1299
|
+
return field ?? form.schema.fields[0]?.name ?? form.schema.name ?? "form";
|
|
1475
1300
|
}
|
|
1476
1301
|
#expire(id) {
|
|
1477
1302
|
const parked = this.#parked.get(id);
|
|
1478
|
-
if (parked === void 0) return;
|
|
1303
|
+
if (parked === void 0 || parked.pending.status !== "pending") return;
|
|
1479
1304
|
parked.cancel();
|
|
1480
|
-
|
|
1305
|
+
const expired = {
|
|
1481
1306
|
...parked,
|
|
1482
|
-
|
|
1483
|
-
...parked.
|
|
1307
|
+
pending: {
|
|
1308
|
+
...parked.pending,
|
|
1484
1309
|
status: "expired"
|
|
1485
1310
|
}
|
|
1486
|
-
}
|
|
1311
|
+
};
|
|
1312
|
+
this.#parked.set(id, expired);
|
|
1313
|
+
parked.form.destroy();
|
|
1487
1314
|
this.#emitter.emit("expire", id);
|
|
1488
1315
|
this.#parked.delete(id);
|
|
1489
|
-
parked.expire();
|
|
1490
1316
|
}
|
|
1491
1317
|
};
|
|
1492
1318
|
//#endregion
|
|
1493
1319
|
//#region src/core/PromptClient.ts
|
|
1494
1320
|
/**
|
|
1495
|
-
* The SSE
|
|
1496
|
-
*
|
|
1497
|
-
* serialized pending prompts, dispatches EACH to a LOCAL {@link PromptFormInterface} terminal (so
|
|
1498
|
-
* a human at THIS machine answers a prompt parked elsewhere), and POSTs the answer back.
|
|
1499
|
-
* Universal — `fetch` + SSE are web-standard, so it runs in a browser or on a server; the
|
|
1500
|
-
* injected `fetch` / timer make it fully deterministic in tests.
|
|
1321
|
+
* The SSE form bridge. It ingests serialized forms from a remote broker, renders them through a
|
|
1322
|
+
* local terminal, and posts answers back without blocking the event stream.
|
|
1501
1323
|
*
|
|
1502
1324
|
* @remarks
|
|
1503
|
-
* - **Connect + reconnect.** {@link connect} opens the SSE stream
|
|
1504
|
-
*
|
|
1505
|
-
*
|
|
1506
|
-
*
|
|
1507
|
-
*
|
|
1508
|
-
*
|
|
1509
|
-
*
|
|
1510
|
-
*
|
|
1511
|
-
*
|
|
1512
|
-
*
|
|
1513
|
-
*
|
|
1514
|
-
*
|
|
1515
|
-
*
|
|
1516
|
-
* `shutdown` event calls {@link disconnect} (not {@link destroy}) — the client stops streaming
|
|
1517
|
-
* without auto-reconnect but stays reusable; a later {@link connect} recovers it.
|
|
1518
|
-
* - **Lean events (§13).** `connect` / `disconnect` / `expire` / `error` — errors are `unknown`.
|
|
1519
|
-
* `disconnect` fires exactly once per connected-to-disconnected transition, whether triggered by
|
|
1520
|
-
* the server ending the stream cleanly or by a deliberate {@link disconnect} / {@link destroy}.
|
|
1325
|
+
* - **Connect + reconnect.** {@link connect} opens the SSE stream and reconnects after a transport
|
|
1326
|
+
* drop with the injected backoff unless reconnect is disabled, the client was destroyed, or
|
|
1327
|
+
* {@link disconnect} deliberately stopped it.
|
|
1328
|
+
* - **Ingest + render.** Each `pending` envelope passes through `isPendingForm`, Form's `parseForm`,
|
|
1329
|
+
* and terminal's `sanitizeSchema`, then enters a serial render queue. The SSE reader never awaits
|
|
1330
|
+
* that queue, so `expire` and `shutdown` remain live while a person is answering.
|
|
1331
|
+
* - **Safe local form.** The rendering copy omits every wire `pattern`, because Form compiles a
|
|
1332
|
+
* pattern during local evaluation. The broker's parked form retains it and remains authoritative.
|
|
1333
|
+
* - **Refusal retry.** A structured `rejected` response seeds a new rendering form with the values
|
|
1334
|
+
* just submitted, applies every {@link FieldError} through `invalidate`, and asks again. No retry
|
|
1335
|
+
* counter truncates the loop; acceptance, expiry, and shutdown are its bounds.
|
|
1336
|
+
* - **Replay safety.** A replayed id is skipped while it is queued, rendering, or posting. Once an
|
|
1337
|
+
* attempt ends, a later delivery of that id may be rendered again.
|
|
1521
1338
|
*
|
|
1522
1339
|
* @example
|
|
1523
1340
|
* ```ts
|
|
1524
1341
|
* const client = createPromptClient({
|
|
1525
1342
|
* url: 'http://localhost:3001/prompts',
|
|
1526
|
-
* terminal: createTerminal(),
|
|
1527
|
-
* on: { connect: () => log('connected') },
|
|
1343
|
+
* terminal: createTerminal(),
|
|
1528
1344
|
* })
|
|
1529
|
-
* await client.connect()
|
|
1345
|
+
* await client.connect()
|
|
1530
1346
|
* ```
|
|
1531
1347
|
*/
|
|
1532
1348
|
var PromptClient = class {
|
|
@@ -1544,8 +1360,11 @@ var PromptClient = class {
|
|
|
1544
1360
|
#connecting = false;
|
|
1545
1361
|
#connected = false;
|
|
1546
1362
|
#destroyed = false;
|
|
1363
|
+
#draining = false;
|
|
1547
1364
|
#warnedInsecureToken = false;
|
|
1548
1365
|
#seen = /* @__PURE__ */ new Set();
|
|
1366
|
+
#queue = /* @__PURE__ */ new Map();
|
|
1367
|
+
#active;
|
|
1549
1368
|
constructor(options) {
|
|
1550
1369
|
this.url = options.url;
|
|
1551
1370
|
this.#terminal = options.terminal;
|
|
@@ -1566,8 +1385,7 @@ var PromptClient = class {
|
|
|
1566
1385
|
return this.#connected;
|
|
1567
1386
|
}
|
|
1568
1387
|
async connect() {
|
|
1569
|
-
if (this.#destroyed) return;
|
|
1570
|
-
if (this.#connecting) return;
|
|
1388
|
+
if (this.#destroyed || this.#connecting) return;
|
|
1571
1389
|
this.#connecting = true;
|
|
1572
1390
|
while (this.#connecting && !this.#destroyed) {
|
|
1573
1391
|
try {
|
|
@@ -1596,7 +1414,7 @@ var PromptClient = class {
|
|
|
1596
1414
|
if (this.#destroyed) return;
|
|
1597
1415
|
this.#destroyed = true;
|
|
1598
1416
|
this.disconnect();
|
|
1599
|
-
this.#
|
|
1417
|
+
this.#interrupt();
|
|
1600
1418
|
this.#emitter.destroy();
|
|
1601
1419
|
}
|
|
1602
1420
|
async #stream() {
|
|
@@ -1622,49 +1440,158 @@ var PromptClient = class {
|
|
|
1622
1440
|
for (;;) {
|
|
1623
1441
|
const { done, value } = await reader.read();
|
|
1624
1442
|
if (done) break;
|
|
1625
|
-
for (const event of parser.parse(decoder.decode(value, { stream: true })))
|
|
1443
|
+
for (const event of parser.parse(decoder.decode(value, { stream: true }))) {
|
|
1444
|
+
this.#handle(event);
|
|
1445
|
+
if (!this.#connecting) break;
|
|
1446
|
+
}
|
|
1447
|
+
if (!this.#connecting) break;
|
|
1626
1448
|
}
|
|
1627
1449
|
} finally {
|
|
1628
1450
|
reader.releaseLock();
|
|
1629
1451
|
}
|
|
1630
1452
|
this.#markDisconnected();
|
|
1631
1453
|
}
|
|
1632
|
-
|
|
1454
|
+
#handle(event) {
|
|
1633
1455
|
if (event.event === SSE_EVENTS.pending) {
|
|
1634
|
-
const parsed =
|
|
1635
|
-
if (
|
|
1456
|
+
const parsed = parseJSON(event.data);
|
|
1457
|
+
if (!isPendingForm(parsed) || this.#seen.has(parsed.id)) return;
|
|
1458
|
+
const schema = parseForm(parsed.schema);
|
|
1459
|
+
if (schema === void 0) {
|
|
1460
|
+
this.#emitter.emit("error", /* @__PURE__ */ new Error(`broker sent an invalid form schema for ${parsed.id}`));
|
|
1461
|
+
return;
|
|
1462
|
+
}
|
|
1463
|
+
this.#seen.add(parsed.id);
|
|
1464
|
+
this.#queue.set(parsed.id, sanitizeSchema(schema));
|
|
1465
|
+
this.#drain();
|
|
1636
1466
|
return;
|
|
1637
1467
|
}
|
|
1638
1468
|
if (event.event === SSE_EVENTS.expire) {
|
|
1639
|
-
const parsed =
|
|
1640
|
-
if (isRecord(parsed) && isString(parsed.id))
|
|
1641
|
-
this.#seen.delete(parsed.id);
|
|
1642
|
-
this.#emitter.emit("expire", parsed.id);
|
|
1643
|
-
}
|
|
1469
|
+
const parsed = parseJSON(event.data);
|
|
1470
|
+
if (isRecord(parsed) && isString(parsed.id)) this.#expire(parsed.id);
|
|
1644
1471
|
return;
|
|
1645
1472
|
}
|
|
1646
|
-
if (event.event === SSE_EVENTS.shutdown)
|
|
1473
|
+
if (event.event === SSE_EVENTS.shutdown) {
|
|
1474
|
+
this.disconnect();
|
|
1475
|
+
this.#interrupt();
|
|
1476
|
+
}
|
|
1647
1477
|
}
|
|
1648
|
-
async #
|
|
1649
|
-
this.#
|
|
1478
|
+
async #drain() {
|
|
1479
|
+
if (this.#draining) return;
|
|
1480
|
+
this.#draining = true;
|
|
1650
1481
|
try {
|
|
1651
|
-
|
|
1652
|
-
|
|
1653
|
-
|
|
1654
|
-
|
|
1482
|
+
while (!this.#destroyed) {
|
|
1483
|
+
let queued;
|
|
1484
|
+
for (const entry of this.#queue) {
|
|
1485
|
+
queued = entry;
|
|
1486
|
+
break;
|
|
1487
|
+
}
|
|
1488
|
+
if (queued === void 0) return;
|
|
1489
|
+
const [id, schema] = queued;
|
|
1490
|
+
this.#queue.delete(id);
|
|
1491
|
+
try {
|
|
1492
|
+
await this.#render(id, schema);
|
|
1493
|
+
} catch (error) {
|
|
1494
|
+
const active = this.#active;
|
|
1495
|
+
if (active?.id !== id || !active.stopped) this.#emitter.emit("error", error);
|
|
1496
|
+
} finally {
|
|
1497
|
+
const active = this.#active;
|
|
1498
|
+
if (active?.id === id) {
|
|
1499
|
+
active.form.destroy();
|
|
1500
|
+
this.#active = void 0;
|
|
1501
|
+
}
|
|
1502
|
+
if (!this.#queue.has(id)) this.#seen.delete(id);
|
|
1503
|
+
}
|
|
1504
|
+
}
|
|
1655
1505
|
} finally {
|
|
1656
|
-
this.#
|
|
1506
|
+
this.#draining = false;
|
|
1657
1507
|
}
|
|
1658
1508
|
}
|
|
1659
|
-
async #
|
|
1660
|
-
|
|
1509
|
+
async #render(id, schema) {
|
|
1510
|
+
let values;
|
|
1511
|
+
let errors = [];
|
|
1512
|
+
while (this.#seen.has(id) && !this.#destroyed) {
|
|
1513
|
+
const form = this.#createRenderingForm(schema, values);
|
|
1514
|
+
this.#active = {
|
|
1515
|
+
id,
|
|
1516
|
+
form,
|
|
1517
|
+
stopped: false
|
|
1518
|
+
};
|
|
1519
|
+
for (const error of errors) form.invalidate(error.field, sanitizeDisplayText(error.message));
|
|
1520
|
+
const submitted = await this.#terminal.ask(form);
|
|
1521
|
+
const active = this.#active;
|
|
1522
|
+
if (active?.id !== id || active.stopped) return;
|
|
1523
|
+
const rejected = await this.#post(id, submitted);
|
|
1524
|
+
const posted = this.#active;
|
|
1525
|
+
if (posted?.id !== id || posted.stopped || rejected === void 0) return;
|
|
1526
|
+
values = submitted;
|
|
1527
|
+
errors = rejected;
|
|
1528
|
+
form.destroy();
|
|
1529
|
+
this.#active = void 0;
|
|
1530
|
+
}
|
|
1531
|
+
}
|
|
1532
|
+
#createRenderingForm(schema, values) {
|
|
1533
|
+
const fields = schema.fields.map((field) => {
|
|
1534
|
+
if (field.rule?.pattern === void 0) return field;
|
|
1535
|
+
const { pattern: _pattern, ...rule } = field.rule;
|
|
1536
|
+
return {
|
|
1537
|
+
...field,
|
|
1538
|
+
rule
|
|
1539
|
+
};
|
|
1540
|
+
});
|
|
1541
|
+
const form = createForm({
|
|
1542
|
+
...schema,
|
|
1543
|
+
fields
|
|
1544
|
+
}, values === void 0 ? void 0 : { values });
|
|
1545
|
+
form.answer.catch(() => void 0);
|
|
1546
|
+
return form;
|
|
1547
|
+
}
|
|
1548
|
+
async #post(id, values) {
|
|
1549
|
+
const response = await this.#fetch(this.url, {
|
|
1661
1550
|
method: "POST",
|
|
1662
1551
|
headers: this.#headers({ "Content-Type": "application/json" }),
|
|
1663
1552
|
body: JSON.stringify({
|
|
1664
1553
|
id,
|
|
1665
|
-
|
|
1554
|
+
values
|
|
1666
1555
|
})
|
|
1667
|
-
})
|
|
1556
|
+
});
|
|
1557
|
+
if (!response.ok) this.#emitter.emit("error", /* @__PURE__ */ new Error(`broker rejected answer ${id}`));
|
|
1558
|
+
const parsed = parseJSON(await response.text());
|
|
1559
|
+
if (!isRecord(parsed) || !isBoolean(parsed.success)) {
|
|
1560
|
+
this.#emitter.emit("error", /* @__PURE__ */ new Error(`broker returned an invalid answer result for ${id}`));
|
|
1561
|
+
return;
|
|
1562
|
+
}
|
|
1563
|
+
if (parsed.success) return void 0;
|
|
1564
|
+
const error = parsed.error;
|
|
1565
|
+
if (!isRecord(error) || !isString(error.reason)) {
|
|
1566
|
+
this.#emitter.emit("error", /* @__PURE__ */ new Error(`broker returned an invalid answer error for ${id}`));
|
|
1567
|
+
return;
|
|
1568
|
+
}
|
|
1569
|
+
if (error.reason === "unknown") return void 0;
|
|
1570
|
+
if (error.reason === "rejected" && arrayOf(isFieldError)(error.errors)) return error.errors;
|
|
1571
|
+
this.#emitter.emit("error", /* @__PURE__ */ new Error(`broker returned an invalid answer refusal for ${id}`));
|
|
1572
|
+
}
|
|
1573
|
+
#expire(id) {
|
|
1574
|
+
const active = this.#active;
|
|
1575
|
+
if (active?.id === id) {
|
|
1576
|
+
this.#active = {
|
|
1577
|
+
...active,
|
|
1578
|
+
stopped: true
|
|
1579
|
+
};
|
|
1580
|
+
this.#seen.delete(id);
|
|
1581
|
+
active.form.destroy();
|
|
1582
|
+
} else if (this.#queue.delete(id)) this.#seen.delete(id);
|
|
1583
|
+
this.#emitter.emit("expire", id);
|
|
1584
|
+
}
|
|
1585
|
+
#interrupt() {
|
|
1586
|
+
this.#queue.clear();
|
|
1587
|
+
this.#seen.clear();
|
|
1588
|
+
const active = this.#active;
|
|
1589
|
+
if (active === void 0) return;
|
|
1590
|
+
this.#active = {
|
|
1591
|
+
...active,
|
|
1592
|
+
stopped: true
|
|
1593
|
+
};
|
|
1594
|
+
active.form.destroy();
|
|
1668
1595
|
}
|
|
1669
1596
|
#markDisconnected() {
|
|
1670
1597
|
if (!this.#connected) return;
|
|
@@ -1697,8 +1624,8 @@ var PromptClient = class {
|
|
|
1697
1624
|
//#region src/core/TerminalManager.ts
|
|
1698
1625
|
/**
|
|
1699
1626
|
* The multi-endpoint terminal MANAGER — a registry of named {@link PromptInterface} brokers (one
|
|
1700
|
-
* per endpoint), so several parties can `ask`
|
|
1701
|
-
* attribution edge on every parked
|
|
1627
|
+
* per endpoint), so several parties can `ask` forms of each other by NAME with a `from` → `to`
|
|
1628
|
+
* attribution edge on every parked form, and a transitive DEADLOCK check across all in-flight
|
|
1702
1629
|
* asks.
|
|
1703
1630
|
*
|
|
1704
1631
|
* @remarks
|
|
@@ -1709,22 +1636,23 @@ var PromptClient = class {
|
|
|
1709
1636
|
* - **`ask`.** The target must already be mounted via {@link add} — `ask` never auto-adds it;
|
|
1710
1637
|
* rejects `TARGET` for an unknown `to` (listing the known names). Rejects `DEADLOCK` when parking
|
|
1711
1638
|
* `from → to` would close a cycle over the CURRENT in-flight edge set (walked transitively);
|
|
1712
|
-
* otherwise parks through the target's broker and
|
|
1713
|
-
*
|
|
1639
|
+
* otherwise parks the caller's live form through the target's broker and returns that form's own
|
|
1640
|
+
* `answer` promise. Edge cleanup never alters the value or rejection the caller observes.
|
|
1714
1641
|
* - **Durable open / save.** `open(name)` restores an EMPTY broker from the `store` (parked
|
|
1715
1642
|
* Promises are process-bound and never resurrected); `save(name)` persists the endpoint's
|
|
1716
1643
|
* configured `timeout`.
|
|
1717
|
-
* - **Removal.** `remove` drops one endpoint
|
|
1718
|
-
*
|
|
1719
|
-
* idempotent.
|
|
1644
|
+
* - **Removal.** `remove` drops one endpoint, a batch (§9.2, array overload FIRST), or every
|
|
1645
|
+
* endpoint when called without an argument. It destroys each broker, which expires every form
|
|
1646
|
+
* still parked on it. `destroy` is idempotent.
|
|
1720
1647
|
*
|
|
1721
1648
|
* @example
|
|
1722
1649
|
* ```ts
|
|
1650
|
+
* const form = createForm({ fields: [{ control: 'text', name: 'name' }] })
|
|
1723
1651
|
* const manager = new TerminalManager()
|
|
1724
1652
|
* manager.add('agent')
|
|
1725
|
-
* const
|
|
1726
|
-
* manager.answer('agent', manager.pending('agent')[0].id, 'Ada')
|
|
1727
|
-
* await
|
|
1653
|
+
* const answer = manager.ask('user', 'agent', form)
|
|
1654
|
+
* manager.answer('agent', manager.pending('agent')[0].id, { name: 'Ada' })
|
|
1655
|
+
* await answer // { name: 'Ada' }
|
|
1728
1656
|
* ```
|
|
1729
1657
|
*/
|
|
1730
1658
|
var TerminalManager = class {
|
|
@@ -1768,6 +1696,8 @@ var TerminalManager = class {
|
|
|
1768
1696
|
const timer = options?.timer ?? this.#timer;
|
|
1769
1697
|
const cap = options?.cap ?? this.#cap;
|
|
1770
1698
|
const broker = createPrompt({
|
|
1699
|
+
...options?.on !== void 0 ? { on: options.on } : {},
|
|
1700
|
+
...options?.error !== void 0 ? { error: options.error } : {},
|
|
1771
1701
|
...timeout !== void 0 ? { timeout } : {},
|
|
1772
1702
|
...timer !== void 0 ? { timer } : {},
|
|
1773
1703
|
...cap !== void 0 ? { cap } : {}
|
|
@@ -1781,11 +1711,11 @@ var TerminalManager = class {
|
|
|
1781
1711
|
broker.emitter.on("answer", listeners.answer);
|
|
1782
1712
|
broker.emitter.on("expire", listeners.expire);
|
|
1783
1713
|
this.#terminals.set(name, broker);
|
|
1784
|
-
this.#config.set(name, options
|
|
1714
|
+
this.#config.set(name, { ...options });
|
|
1785
1715
|
this.#listeners.set(name, listeners);
|
|
1786
1716
|
return broker;
|
|
1787
1717
|
}
|
|
1788
|
-
ask(from, to, form
|
|
1718
|
+
ask(from, to, form) {
|
|
1789
1719
|
const broker = this.#terminals.get(to);
|
|
1790
1720
|
if (broker === void 0) {
|
|
1791
1721
|
const known = this.terminals();
|
|
@@ -1800,21 +1730,18 @@ var TerminalManager = class {
|
|
|
1800
1730
|
to,
|
|
1801
1731
|
path: cycle
|
|
1802
1732
|
}));
|
|
1803
|
-
const
|
|
1804
|
-
form,
|
|
1805
|
-
options,
|
|
1733
|
+
const id = broker.park(form, {
|
|
1806
1734
|
from,
|
|
1807
1735
|
to
|
|
1808
1736
|
});
|
|
1809
|
-
if (broker.pending(
|
|
1810
|
-
this.#edges.set(
|
|
1737
|
+
if (broker.pending(id) !== void 0) {
|
|
1738
|
+
this.#edges.set(id, {
|
|
1811
1739
|
from,
|
|
1812
1740
|
to
|
|
1813
1741
|
});
|
|
1814
|
-
|
|
1815
|
-
ticket.value.then(clear, clear);
|
|
1742
|
+
form.answer.then(this.#createEdgeClear(id), this.#createEdgeClear(id));
|
|
1816
1743
|
}
|
|
1817
|
-
return
|
|
1744
|
+
return form.answer;
|
|
1818
1745
|
}
|
|
1819
1746
|
pending(to) {
|
|
1820
1747
|
if (to !== void 0) {
|
|
@@ -1825,13 +1752,15 @@ var TerminalManager = class {
|
|
|
1825
1752
|
for (const broker of this.#terminals.values()) result.push(...broker.pending());
|
|
1826
1753
|
return result;
|
|
1827
1754
|
}
|
|
1828
|
-
answer(to, id,
|
|
1755
|
+
answer(to, id, values) {
|
|
1829
1756
|
const broker = this.#terminals.get(to);
|
|
1830
1757
|
if (broker === void 0) return {
|
|
1831
1758
|
success: false,
|
|
1832
|
-
error: "terminal"
|
|
1759
|
+
error: { reason: "terminal" }
|
|
1833
1760
|
};
|
|
1834
|
-
|
|
1761
|
+
const result = broker.answer(id, values);
|
|
1762
|
+
if (result.success) this.#edges.delete(id);
|
|
1763
|
+
return result;
|
|
1835
1764
|
}
|
|
1836
1765
|
async open(name) {
|
|
1837
1766
|
if (this.#destroyed) throw new TerminalError("DESTROYED", "manager destroyed");
|
|
@@ -1855,6 +1784,10 @@ var TerminalManager = class {
|
|
|
1855
1784
|
return true;
|
|
1856
1785
|
}
|
|
1857
1786
|
remove(names) {
|
|
1787
|
+
if (names === void 0) {
|
|
1788
|
+
for (const name of [...this.#terminals.keys()]) this.#removeOne(name);
|
|
1789
|
+
return;
|
|
1790
|
+
}
|
|
1858
1791
|
if (isArray(names)) {
|
|
1859
1792
|
let removed = false;
|
|
1860
1793
|
for (const name of names) if (this.#removeOne(name)) removed = true;
|
|
@@ -1862,23 +1795,27 @@ var TerminalManager = class {
|
|
|
1862
1795
|
}
|
|
1863
1796
|
return this.#removeOne(names);
|
|
1864
1797
|
}
|
|
1865
|
-
clear() {
|
|
1866
|
-
for (const name of [...this.#terminals.keys()]) this.#removeOne(name);
|
|
1867
|
-
}
|
|
1868
1798
|
destroy() {
|
|
1869
1799
|
if (this.#destroyed) return;
|
|
1870
1800
|
this.#destroyed = true;
|
|
1871
|
-
this.
|
|
1801
|
+
this.remove();
|
|
1802
|
+
this.#edges.clear();
|
|
1872
1803
|
this.#emitter.destroy();
|
|
1873
1804
|
}
|
|
1874
1805
|
#createPendingListener() {
|
|
1875
|
-
return (
|
|
1806
|
+
return (form) => this.#emitter.emit("pending", form);
|
|
1876
1807
|
}
|
|
1877
1808
|
#createAnswerListener(name) {
|
|
1878
|
-
return (id,
|
|
1809
|
+
return (id, values) => {
|
|
1810
|
+
this.#edges.delete(id);
|
|
1811
|
+
this.#emitter.emit("answer", name, id, values);
|
|
1812
|
+
};
|
|
1879
1813
|
}
|
|
1880
1814
|
#createExpireListener(name) {
|
|
1881
|
-
return (id) =>
|
|
1815
|
+
return (id) => {
|
|
1816
|
+
this.#edges.delete(id);
|
|
1817
|
+
this.#emitter.emit("expire", name, id);
|
|
1818
|
+
};
|
|
1882
1819
|
}
|
|
1883
1820
|
#createEdgeClear(id) {
|
|
1884
1821
|
return () => {
|
|
@@ -1898,8 +1835,12 @@ var TerminalManager = class {
|
|
|
1898
1835
|
this.#terminals.delete(name);
|
|
1899
1836
|
this.#config.delete(name);
|
|
1900
1837
|
this.#listeners.delete(name);
|
|
1838
|
+
this.#clearEdges(name);
|
|
1901
1839
|
return true;
|
|
1902
1840
|
}
|
|
1841
|
+
#clearEdges(to) {
|
|
1842
|
+
for (const [id, edge] of this.#edges) if (edge.to === to) this.#edges.delete(id);
|
|
1843
|
+
}
|
|
1903
1844
|
#findCycle(from, to) {
|
|
1904
1845
|
if (from === to) return [from, to];
|
|
1905
1846
|
const visited = /* @__PURE__ */ new Set([to]);
|
|
@@ -2047,30 +1988,26 @@ var DatabaseTerminalStore = class {
|
|
|
2047
1988
|
//#endregion
|
|
2048
1989
|
//#region src/core/factories.ts
|
|
2049
1990
|
/**
|
|
2050
|
-
* Create the headless
|
|
2051
|
-
*
|
|
2052
|
-
* timeout). The tri-surface's headless arm: subscribe `emitter.on('pending', …)` to forward each
|
|
2053
|
-
* prompt to whoever can answer (an SSE transport, a remote terminal), then route the answer back
|
|
2054
|
-
* through `answer(id, value)`.
|
|
1991
|
+
* Create the headless {@link PromptInterface} broker. It parks live forms and applies remote
|
|
1992
|
+
* answers to the authoritative instances.
|
|
2055
1993
|
*
|
|
2056
1994
|
* @param options - See {@link PromptOptions}
|
|
2057
1995
|
* @returns A {@link PromptInterface}
|
|
2058
1996
|
*
|
|
2059
1997
|
* @remarks
|
|
2060
|
-
*
|
|
2061
|
-
*
|
|
2062
|
-
*
|
|
2063
|
-
* `options.timeout` (default {@link import('./constants.js').DEFAULT_PROMPT_TIMEOUT_MS}) and its
|
|
2064
|
-
* Promise rejects with a {@link import('./errors.js').TerminalError}; inject `options.timer` to
|
|
2065
|
-
* drive expiry without real time.
|
|
1998
|
+
* The caller awaits the parked form's own `answer`. Timeout or teardown destroys the form, so that
|
|
1999
|
+
* promise rejects with the Form package's `ABANDONED` error. Inject `options.timer` to drive
|
|
2000
|
+
* expiry without real time.
|
|
2066
2001
|
*
|
|
2067
2002
|
* @example
|
|
2068
2003
|
* ```ts
|
|
2069
2004
|
* import { createPrompt } from '@src/core'
|
|
2005
|
+
* import { createForm } from '@orkestrel/form'
|
|
2070
2006
|
*
|
|
2071
2007
|
* const prompt = createPrompt()
|
|
2072
|
-
*
|
|
2073
|
-
* const
|
|
2008
|
+
* const form = createForm({ fields: [{ control: 'text', name: 'name' }] })
|
|
2009
|
+
* const id = prompt.park(form)
|
|
2010
|
+
* prompt.answer(id, { name: 'Ada' })
|
|
2074
2011
|
* ```
|
|
2075
2012
|
*/
|
|
2076
2013
|
function createPrompt(options) {
|
|
@@ -2078,9 +2015,8 @@ function createPrompt(options) {
|
|
|
2078
2015
|
}
|
|
2079
2016
|
/**
|
|
2080
2017
|
* Create the SSE prompt {@link PromptClientInterface} BRIDGE — it connects to a remote broker's SSE
|
|
2081
|
-
* endpoint, dispatches each received
|
|
2082
|
-
*
|
|
2083
|
-
* back. Universal — `fetch` / SSE are web-standard.
|
|
2018
|
+
* endpoint, dispatches each received form to a local {@link import('./types.js').TerminalInterface},
|
|
2019
|
+
* and POSTs the answer back. Universal — `fetch` / SSE are web-standard.
|
|
2084
2020
|
*
|
|
2085
2021
|
* @param options - See {@link PromptClientOptions} (`url` + `terminal` required)
|
|
2086
2022
|
* @returns A {@link PromptClientInterface}
|
|
@@ -2104,12 +2040,12 @@ function createPromptClient(options) {
|
|
|
2104
2040
|
return new PromptClient(options);
|
|
2105
2041
|
}
|
|
2106
2042
|
/**
|
|
2107
|
-
* Create the multi-endpoint {@link
|
|
2043
|
+
* Create the multi-endpoint {@link TerminalManager} — a named registry of
|
|
2108
2044
|
* {@link PromptInterface} brokers so several parties can `ask` prompts of each other by name,
|
|
2109
2045
|
* with a transitive DEADLOCK check across every in-flight ask.
|
|
2110
2046
|
*
|
|
2111
2047
|
* @param options - See {@link TerminalManagerOptions}
|
|
2112
|
-
* @returns A {@link
|
|
2048
|
+
* @returns A {@link TerminalManager}
|
|
2113
2049
|
*
|
|
2114
2050
|
* @example
|
|
2115
2051
|
* ```ts
|
|
@@ -2163,6 +2099,6 @@ function createDatabaseTerminalStore(driver = createMemoryDriver()) {
|
|
|
2163
2099
|
}).table("terminals"));
|
|
2164
2100
|
}
|
|
2165
2101
|
//#endregion
|
|
2166
|
-
export { ACCEPT_EVENT_STREAM,
|
|
2102
|
+
export { ACCEPT_EVENT_STREAM, BACKSPACE, CONTROL_NAMES, CTRL_A, CTRL_C, CTRL_D, CTRL_E, CTRL_U, DEFAULT_MASK, DEFAULT_PROMPT_THEME, DEFAULT_PROMPT_TIMEOUT_MS, DEFAULT_RECONNECT_DELAY_MS, DELETE, DatabaseTerminalStore, ESCAPE, HEADER_TOKEN, KEY_CSI, KEY_SS3, MemoryTerminalStore, NEWLINE, PROMPT_ICONS, PROMPT_ROLES, Prompt, PromptClient, RETURN, SEQUENCE_NAMES, SPACE, SSE_BUFFER_LIMIT, SSE_EVENTS, TAB, TerminalError, TerminalManager, checkboxReduce, checkboxView, confirmReduce, confirmView, createCheckboxState, createConfirmState, createDatabaseTerminalStore, createEditorState, createInputState, createMemoryTerminalStore, createPasswordState, createPrompt, createPromptClient, createPromptTheme, createSelectState, createTerminalManager, defaultTimer, editLine, editorReduce, editorView, errorLine, globalFetch, hintedHeader, inputReduce, inputView, isAbortError, isInsecureRemote, isPendingForm, isPendingFormStatus, isPrintable, isTerminalError, isTerminalSnapshot, isWireEvent, parseKey, passwordReduce, passwordView, promptHeader, sanitizeDisplayText, sanitizeSchema, sanitizeThemeIcons, selectReduce, selectView, serializeExpire, serializePending, serializeShutdown, submitHeader, toggleIndex };
|
|
2167
2103
|
|
|
2168
2104
|
//# sourceMappingURL=index.js.map
|