@orkestrel/terminal 0.0.6 → 0.0.8

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,6 +1,6 @@
1
1
  Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
2
- let _orkestrel_contract = require("@orkestrel/contract");
3
2
  let _orkestrel_console = require("@orkestrel/console");
3
+ let _orkestrel_contract = require("@orkestrel/contract");
4
4
  let _orkestrel_emitter = require("@orkestrel/emitter");
5
5
  let _orkestrel_sse = require("@orkestrel/sse");
6
6
  let _orkestrel_database = require("@orkestrel/database");
@@ -130,9 +130,10 @@ var CONTROL_NAMES = Object.freeze({
130
130
  });
131
131
  /** The default mask glyph a {@link import('./types.js').PasswordState} renders each input character as — `*`. */
132
132
  var DEFAULT_MASK = "*";
133
- /** Matches an email address — a non-trivial `local@domain.tld` shape. The `email` rule tests against this. */
134
- var EMAIL_PATTERN = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
135
- /** Matches an HTTP(S) URL. The `url` rule tests against this. */
133
+ /**
134
+ * Matches an HTTP(S) URL. The `url` rule tests against this. Kept locally because it is
135
+ * narrower than `FORMAT_PATTERNS.uri`, which accepts other URI schemes.
136
+ */
136
137
  var URL_PATTERN = /^https?:\/\/.+/;
137
138
  /** Matches a numeric value (integer or decimal, optional sign). The `numeric` rule tests against this. */
138
139
  var NUMERIC_PATTERN = /^-?\d+(\.\d+)?$/;
@@ -159,11 +160,8 @@ var RULE_MESSAGES = Object.freeze({
159
160
  invalid: "Invalid input"
160
161
  });
161
162
  /**
162
- * The prompt-view icon glyphs the reducers render the prompt line and choice rows with. PLAIN
163
- * glyphs — color is applied by the {@link import('./types.js').PromptState}'s
164
- * {@link import('@orkestrel/console').StylerInterface} at render time, never baked into the
165
- * constant (AGENTS — styling orthogonal to data; the rework fixes the terminal's ANSI-in-the-icon).
166
- * Frozen.
163
+ * The six default glyphs {@link DEFAULT_PROMPT_THEME} assembles its `icons` from. Read only when
164
+ * the default theme is assembled; a view reads its resolved theme and never this constant. Frozen.
167
165
  *
168
166
  * @remarks
169
167
  * - `question` — the leading mark on a prompt's message line.
@@ -179,6 +177,79 @@ var PROMPT_ICONS = Object.freeze({
179
177
  checked: "☑",
180
178
  unchecked: "☐"
181
179
  });
180
+ /**
181
+ * Every {@link import('./types.js').PromptRole}, in one frozen list — the role axis's source of
182
+ * truth. {@link import('./helpers.js').createPromptTheme} walks it to merge a partial theme, and
183
+ * a consumer building a complete role map reads it rather than retyping every name.
184
+ */
185
+ var PROMPT_ROLES = Object.freeze([
186
+ "question",
187
+ "pointer",
188
+ "message",
189
+ "content",
190
+ "success",
191
+ "error",
192
+ "selected",
193
+ "focus",
194
+ "hint",
195
+ "muted",
196
+ "description"
197
+ ]);
198
+ /**
199
+ * The {@link import('./types.js').PromptTheme} every prompt renders with unless its options supply
200
+ * another — the glyph set assembled from {@link PROMPT_ICONS} plus the console
201
+ * {@link import('@orkestrel/console').STATUS_ICONS} `success` / `error` marks, and the console
202
+ * {@link import('@orkestrel/console').Style} each role is painted with. Deeply frozen through the
203
+ * console module's own {@link import('@orkestrel/console').freezeStyle}; the baseline
204
+ * {@link import('./helpers.js').createPromptTheme} merges a partial theme over.
205
+ *
206
+ * @remarks
207
+ * The default roles reproduce the views' historical coloring exactly: `question` / `pointer` cyan,
208
+ * `message` / `focus` bold, `success` / `selected` green, `error` red, `hint` / `muted` /
209
+ * `description` dim, and `content` the EMPTY style — an empty style renders bare text, so primary
210
+ * content keeps the bytes it had before it became themeable.
211
+ * Two roles sharing a style today are still two roles — re-mapping one leaves the other alone.
212
+ */
213
+ var DEFAULT_PROMPT_THEME = Object.freeze({
214
+ icons: Object.freeze({
215
+ question: PROMPT_ICONS.question,
216
+ pointer: PROMPT_ICONS.pointer,
217
+ dot: PROMPT_ICONS.dot,
218
+ selected: PROMPT_ICONS.selected,
219
+ checked: PROMPT_ICONS.checked,
220
+ unchecked: PROMPT_ICONS.unchecked,
221
+ success: _orkestrel_console.STATUS_ICONS.success,
222
+ error: _orkestrel_console.STATUS_ICONS.error
223
+ }),
224
+ roles: Object.freeze({
225
+ question: (0, _orkestrel_console.freezeStyle)({
226
+ foreground: "cyan",
227
+ attributes: []
228
+ }),
229
+ pointer: (0, _orkestrel_console.freezeStyle)({
230
+ foreground: "cyan",
231
+ attributes: []
232
+ }),
233
+ message: (0, _orkestrel_console.freezeStyle)({ attributes: ["bold"] }),
234
+ content: (0, _orkestrel_console.freezeStyle)({ attributes: [] }),
235
+ success: (0, _orkestrel_console.freezeStyle)({
236
+ foreground: "green",
237
+ attributes: []
238
+ }),
239
+ error: (0, _orkestrel_console.freezeStyle)({
240
+ foreground: "red",
241
+ attributes: []
242
+ }),
243
+ selected: (0, _orkestrel_console.freezeStyle)({
244
+ foreground: "green",
245
+ attributes: []
246
+ }),
247
+ focus: (0, _orkestrel_console.freezeStyle)({ attributes: ["bold"] }),
248
+ hint: (0, _orkestrel_console.freezeStyle)({ attributes: ["dim"] }),
249
+ muted: (0, _orkestrel_console.freezeStyle)({ attributes: ["dim"] }),
250
+ description: (0, _orkestrel_console.freezeStyle)({ attributes: ["dim"] })
251
+ })
252
+ });
182
253
  /** How long (ms) the {@link import('./types.js').PromptInterface} broker parks an unanswered prompt before it expires — 5 minutes. */
183
254
  var DEFAULT_PROMPT_TIMEOUT_MS = 3e5;
184
255
  /** How long (ms) the {@link import('./types.js').PromptClientInterface} waits before each reconnect attempt — 2 seconds. */
@@ -272,6 +343,63 @@ var isPendingPrompt = (0, _orkestrel_contract.recordOf)({
272
343
  from: _orkestrel_contract.isString,
273
344
  to: _orkestrel_contract.isString
274
345
  }, ["from", "to"]);
346
+ /**
347
+ * Narrow an unknown value to a console {@link Style} — the value every
348
+ * {@link import('./types.js').PromptRole} carries, built from the console module's own
349
+ * {@link import('@orkestrel/console').COLORS} + {@link import('@orkestrel/console').ATTRIBUTES} so
350
+ * the style vocabulary has ONE source.
351
+ *
352
+ * @remarks
353
+ * The record is CLOSED and matches the `Style` contract exactly: `foreground` and `background` are
354
+ * optional and each must name a color (`default` included, as the console `Color` union has it),
355
+ * `attributes` is REQUIRED and must be an array over the attribute set, and any other key rejects
356
+ * the whole value.
357
+ */
358
+ var isStyle = (0, _orkestrel_contract.recordOf)({
359
+ foreground: (0, _orkestrel_contract.literalOf)(..._orkestrel_console.COLORS, "default"),
360
+ background: (0, _orkestrel_contract.literalOf)(..._orkestrel_console.COLORS, "default"),
361
+ attributes: (0, _orkestrel_contract.arrayOf)((0, _orkestrel_contract.literalOf)(..._orkestrel_console.ATTRIBUTES))
362
+ }, ["foreground", "background"]);
363
+ /**
364
+ * Narrow an unknown wire value to a {@link PromptThemeOptions} — the §14 guard a remote prompt's
365
+ * `theme` option passes before it reaches a local prompt.
366
+ *
367
+ * @remarks
368
+ * The shape is CLOSED at every level: an unknown icon slot, an unknown role, a non-string glyph, or
369
+ * a role whose value is not a {@link isStyle}-shaped console style rejects the whole theme, so a
370
+ * hostile payload degrades to the default theme rather than partially applying. Glyphs still carry
371
+ * arbitrary text at this point — {@link import('./helpers.js').sanitizeThemeIcons} strips their
372
+ * control bytes on dispatch.
373
+ *
374
+ * The icons and roles shapes are checked against their complete union-keyed guard records. A slot
375
+ * added to either union without an entry here is a compile error rather than a wire theme that is
376
+ * silently refused.
377
+ */
378
+ var isPromptThemeOptions = (0, _orkestrel_contract.recordOf)({
379
+ icons: (0, _orkestrel_contract.recordOf)({
380
+ question: _orkestrel_contract.isString,
381
+ pointer: _orkestrel_contract.isString,
382
+ dot: _orkestrel_contract.isString,
383
+ selected: _orkestrel_contract.isString,
384
+ checked: _orkestrel_contract.isString,
385
+ unchecked: _orkestrel_contract.isString,
386
+ success: _orkestrel_contract.isString,
387
+ error: _orkestrel_contract.isString
388
+ }, true),
389
+ roles: (0, _orkestrel_contract.recordOf)({
390
+ question: isStyle,
391
+ pointer: isStyle,
392
+ message: isStyle,
393
+ content: isStyle,
394
+ success: isStyle,
395
+ error: isStyle,
396
+ selected: isStyle,
397
+ focus: isStyle,
398
+ hint: isStyle,
399
+ description: isStyle,
400
+ muted: isStyle
401
+ }, true)
402
+ }, true);
275
403
  /** Narrow an unknown value to a {@link TerminalSnapshot}. */
276
404
  var isTerminalSnapshot = (0, _orkestrel_contract.recordOf)({
277
405
  id: _orkestrel_contract.isNonEmptyString,
@@ -396,7 +524,7 @@ function evaluateRule(rule, check, input) {
396
524
  }
397
525
  break;
398
526
  case "email":
399
- if (check === true && !EMAIL_PATTERN.test(input)) return RULE_MESSAGES.email;
527
+ if (check === true && !_orkestrel_contract.FORMAT_PATTERNS.email.test(input)) return RULE_MESSAGES.email;
400
528
  break;
401
529
  case "url":
402
530
  if (check === true && !URL_PATTERN.test(input)) return RULE_MESSAGES.url;
@@ -501,33 +629,116 @@ function normalizeCheckboxChoice(choice) {
501
629
  value: choice
502
630
  } : choice;
503
631
  }
504
- /** The styled prompt-message header (`? message`) — the leading line every active prompt view shares. */
505
- function promptHeader(styler, message) {
506
- return `${styler.cyan(PROMPT_ICONS.question)} ${styler.bold(message)}`;
632
+ /**
633
+ * Build a complete {@link PromptTheme} by merging a partial one over
634
+ * {@link DEFAULT_PROMPT_THEME}, leaf by leaf — each supplied icon replaces that glyph, each
635
+ * supplied role replaces that {@link Style}, and everything else keeps its default. Each supplied
636
+ * style is snapshotted through the console module's own
637
+ * {@link import('@orkestrel/console').freezeStyle}, so the result is deeply frozen and a caller
638
+ * mutating its own attribute list afterwards cannot reach into a built theme.
639
+ *
640
+ * @param options - The partial theme to merge, or `undefined` for the defaults
641
+ * @returns The resolved, deeply frozen theme every prompt state carries
642
+ *
643
+ * @example
644
+ * ```ts
645
+ * createPromptTheme() // the defaults
646
+ * createPromptTheme({
647
+ * icons: { pointer: '=>' },
648
+ * roles: { message: { foreground: 'magenta', attributes: ['bold'] } },
649
+ * })
650
+ * ```
651
+ */
652
+ function createPromptTheme(options) {
653
+ const icons = {
654
+ ...DEFAULT_PROMPT_THEME.icons,
655
+ ...options?.icons
656
+ };
657
+ const roles = { ...DEFAULT_PROMPT_THEME.roles };
658
+ for (const role of PROMPT_ROLES) {
659
+ const style = options?.roles?.[role];
660
+ if (style !== void 0) roles[role] = (0, _orkestrel_console.freezeStyle)(style);
661
+ }
662
+ return Object.freeze({
663
+ icons: Object.freeze(icons),
664
+ roles: Object.freeze(roles)
665
+ });
507
666
  }
508
- /** The styled submit line (`✔ message`) — the committed header an interactive prompt shows once resolved. */
509
- function submitHeader(styler, message) {
510
- return `${styler.green(_orkestrel_console.STATUS_ICONS.success)} ${styler.bold(message)}`;
667
+ /**
668
+ * Sanitize text for one single-line display slot. Composes console's {@link stripControls} pass
669
+ * with removal of tab, line feed, and carriage return, so no C0 control or DEL survives.
670
+ *
671
+ * @param text - The text to sanitize for a glyph or hint slot
672
+ * @returns The text with every C0 control character and DEL removed
673
+ *
674
+ * @example
675
+ * ```ts
676
+ * sanitizeDisplayText('Q\rOVERWRITE\nNEXT\tX') // 'QOVERWRITENEXTX'
677
+ * ```
678
+ */
679
+ function sanitizeDisplayText(text) {
680
+ return (0, _orkestrel_console.stripControls)(text).replaceAll(" ", "").replaceAll("\n", "").replaceAll("\r", "");
681
+ }
682
+ /**
683
+ * Sanitize every glyph a wire-supplied {@link PromptThemeOptions} carries for a single-line display
684
+ * slot. Only the icons need it: a role is guard-narrowed to a console {@link Style}, whose colors
685
+ * and attributes are fixed name sets, so no role can carry a byte a terminal would act on.
686
+ *
687
+ * @param theme - The narrowed theme options a remote prompt supplied
688
+ * @returns The same theme with every supplied glyph sanitized for a single-line display slot
689
+ */
690
+ function sanitizeThemeIcons(theme) {
691
+ const icons = theme.icons;
692
+ if (icons === void 0) return theme;
693
+ const sanitized = {};
694
+ for (const [icon, glyph] of Object.entries(icons)) sanitized[icon] = sanitizeDisplayText(glyph);
695
+ return {
696
+ ...theme,
697
+ icons: sanitized
698
+ };
699
+ }
700
+ /** The styled prompt-message header (`? message`) — the leading line every active prompt view shares, themed by the `question` + `message` roles. */
701
+ function promptHeader(styler, theme, message) {
702
+ return `${styler.render(theme.roles.question, theme.icons.question)} ${styler.render(theme.roles.message, message)}`;
703
+ }
704
+ /**
705
+ * Render a prompt header followed by a key hint painted with the `hint` role, or the header alone
706
+ * when no hint is supplied.
707
+ *
708
+ * @param styler - The console styler that renders each role
709
+ * @param theme - The resolved prompt theme
710
+ * @param message - The prompt's question text
711
+ * @param hint - The optional key hint to append
712
+ * @returns The rendered header with the optional hint
713
+ */
714
+ function hintedHeader(styler, theme, message, hint) {
715
+ const head = promptHeader(styler, theme, message);
716
+ return hint === void 0 ? head : `${head} ${styler.render(theme.roles.hint, hint)}`;
717
+ }
718
+ /** The styled submit line (`✔ message`) — the committed header an interactive prompt shows once resolved, themed by the `success` + `message` roles. */
719
+ function submitHeader(styler, theme, message) {
720
+ return `${styler.render(theme.roles.success, theme.icons.success)} ${styler.render(theme.roles.message, message)}`;
511
721
  }
512
- /** The styled error line (`✖ message`) — appended beneath a prompt view when the last submit failed validation. */
513
- function errorLine(styler, message) {
514
- return `${styler.red(_orkestrel_console.STATUS_ICONS.error)} ${styler.red(message)}`;
722
+ /** The styled error line (`✖ message`) — appended beneath a prompt view when the last submit failed validation, themed by the `error` role. */
723
+ function errorLine(styler, theme, message) {
724
+ return `${styler.render(theme.roles.error, theme.icons.error)} ${styler.render(theme.roles.error, message)}`;
515
725
  }
516
- /** Build the initial {@link InputState} from {@link InputOptions} — resolving the validator + styler, seeding an empty value. */
726
+ /** Build the initial {@link InputState} from {@link InputOptions} — resolving the validator + styler + theme, seeding an empty value. */
517
727
  function createInputState(options) {
518
728
  return {
519
729
  message: options.message,
520
730
  default: options.default ?? "",
521
731
  validator: resolveValidation(options.validate),
522
732
  styler: options.styler ?? (0, _orkestrel_console.createStyler)(),
733
+ theme: createPromptTheme(options.theme),
523
734
  value: ""
524
735
  };
525
736
  }
526
- /** Render an {@link InputState} as a styled view — the header, the typed value (or dimmed default hint), and any error. */
737
+ /** Render an {@link InputState} as a styled view — the header, the typed value painted by the `content` role (or the default painted as a hint), and any error. */
527
738
  function inputView(state) {
528
- const shown = state.value.length > 0 ? state.value : state.styler.dim(state.default);
529
- const head = `${promptHeader(state.styler, state.message)} ${state.styler.cyan(PROMPT_ICONS.pointer)} ${shown}`;
530
- return state.error === void 0 ? head : `${head}\n${errorLine(state.styler, state.error)}`;
739
+ const shown = state.value.length > 0 ? state.styler.render(state.theme.roles.content, state.value) : state.styler.render(state.theme.roles.hint, state.default);
740
+ const head = `${promptHeader(state.styler, state.theme, state.message)} ${state.styler.render(state.theme.roles.pointer, state.theme.icons.pointer)} ${shown}`;
741
+ return state.error === void 0 ? head : `${head}\n${errorLine(state.styler, state.theme, state.error)}`;
531
742
  }
532
743
  /**
533
744
  * Advance an input prompt by one {@link KeyEvent} — the pure `(state, key) → PromptStep<string>`
@@ -561,7 +772,7 @@ function inputReduce(state, key) {
561
772
  ...clear,
562
773
  value: answer
563
774
  },
564
- view: `${submitHeader(state.styler, state.message)} ${state.styler.dim(answer)}`,
775
+ view: `${submitHeader(state.styler, state.theme, state.message)} ${state.styler.render(state.theme.roles.hint, answer)}`,
565
776
  status: "submit",
566
777
  value: answer
567
778
  };
@@ -582,21 +793,22 @@ function inputReduce(state, key) {
582
793
  status: "active"
583
794
  };
584
795
  }
585
- /** Build the initial {@link PasswordState} from {@link PasswordOptions} — resolving the validator + styler + mask. */
796
+ /** Build the initial {@link PasswordState} from {@link PasswordOptions} — resolving the validator + styler + theme + mask. */
586
797
  function createPasswordState(options) {
587
798
  return {
588
799
  message: options.message,
589
800
  mask: options.mask ?? "*",
590
801
  validator: resolveValidation(options.validate),
591
802
  styler: options.styler ?? (0, _orkestrel_console.createStyler)(),
803
+ theme: createPromptTheme(options.theme),
592
804
  value: ""
593
805
  };
594
806
  }
595
- /** Render a {@link PasswordState} as a styled view — the header, the value masked to `mask` repeated, and any error. */
807
+ /** Render a {@link PasswordState} as a styled view — the header, the value masked to `mask` repeated and painted by the `content` role, and any error. */
596
808
  function passwordView(state) {
597
- const masked = state.mask.repeat(state.value.length);
598
- const head = `${promptHeader(state.styler, state.message)} ${state.styler.cyan(PROMPT_ICONS.pointer)} ${masked}`;
599
- return state.error === void 0 ? head : `${head}\n${errorLine(state.styler, state.error)}`;
809
+ const masked = state.styler.render(state.theme.roles.content, state.mask.repeat(state.value.length));
810
+ const head = `${promptHeader(state.styler, state.theme, state.message)} ${state.styler.render(state.theme.roles.pointer, state.theme.icons.pointer)} ${masked}`;
811
+ return state.error === void 0 ? head : `${head}\n${errorLine(state.styler, state.theme, state.error)}`;
600
812
  }
601
813
  /**
602
814
  * Advance a password prompt by one {@link KeyEvent} — the pure `(state, key) → PromptStep<string>`
@@ -626,7 +838,7 @@ function passwordReduce(state, key) {
626
838
  }
627
839
  return {
628
840
  state: clear,
629
- view: `${submitHeader(state.styler, state.message)} ${state.styler.dim(state.mask.repeat(state.value.length))}`,
841
+ view: `${submitHeader(state.styler, state.theme, state.message)} ${state.styler.render(state.theme.roles.hint, state.mask.repeat(state.value.length))}`,
630
842
  status: "submit",
631
843
  value: state.value
632
844
  };
@@ -647,18 +859,26 @@ function passwordReduce(state, key) {
647
859
  status: "active"
648
860
  };
649
861
  }
650
- /** Build the initial {@link ConfirmState} from {@link ConfirmOptions} — defaulting the answer to `false`. */
862
+ /** Build the initial {@link ConfirmState} from {@link ConfirmOptions} — defaulting the answer to `false`, resolving the styler + theme, carrying any hint override. */
651
863
  function createConfirmState(options) {
652
864
  return {
653
865
  message: options.message,
654
866
  default: options.default ?? false,
655
- styler: options.styler ?? (0, _orkestrel_console.createStyler)()
867
+ ...options.hint !== void 0 ? { hint: options.hint } : {},
868
+ styler: options.styler ?? (0, _orkestrel_console.createStyler)(),
869
+ theme: createPromptTheme(options.theme)
656
870
  };
657
871
  }
658
- /** Render a {@link ConfirmState} as a styled view — the header plus a `(Y/n)` hint with the default letter emphasized. */
872
+ /**
873
+ * Render a {@link ConfirmState} as a styled view — the header plus the `(Y/n)` group with the
874
+ * default letter painted by the `selected` role. A supplied `hint` replaces that whole group,
875
+ * parentheses included, painted by the `hint` role.
876
+ */
659
877
  function confirmView(state) {
660
- const hint = state.default ? `${state.styler.green("Y")}${state.styler.dim("/n")}` : `${state.styler.dim("y/")}${state.styler.green("N")}`;
661
- return `${promptHeader(state.styler, state.message)} ${state.styler.dim("(")}${hint}${state.styler.dim(")")}`;
878
+ if (state.hint !== void 0) return hintedHeader(state.styler, state.theme, state.message, state.hint);
879
+ const head = promptHeader(state.styler, state.theme, state.message);
880
+ const answer = state.default ? `${state.styler.render(state.theme.roles.selected, "Y")}${state.styler.render(state.theme.roles.hint, "/n")}` : `${state.styler.render(state.theme.roles.hint, "y/")}${state.styler.render(state.theme.roles.selected, "N")}`;
881
+ return `${head} ${state.styler.render(state.theme.roles.hint, "(")}${answer}${state.styler.render(state.theme.roles.hint, ")")}`;
662
882
  }
663
883
  /**
664
884
  * Advance a confirm prompt by one {@link KeyEvent} — the pure `(state, key) → PromptStep<boolean>`
@@ -683,29 +903,31 @@ function confirmReduce(state, key) {
683
903
  };
684
904
  return {
685
905
  state,
686
- view: `${submitHeader(state.styler, state.message)} ${state.styler.dim(answer ? "yes" : "no")}`,
906
+ view: `${submitHeader(state.styler, state.theme, state.message)} ${state.styler.render(state.theme.roles.hint, answer ? "yes" : "no")}`,
687
907
  status: "submit",
688
908
  value: answer
689
909
  };
690
910
  }
691
- /** Build the initial {@link SelectState} from {@link SelectOptions} — normalizing choices and pre-focusing the default. */
911
+ /** Build the initial {@link SelectState} from {@link SelectOptions} — normalizing choices, resolving the styler + theme, carrying any hint, and pre-focusing the default. */
692
912
  function createSelectState(options) {
693
913
  const choices = options.choices.map(normalizeChoice);
694
914
  const index = choices.findIndex((choice) => choice.value === options.default);
695
915
  return {
696
916
  message: options.message,
697
917
  choices,
918
+ ...options.hint !== void 0 ? { hint: options.hint } : {},
698
919
  styler: options.styler ?? (0, _orkestrel_console.createStyler)(),
920
+ theme: createPromptTheme(options.theme),
699
921
  focused: index >= 0 ? index : 0
700
922
  };
701
923
  }
702
- /** Render a {@link SelectState} as a MULTI-LINE styled view — the header followed by one row per choice, the focused row marked. */
924
+ /** Render a {@link SelectState} as a MULTI-LINE styled view — the header (plus any hint) followed by one row per choice, the focused row marked. */
703
925
  function selectView(state) {
704
926
  const lines = state.choices.map((choice, index) => {
705
927
  const active = index === state.focused;
706
- return `${active ? state.styler.cyan(PROMPT_ICONS.pointer) : " "} ${active ? state.styler.green(PROMPT_ICONS.selected) : state.styler.dim(PROMPT_ICONS.dot)} ${active ? state.styler.bold(choice.name) : choice.name}${choice.description === void 0 ? "" : ` ${state.styler.dim(choice.description)}`}`;
928
+ return `${active ? state.styler.render(state.theme.roles.pointer, state.theme.icons.pointer) : " "} ${active ? state.styler.render(state.theme.roles.selected, state.theme.icons.selected) : state.styler.render(state.theme.roles.muted, state.theme.icons.dot)} ${active ? state.styler.render(state.theme.roles.focus, choice.name) : state.styler.render(state.theme.roles.content, choice.name)}${choice.description === void 0 ? "" : ` ${state.styler.render(state.theme.roles.description, choice.description)}`}`;
707
929
  });
708
- return [promptHeader(state.styler, state.message), ...lines].join("\n");
930
+ return [hintedHeader(state.styler, state.theme, state.message, state.hint), ...lines].join("\n");
709
931
  }
710
932
  /**
711
933
  * Advance a select prompt by one {@link KeyEvent} — the pure `(state, key) → PromptStep<string>`
@@ -752,7 +974,7 @@ function selectReduce(state, key) {
752
974
  const value = choice?.value ?? "";
753
975
  return {
754
976
  state,
755
- view: `${submitHeader(state.styler, state.message)} ${state.styler.dim(choice?.name ?? "")}`,
977
+ view: `${submitHeader(state.styler, state.theme, state.message)} ${state.styler.render(state.theme.roles.hint, choice?.name ?? "")}`,
756
978
  status: "submit",
757
979
  value
758
980
  };
@@ -763,7 +985,7 @@ function selectReduce(state, key) {
763
985
  status: "active"
764
986
  };
765
987
  }
766
- /** Build the initial {@link CheckboxState} from {@link CheckboxOptions} — normalizing choices, seeding the checked set, carrying min/max. */
988
+ /** Build the initial {@link CheckboxState} from {@link CheckboxOptions} — normalizing choices, resolving the styler + theme, carrying any hint, seeding the checked set, carrying min/max. */
767
989
  function createCheckboxState(options) {
768
990
  const choices = options.choices.map(normalizeCheckboxChoice);
769
991
  const checked = choices.reduce((indices, choice, index) => {
@@ -773,27 +995,29 @@ function createCheckboxState(options) {
773
995
  return {
774
996
  message: options.message,
775
997
  choices,
998
+ ...options.hint !== void 0 ? { hint: options.hint } : {},
776
999
  styler: options.styler ?? (0, _orkestrel_console.createStyler)(),
1000
+ theme: createPromptTheme(options.theme),
777
1001
  focused: 0,
778
1002
  checked,
779
1003
  ...options.min !== void 0 ? { min: options.min } : {},
780
1004
  ...options.max !== void 0 ? { max: options.max } : {}
781
1005
  };
782
1006
  }
783
- /** Render a {@link CheckboxState} as a MULTI-LINE styled view — the header, one box per choice (focused + checked marked), a count, and any error. */
1007
+ /** Render a {@link CheckboxState} as a MULTI-LINE styled view — the header (plus any hint), one box per choice (focused + checked marked), a count, and any error. */
784
1008
  function checkboxView(state) {
785
1009
  const lines = state.choices.map((choice, index) => {
786
1010
  const active = index === state.focused;
787
1011
  const ticked = state.checked.includes(index);
788
- return `${active ? state.styler.cyan(PROMPT_ICONS.pointer) : " "} ${ticked ? state.styler.green(PROMPT_ICONS.checked) : state.styler.dim(PROMPT_ICONS.unchecked)} ${active ? state.styler.bold(choice.name) : choice.name}${choice.description === void 0 ? "" : ` ${state.styler.dim(choice.description)}`}`;
1012
+ return `${active ? state.styler.render(state.theme.roles.pointer, state.theme.icons.pointer) : " "} ${ticked ? state.styler.render(state.theme.roles.selected, state.theme.icons.checked) : state.styler.render(state.theme.roles.muted, state.theme.icons.unchecked)} ${active ? state.styler.render(state.theme.roles.focus, choice.name) : state.styler.render(state.theme.roles.content, choice.name)}${choice.description === void 0 ? "" : ` ${state.styler.render(state.theme.roles.description, choice.description)}`}`;
789
1013
  });
790
- const summary = state.styler.dim(`${state.checked.length} selected`);
1014
+ const summary = state.styler.render(state.theme.roles.hint, `${state.checked.length} selected`);
791
1015
  const body = [
792
- promptHeader(state.styler, state.message),
1016
+ hintedHeader(state.styler, state.theme, state.message, state.hint),
793
1017
  ...lines,
794
1018
  summary
795
1019
  ].join("\n");
796
- return state.error === void 0 ? body : `${body}\n${errorLine(state.styler, state.error)}`;
1020
+ return state.error === void 0 ? body : `${body}\n${errorLine(state.styler, state.theme, state.error)}`;
797
1021
  }
798
1022
  /**
799
1023
  * Advance a checkbox prompt by one {@link KeyEvent} — the pure
@@ -862,7 +1086,7 @@ function checkboxReduce(state, key) {
862
1086
  const summary = ordered.map((index) => state.choices[index]?.name).filter((name) => name !== void 0).join(", ");
863
1087
  return {
864
1088
  state: clear,
865
- view: `${submitHeader(state.styler, state.message)} ${state.styler.dim(summary)}`,
1089
+ view: `${submitHeader(state.styler, state.theme, state.message)} ${state.styler.render(state.theme.roles.hint, summary)}`,
866
1090
  status: "submit",
867
1091
  value: values
868
1092
  };
@@ -882,21 +1106,29 @@ function gateSelection(count, min, max) {
882
1106
  if (min !== void 0 && count < min) return `Select at least ${String(min)} option${min === 1 ? "" : "s"}`;
883
1107
  if (max !== void 0 && count > max) return `Select no more than ${String(max)} option${max === 1 ? "" : "s"}`;
884
1108
  }
885
- /** Build the initial {@link EditorState} from {@link EditorOptions} — resolving the validator + styler, seeding empty lines. */
1109
+ /** Build the initial {@link EditorState} from {@link EditorOptions} — resolving the validator + styler + theme, carrying any hint override, seeding empty lines. */
886
1110
  function createEditorState(options) {
887
1111
  return {
888
1112
  message: options.message,
889
1113
  default: options.default ?? "",
1114
+ ...options.hint !== void 0 ? { hint: options.hint } : {},
890
1115
  validator: resolveValidation(options.validate),
891
1116
  styler: options.styler ?? (0, _orkestrel_console.createStyler)(),
1117
+ theme: createPromptTheme(options.theme),
892
1118
  lines: [],
893
1119
  current: ""
894
1120
  };
895
1121
  }
896
- /** Render an {@link EditorState} as a MULTI-LINE styled view — the header (with a Ctrl+D hint), the committed lines, the in-progress line, and any error. */
1122
+ /**
1123
+ * Render an {@link EditorState} as a MULTI-LINE styled view — the header with its finish hint, the
1124
+ * committed and in-progress lines painted by the `content` role, and any error. A supplied `hint`
1125
+ * replaces `(Ctrl+D to finish)` in full; the finish key stays ctrl-d.
1126
+ */
897
1127
  function editorView(state) {
898
- const view = [`${promptHeader(state.styler, state.message)} ${state.styler.dim("(Ctrl+D to finish)")}`, ...[...state.lines, `${state.styler.cyan(PROMPT_ICONS.pointer)} ${state.current}`]].join("\n");
899
- return state.error === void 0 ? view : `${view}\n${errorLine(state.styler, state.error)}`;
1128
+ const head = hintedHeader(state.styler, state.theme, state.message, state.hint ?? "(Ctrl+D to finish)");
1129
+ const pointer = state.styler.render(state.theme.roles.pointer, state.theme.icons.pointer);
1130
+ const view = [head, ...[...state.lines.map((line) => state.styler.render(state.theme.roles.content, line)), `${pointer} ${state.styler.render(state.theme.roles.content, state.current)}`]].join("\n");
1131
+ return state.error === void 0 ? view : `${view}\n${errorLine(state.styler, state.theme, state.error)}`;
900
1132
  }
901
1133
  /**
902
1134
  * Advance an editor prompt by one {@link KeyEvent} — the pure `(state, key) → PromptStep<string>`
@@ -930,7 +1162,7 @@ function editorReduce(state, key) {
930
1162
  }
931
1163
  return {
932
1164
  state: clear,
933
- view: `${submitHeader(state.styler, state.message)} ${state.styler.dim(`${String(lines.length)} line${lines.length === 1 ? "" : "s"}`)}`,
1165
+ view: `${submitHeader(state.styler, state.theme, state.message)} ${state.styler.render(state.theme.roles.hint, `${String(lines.length)} line${lines.length === 1 ? "" : "s"}`)}`,
934
1166
  status: "submit",
935
1167
  value: answer
936
1168
  };
@@ -980,7 +1212,8 @@ function editLine(value, key) {
980
1212
  * serializes over SSE. Drops everything that cannot cross the wire (a `styler`, and any
981
1213
  * function-valued `validate` rule), while KEEPING the declarative data a remote client needs to
982
1214
  * reconstruct the prompt: the {@link ValidationRules} as data, plus `message` / `choices` /
983
- * `default` / `mask` / `min` / `max`. This is WHY T-a's validation is rules-as-data: a `Validator`
1215
+ * `default` / `mask` / `min` / `max` / `hint` / `theme`. This is WHY T-a's validation is
1216
+ * rules-as-data, and why a theme is data too: a `Validator`
984
1217
  * FUNCTION can't be serialized, but its declarative rules can — the client rebuilds the validator
985
1218
  * from them via {@link resolveValidation}.
986
1219
  *
@@ -1114,6 +1347,13 @@ function sanitizeChoiceLabels(choices) {
1114
1347
  * §14-narrowed, never an `as`; the validator rebuilt from rules via {@link reconstructValidationRules}),
1115
1348
  * then calls the matching prompt form and returns its resolved value.
1116
1349
  *
1350
+ * @remarks
1351
+ * `theme` and `hint` are plain data, so they cross the wire with the rest of the options bag and
1352
+ * are reconstructed here for every form that accepts them: the theme is narrowed by
1353
+ * {@link import('./validators.js').isPromptThemeOptions} (an off-shape theme is dropped, leaving
1354
+ * the defaults), then every glyph and hint is passed through {@link sanitizeDisplayText}. Choice
1355
+ * labels use the stream-safe {@link sanitizeChoiceLabels} treatment instead.
1356
+ *
1117
1357
  * @param terminal - The local {@link PromptFormInterface} to drive
1118
1358
  * @param pending - The decoded pending prompt to dispatch
1119
1359
  * @returns The prompt's resolved value (a `string` / `boolean` / `readonly string[]` per form)
@@ -1122,13 +1362,18 @@ function dispatchPendingPrompt(terminal, pending) {
1122
1362
  const options = pending.options;
1123
1363
  const validate = reconstructValidationRules(options.validate);
1124
1364
  const message = (0, _orkestrel_console.stripControls)(pending.message);
1365
+ const supplied = resolveOption(options, "theme", isPromptThemeOptions);
1366
+ const theme = supplied === void 0 ? void 0 : sanitizeThemeIcons(supplied);
1367
+ const raw = resolveOption(options, "hint", _orkestrel_contract.isString);
1368
+ const hint = raw === void 0 ? void 0 : sanitizeDisplayText(raw);
1125
1369
  switch (pending.form) {
1126
1370
  case "input": {
1127
1371
  const value = resolveOption(options, "default", _orkestrel_contract.isString);
1128
1372
  return terminal.input({
1129
1373
  message,
1130
1374
  ...value !== void 0 ? { default: (0, _orkestrel_console.stripControls)(value) } : {},
1131
- ...validate !== void 0 ? { validate } : {}
1375
+ ...validate !== void 0 ? { validate } : {},
1376
+ ...theme !== void 0 ? { theme } : {}
1132
1377
  });
1133
1378
  }
1134
1379
  case "password": {
@@ -1136,14 +1381,17 @@ function dispatchPendingPrompt(terminal, pending) {
1136
1381
  return terminal.password({
1137
1382
  message,
1138
1383
  ...value !== void 0 ? { mask: (0, _orkestrel_console.stripControls)(value) } : {},
1139
- ...validate !== void 0 ? { validate } : {}
1384
+ ...validate !== void 0 ? { validate } : {},
1385
+ ...theme !== void 0 ? { theme } : {}
1140
1386
  });
1141
1387
  }
1142
1388
  case "confirm": {
1143
1389
  const value = resolveOption(options, "default", _orkestrel_contract.isBoolean);
1144
1390
  return terminal.confirm({
1145
1391
  message,
1146
- ...value !== void 0 ? { default: value } : {}
1392
+ ...value !== void 0 ? { default: value } : {},
1393
+ ...hint !== void 0 ? { hint } : {},
1394
+ ...theme !== void 0 ? { theme } : {}
1147
1395
  });
1148
1396
  }
1149
1397
  case "select": {
@@ -1151,7 +1399,9 @@ function dispatchPendingPrompt(terminal, pending) {
1151
1399
  return terminal.select({
1152
1400
  message,
1153
1401
  choices: sanitizeChoiceLabels(resolveChoices(options, isPromptChoice)),
1154
- ...value !== void 0 ? { default: (0, _orkestrel_console.stripControls)(value) } : {}
1402
+ ...value !== void 0 ? { default: (0, _orkestrel_console.stripControls)(value) } : {},
1403
+ ...hint !== void 0 ? { hint } : {},
1404
+ ...theme !== void 0 ? { theme } : {}
1155
1405
  });
1156
1406
  }
1157
1407
  case "checkbox": {
@@ -1161,7 +1411,9 @@ function dispatchPendingPrompt(terminal, pending) {
1161
1411
  message,
1162
1412
  choices: sanitizeChoiceLabels(resolveChoices(options, isCheckboxChoice)),
1163
1413
  ...min !== void 0 ? { min } : {},
1164
- ...max !== void 0 ? { max } : {}
1414
+ ...max !== void 0 ? { max } : {},
1415
+ ...hint !== void 0 ? { hint } : {},
1416
+ ...theme !== void 0 ? { theme } : {}
1165
1417
  });
1166
1418
  }
1167
1419
  case "editor": {
@@ -1169,7 +1421,9 @@ function dispatchPendingPrompt(terminal, pending) {
1169
1421
  return terminal.editor({
1170
1422
  message,
1171
1423
  ...value !== void 0 ? { default: (0, _orkestrel_console.stripControls)(value) } : {},
1172
- ...validate !== void 0 ? { validate } : {}
1424
+ ...validate !== void 0 ? { validate } : {},
1425
+ ...hint !== void 0 ? { hint } : {},
1426
+ ...theme !== void 0 ? { theme } : {}
1173
1427
  });
1174
1428
  }
1175
1429
  }
@@ -1198,19 +1452,6 @@ function isAbortError(error) {
1198
1452
  return (error instanceof DOMException || error instanceof Error) && error.name === "AbortError";
1199
1453
  }
1200
1454
  /**
1201
- * Parse a JSON wire string TOTAL — a malformed / empty payload yields `undefined` (the caller's
1202
- * guard then rejects it), never a throw. The {@link import('./PromptClient.js').PromptClient}
1203
- * decodes every SSE `data` field through this before §14-narrowing it.
1204
- */
1205
- function parseWireJSON(text) {
1206
- if (text.length === 0) return void 0;
1207
- try {
1208
- return JSON.parse(text);
1209
- } catch {
1210
- return;
1211
- }
1212
- }
1213
- /**
1214
1455
  * Whether `url` is an INSECURE remote endpoint — a plain `http://` URL whose host is NOT a
1215
1456
  * loopback address. Pure string parsing (no `URL` global), so it stays total on malformed input.
1216
1457
  *
@@ -1281,9 +1522,9 @@ function isAnswerPayload(value) {
1281
1522
  * ({@link serializePromptOptions}) so a transport can forward them as-is.
1282
1523
  * - **Answer validates + type-checks.** {@link answer} runs the prompt's per-form gate: it
1283
1524
  * type-checks `value` to the form (string / boolean / string[]) AND, for the text forms, runs
1284
- * the validator resolved from the original `validate` rules. A rejected answer returns `false`
1285
- * and the prompt stays `pending`; an accepted answer resolves the Promise, emits `answer`, and
1286
- * removes the prompt.
1525
+ * the validator resolved from the original `validate` rules. A rejected answer returns a failure
1526
+ * `Result` with error `'rejected'` and the prompt stays `pending`; an accepted answer resolves the
1527
+ * Promise, emits `answer`, and removes the prompt.
1287
1528
  * - **Timeout → expire → reject.** An unanswered prompt expires after `timeout` ms (via the
1288
1529
  * INJECTED timer): `expire` fires and the parked Promise rejects with a {@link TerminalError}
1289
1530
  * (`code: 'EXPIRE'`). {@link destroy} expires every still-pending prompt the same way.
@@ -1632,12 +1873,12 @@ var PromptClient = class {
1632
1873
  }
1633
1874
  async #handle(event) {
1634
1875
  if (event.event === SSE_EVENTS.pending) {
1635
- const parsed = parseWireJSON(event.data);
1876
+ const parsed = (0, _orkestrel_contract.parseJSON)(event.data);
1636
1877
  if (isPendingPrompt(parsed) && !this.#seen.has(parsed.id)) await this.#dispatch(parsed.id, parsed);
1637
1878
  return;
1638
1879
  }
1639
1880
  if (event.event === SSE_EVENTS.expire) {
1640
- const parsed = parseWireJSON(event.data);
1881
+ const parsed = (0, _orkestrel_contract.parseJSON)(event.data);
1641
1882
  if ((0, _orkestrel_contract.isRecord)(parsed) && (0, _orkestrel_contract.isString)(parsed.id)) {
1642
1883
  this.#seen.delete(parsed.id);
1643
1884
  this.#emitter.emit("expire", parsed.id);
@@ -1715,9 +1956,9 @@ var PromptClient = class {
1715
1956
  * - **Durable open / save.** `open(name)` restores an EMPTY broker from the `store` (parked
1716
1957
  * Promises are process-bound and never resurrected); `save(name)` persists the endpoint's
1717
1958
  * configured `timeout`.
1718
- * - **Removal.** `remove` drops one endpoint or a batch (§9.2, array overload FIRST), destroying
1719
- * each broker (which expires every prompt still parked on it). `clear` drops all; `destroy` is
1720
- * idempotent.
1959
+ * - **Removal.** `remove` drops one endpoint, a batch (§9.2, array overload FIRST), or every
1960
+ * endpoint when called without an argument. It destroys each broker, which expires every prompt
1961
+ * still parked on it. `destroy` is idempotent.
1721
1962
  *
1722
1963
  * @example
1723
1964
  * ```ts
@@ -1856,6 +2097,10 @@ var TerminalManager = class {
1856
2097
  return true;
1857
2098
  }
1858
2099
  remove(names) {
2100
+ if (names === void 0) {
2101
+ for (const name of [...this.#terminals.keys()]) this.#removeOne(name);
2102
+ return;
2103
+ }
1859
2104
  if ((0, _orkestrel_contract.isArray)(names)) {
1860
2105
  let removed = false;
1861
2106
  for (const name of names) if (this.#removeOne(name)) removed = true;
@@ -1863,13 +2108,10 @@ var TerminalManager = class {
1863
2108
  }
1864
2109
  return this.#removeOne(names);
1865
2110
  }
1866
- clear() {
1867
- for (const name of [...this.#terminals.keys()]) this.#removeOne(name);
1868
- }
1869
2111
  destroy() {
1870
2112
  if (this.#destroyed) return;
1871
2113
  this.#destroyed = true;
1872
- this.clear();
2114
+ this.remove();
1873
2115
  this.#emitter.destroy();
1874
2116
  }
1875
2117
  #createPendingListener() {
@@ -2174,11 +2416,11 @@ exports.CTRL_D = CTRL_D;
2174
2416
  exports.CTRL_E = CTRL_E;
2175
2417
  exports.CTRL_U = CTRL_U;
2176
2418
  exports.DEFAULT_MASK = DEFAULT_MASK;
2419
+ exports.DEFAULT_PROMPT_THEME = DEFAULT_PROMPT_THEME;
2177
2420
  exports.DEFAULT_PROMPT_TIMEOUT_MS = DEFAULT_PROMPT_TIMEOUT_MS;
2178
2421
  exports.DEFAULT_RECONNECT_DELAY_MS = DEFAULT_RECONNECT_DELAY_MS;
2179
2422
  exports.DELETE = DELETE;
2180
2423
  exports.DatabaseTerminalStore = DatabaseTerminalStore;
2181
- exports.EMAIL_PATTERN = EMAIL_PATTERN;
2182
2424
  exports.ESCAPE = ESCAPE;
2183
2425
  exports.HEADER_TOKEN = HEADER_TOKEN;
2184
2426
  exports.INTEGER_PATTERN = INTEGER_PATTERN;
@@ -2188,6 +2430,7 @@ exports.MemoryTerminalStore = MemoryTerminalStore;
2188
2430
  exports.NEWLINE = NEWLINE;
2189
2431
  exports.NUMERIC_PATTERN = NUMERIC_PATTERN;
2190
2432
  exports.PROMPT_ICONS = PROMPT_ICONS;
2433
+ exports.PROMPT_ROLES = PROMPT_ROLES;
2191
2434
  exports.Prompt = Prompt;
2192
2435
  exports.PromptClient = PromptClient;
2193
2436
  exports.RETURN = RETURN;
@@ -2216,6 +2459,7 @@ exports.createMemoryTerminalStore = createMemoryTerminalStore;
2216
2459
  exports.createPasswordState = createPasswordState;
2217
2460
  exports.createPrompt = createPrompt;
2218
2461
  exports.createPromptClient = createPromptClient;
2462
+ exports.createPromptTheme = createPromptTheme;
2219
2463
  exports.createSelectState = createSelectState;
2220
2464
  exports.createTerminalManager = createTerminalManager;
2221
2465
  exports.defaultTimer = defaultTimer;
@@ -2227,6 +2471,7 @@ exports.errorLine = errorLine;
2227
2471
  exports.evaluateRule = evaluateRule;
2228
2472
  exports.gateSelection = gateSelection;
2229
2473
  exports.globalFetch = globalFetch;
2474
+ exports.hintedHeader = hintedHeader;
2230
2475
  exports.inputReduce = inputReduce;
2231
2476
  exports.inputView = inputView;
2232
2477
  exports.isAbortError = isAbortError;
@@ -2237,13 +2482,14 @@ exports.isPendingPrompt = isPendingPrompt;
2237
2482
  exports.isPendingPromptStatus = isPendingPromptStatus;
2238
2483
  exports.isPrintable = isPrintable;
2239
2484
  exports.isPromptChoice = isPromptChoice;
2485
+ exports.isPromptThemeOptions = isPromptThemeOptions;
2240
2486
  exports.isPromptType = isPromptType;
2487
+ exports.isStyle = isStyle;
2241
2488
  exports.isTerminalError = isTerminalError;
2242
2489
  exports.isTerminalSnapshot = isTerminalSnapshot;
2243
2490
  exports.normalizeCheckboxChoice = normalizeCheckboxChoice;
2244
2491
  exports.normalizeChoice = normalizeChoice;
2245
2492
  exports.parseKey = parseKey;
2246
- exports.parseWireJSON = parseWireJSON;
2247
2493
  exports.passing = passing;
2248
2494
  exports.passwordReduce = passwordReduce;
2249
2495
  exports.passwordView = passwordView;
@@ -2253,6 +2499,8 @@ exports.resolveChoices = resolveChoices;
2253
2499
  exports.resolveOption = resolveOption;
2254
2500
  exports.resolveValidation = resolveValidation;
2255
2501
  exports.sanitizeChoiceLabels = sanitizeChoiceLabels;
2502
+ exports.sanitizeDisplayText = sanitizeDisplayText;
2503
+ exports.sanitizeThemeIcons = sanitizeThemeIcons;
2256
2504
  exports.selectReduce = selectReduce;
2257
2505
  exports.selectView = selectView;
2258
2506
  exports.serializeChoices = serializeChoices;