hacksh 0.17.0 → 0.18.0

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 CHANGED
@@ -528,7 +528,7 @@ finishes. A printer failure rejects the invocation without changing a successful
528
528
 
529
529
  ### Interactive input
530
530
 
531
- Every `run` context has a `prompt` with `select`, `confirm`, and `text` methods. Interactive input
531
+ Every `run` context has a `prompt` with `select`, `autocomplete`, `confirm`, and `text` methods. Interactive input
532
532
  is separate from `logger`: prompts share the invocation's selected printer, serialized output queue,
533
533
  terminal lock, output mode, and abort signal.
534
534
 
@@ -562,15 +562,57 @@ uses `initialValue` when supplied and otherwise means no. Text displays its plac
562
562
  using it as input, supports editing, and repeats the prompt only when `validate` returns a nonempty
563
563
  message; `""` means valid. Backspace removes one Unicode code point. Input pasted after a selection
564
564
  or confirmation is retained for the next sequential prompt instead of being discarded.
565
- The built-in prompt API intentionally stops at select, confirm, and text; it does not provide a
566
- multiselect prompt.
565
+ The built-in prompt API does not provide a multiselect prompt.
566
+
567
+ Use `autocomplete` for a searchable single choice:
568
+
569
+ ```ts
570
+ const environment = await prompt.autocomplete({
571
+ message: "Environment",
572
+ placeholder: "Type to filter…",
573
+ options: [
574
+ { value: "production", label: "Production", priority: 20 },
575
+ { value: "staging", label: "Staging", hint: "safe default", priority: 10 },
576
+ ],
577
+ sortBy: (item) => item.priority,
578
+ }); // "production" | "staging"; displays Staging before Production
579
+ ```
580
+
581
+ Autocomplete filters labels (falling back to values) by case-insensitive substring,
582
+ preserving the configured option order rather than ranking matches. An empty query
583
+ shows all options, initially with none selected. Down enters selection at the first
584
+ match; Up/Down move without wrapping. Up at the first match or Escape returns to
585
+ editing. Typing or Backspace while selecting edits the query and returns to input.
586
+ Enter submits only a selected match; it does nothing while editing or with no matches.
587
+ Backspace removes one grapheme cluster. Escape while editing and Ctrl-C cancel.
588
+ The placeholder is display-only, reappears when the query becomes empty, and remains
589
+ until the terminal delivers committed input; OS IME composition is not observable.
590
+ The pretty printer anchors the real cursor at the input end, including wrapped input.
591
+
592
+ Both `select` and `autocomplete` accept `sortBy: (item) => string | number`. The
593
+ callback receives the complete inferred option item, including custom fields such
594
+ as `priority`; the prompt still returns only its `value`, preserving literal types.
595
+ Without `sortBy`, options retain their definition order. With it, keys are computed
596
+ once per option at prompt startup and sorted ascending, stably: equal keys retain
597
+ definition order, and the original array is never reordered. Autocomplete filtering
598
+ preserves this sorted order without recomputing keys. `initialValue` on select
599
+ chooses a value in the sorted list without changing that order.
600
+
601
+ Numeric keys use numeric order; strings use locale-independent UTF-16 lexical
602
+ order, not locale-aware collation or numeric-string ordering. All keys in one
603
+ prompt must have the same type. Invalid keys (including `NaN`) or mixed types
604
+ produce `HackshPromptError` with reason `invalid-options`; a thrown callback
605
+ produces `input-error` with the original error as `cause`.
606
+ Explicit option types take the whole item type, for example
607
+ `SelectOptions<{ value: "staging" | "production"; priority: number }>`.
567
608
 
568
609
  Prompts are allowed only when the resolved mode is interactive: pretty output outside CI with TTY
569
610
  stdin and stdout. JSON, CI, and non-TTY invocations throw `HackshPromptError` immediately even if a
570
611
  default is available. Its `reason` and `prompt` fields identify why input failed and the prompt's
571
- `kind`, `message`, and select `choices`. Reasons distinguish `non-interactive`, `cancelled`,
612
+ `kind`, `message`, and select/autocomplete `choices`. Reasons distinguish `non-interactive`, `cancelled`,
572
613
  `aborted`, `eof`, `overlapping`, `invalid-options`, `input-error`, and `printer-error`. Ctrl-C and
573
- Escape cancel; `runMain` reports cancellation and exits with status 130. A prompt that starts or is
614
+ Escape cancel (except Escape from autocomplete selection, which returns to editing);
615
+ `runMain` reports cancellation and exits with status 130. A prompt that starts or is
574
616
  waiting after the invocation's `signal` aborts fails with `aborted` and the signal's reason as its
575
617
  `cause`. Only one prompt may be active in an invocation. Input listeners and raw terminal mode are
576
618
  restored after every outcome.
@@ -878,6 +920,17 @@ await showUsage(main, {
878
920
  | `vp run ready` | Run check, test, and build |
879
921
  | `pnpm test:packed` | Test the tarball with Node 22.18, host Node, and Deno |
880
922
 
923
+ The autocomplete API has PTY tests in `src/autocomplete-pty.test.ts`.
924
+ Run them with `pnpm test run src/autocomplete-pty.test.ts`. They launch
925
+ `src/fixtures/autocomplete-pty-cli.ts`, a CLI using the public `prompt.autocomplete`
926
+ API, and interpret its output with a headless terminal, checking visible choices,
927
+ placeholder transitions, cursor coordinates, and submission/cancellation cleanup.
928
+ For manual terminal verification, run `node src/fixtures/autocomplete-pty-cli.ts`.
929
+ Expected screens use indented multiline strings; ANSI handling stays in
930
+ `src/test-support/terminal.ts`. OS IME composition windows are outside this suite.
931
+ The development-only `node-pty` dependency requires Python and a C/C++ build
932
+ toolchain (Xcode Command Line Tools on macOS).
933
+
881
934
  The packed-consumer test must execute its Node consumer with exactly Node 22.18.0. Run pnpm itself
882
935
  under that version, or supply an absolute executable path explicitly:
883
936
 
@@ -91,7 +91,7 @@ declare class HackshValidationError extends HackshUsageError {
91
91
  readonly issues: ReadonlyArray<HackshIssue>;
92
92
  constructor(command: AnyCommandDef, issues: ReadonlyArray<HackshIssue>);
93
93
  }
94
- type PromptKind = "select" | "confirm" | "text";
94
+ type PromptKind = "select" | "autocomplete" | "confirm" | "text";
95
95
  type HackshPromptErrorReason = "non-interactive" | "cancelled" | "aborted" | "eof" | "overlapping" | "invalid-options" | "input-error" | "printer-error";
96
96
  interface PromptErrorDetails {
97
97
  kind: PromptKind;
@@ -277,7 +277,15 @@ interface TextPromptState extends PromptStateBase {
277
277
  value: string;
278
278
  placeholder?: string;
279
279
  }
280
- type PromptState = SelectPromptState | ConfirmPromptState | TextPromptState;
280
+ interface AutocompletePromptState extends PromptStateBase {
281
+ kind: "autocomplete";
282
+ query: string;
283
+ placeholder?: string;
284
+ phase: "input" | "select";
285
+ choices: readonly PromptChoiceState[];
286
+ value?: string;
287
+ }
288
+ type PromptState = SelectPromptState | ConfirmPromptState | TextPromptState | AutocompletePromptState;
281
289
  interface PromptPrintEvent {
282
290
  type: "prompt";
283
291
  prompt: PromptState;
@@ -352,10 +360,16 @@ interface SelectOption<T extends string> {
352
360
  label?: string;
353
361
  hint?: string;
354
362
  }
355
- interface SelectOptions<T extends string> {
363
+ interface ChoiceOptions<TItem extends SelectOption<string>> {
356
364
  message: string;
357
- options: readonly SelectOption<T>[];
358
- initialValue?: T;
365
+ options: readonly TItem[];
366
+ sortBy?: (item: NoInfer<TItem>) => string | number;
367
+ }
368
+ interface SelectOptions<TItem extends SelectOption<string>> extends ChoiceOptions<TItem> {
369
+ initialValue?: NoInfer<TItem["value"]>;
370
+ }
371
+ interface AutocompleteOptions<TItem extends SelectOption<string>> extends ChoiceOptions<TItem> {
372
+ placeholder?: string;
359
373
  }
360
374
  interface ConfirmOptions {
361
375
  message: string;
@@ -367,7 +381,8 @@ interface TextOptions {
367
381
  validate?(value: string): string | undefined;
368
382
  }
369
383
  interface Prompter {
370
- select<const T extends string>(options: SelectOptions<T>): Promise<T>;
384
+ select<const TItem extends SelectOption<string>>(options: SelectOptions<TItem>): Promise<TItem["value"]>;
385
+ autocomplete<const TItem extends SelectOption<string>>(options: AutocompleteOptions<TItem>): Promise<TItem["value"]>;
371
386
  confirm(options: ConfirmOptions): Promise<boolean>;
372
387
  text(options: TextOptions): Promise<string>;
373
388
  }
@@ -416,5 +431,5 @@ interface CommandDef<S extends ArgsSchema = EmptyArgs, R = unknown, Children ext
416
431
  type AnyCommandDef = CommandDef<ArgsSchema>;
417
432
  declare function defineCommand<S extends ArgsSchema = EmptyArgs, R = never, Children extends Record<string, AnyCommandDef> = {}>(def: CommandDef<S, R, Children>): NoInfer<CommandDef<S, R, Children>>;
418
433
  //#endregion
419
- export { HackshErrorKind as $, SelectPromptState as A, UsageArgumentSummary as B, PrintStream as C, PromptState as D, PromptPrintEvent as E, UsagePrintEvent as F, showUsage as G, UsageOptionSummary as H, VersionPrintEvent as I, LogLevel as J, summarizeUsage as K, createJsonPrinter as L, TerminalInput as M, TerminalOutput as N, PromptStatus as O, TextPromptState as P, HackshError as Q, resolveOutputMode as R, PrintEvent as S, PromptChoiceState as T, UsageSummary as U, UsageCommandSummary as V, renderUsage as W, LogRecord as X, LogProperties as Y, Logger as Z, OutputFormat as _, ExecReturn as _t, ArgsSchema as a, HackshPromptErrorReason as at, OutputPrintEvent as b, SelectOption as c, PromptErrorDetails as ct, Output as d, Exec as dt, HackshErrorSummary as et, ConfirmPromptState as f, ExecCommand as ft, LogPrintEvent as g, ExecResult as gt, JsonPrinterOptions as h, ExecOptions as ht, defineCommand as i, HackshPromptError as it, StreamColors as j, ResolveOutputModeOptions as k, SelectOptions as l, PromptKind as lt, ExecPrintEvent as m, ExecFailureReason as mt, CommandContext as n, HackshExitError as nt, ConfirmOptions as o, HackshUsageError as ot, ErrorPrintEvent as p, ExecFailure as pt, LogKind as q, CommandDef as r, HackshIssue as rt, Prompter as s, HackshValidationError as st, AnyCommandDef as t, HackshExecError as tt, TextOptions as u, ValidationIssueSummary as ut, OutputMode as v, ExecSkipped as vt, Printer as w, OutputWriter as x, OutputPreference as y, ExecSuccess as yt, ShowUsageOptions as z };
420
- //# sourceMappingURL=command-t16M3-AZ.d.ts.map
434
+ export { Logger as $, PromptStatus as A, resolveOutputMode as B, OutputWriter as C, PromptChoiceState as D, Printer as E, TerminalOutput as F, UsageSummary as G, UsageArgumentSummary as H, TextPromptState as I, summarizeUsage as J, renderUsage as K, UsagePrintEvent as L, SelectPromptState as M, StreamColors as N, PromptPrintEvent as O, TerminalInput as P, LogRecord as Q, VersionPrintEvent as R, OutputPrintEvent as S, PrintStream as T, UsageCommandSummary as U, ShowUsageOptions as V, UsageOptionSummary as W, LogLevel as X, LogKind as Y, LogProperties as Z, JsonPrinterOptions as _, ExecOptions as _t, ArgsSchema as a, HackshIssue as at, OutputMode as b, ExecSkipped as bt, Prompter as c, HackshUsageError as ct, TextOptions as d, PromptKind as dt, HackshError as et, Output as f, ValidationIssueSummary as ft, ExecPrintEvent as g, ExecFailureReason as gt, ErrorPrintEvent as h, ExecFailure as ht, defineCommand as i, HackshExitError as it, ResolveOutputModeOptions as j, PromptState as k, SelectOption as l, HackshValidationError as lt, ConfirmPromptState as m, ExecCommand as mt, CommandContext as n, HackshErrorSummary as nt, AutocompleteOptions as o, HackshPromptError as ot, AutocompletePromptState as p, Exec as pt, showUsage as q, CommandDef as r, HackshExecError as rt, ConfirmOptions as s, HackshPromptErrorReason as st, AnyCommandDef as t, HackshErrorKind as tt, SelectOptions as u, PromptErrorDetails as ut, LogPrintEvent as v, ExecResult as vt, PrintEvent as w, OutputPreference as x, ExecSuccess as xt, OutputFormat as y, ExecReturn as yt, createJsonPrinter as z };
435
+ //# sourceMappingURL=command-Czf4qXhe.d.ts.map
package/dist/index.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { G as showUsage, K as summarizeUsage, M as TerminalInput, N as TerminalOutput, Q as HackshError, W as renderUsage, Z as Logger, _t as ExecReturn, a as ArgsSchema, at as HackshPromptErrorReason, c as SelectOption, ct as PromptErrorDetails, d as Output, dt as Exec, ft as ExecCommand, gt as ExecResult, ht as ExecOptions, i as defineCommand, it as HackshPromptError, l as SelectOptions, lt as PromptKind, mt as ExecFailureReason, n as CommandContext, nt as HackshExitError, o as ConfirmOptions, ot as HackshUsageError, pt as ExecFailure, r as CommandDef, rt as HackshIssue, s as Prompter, st as HackshValidationError, t as AnyCommandDef, tt as HackshExecError, u as TextOptions, vt as ExecSkipped, w as Printer, y as OutputPreference, yt as ExecSuccess, z as ShowUsageOptions } from "./command-t16M3-AZ.js";
1
+ import { $ as Logger, E as Printer, F as TerminalOutput, J as summarizeUsage, K as renderUsage, P as TerminalInput, V as ShowUsageOptions, _t as ExecOptions, a as ArgsSchema, at as HackshIssue, bt as ExecSkipped, c as Prompter, ct as HackshUsageError, d as TextOptions, dt as PromptKind, et as HackshError, f as Output, gt as ExecFailureReason, ht as ExecFailure, i as defineCommand, it as HackshExitError, l as SelectOption, lt as HackshValidationError, mt as ExecCommand, n as CommandContext, o as AutocompleteOptions, ot as HackshPromptError, pt as Exec, q as showUsage, r as CommandDef, rt as HackshExecError, s as ConfirmOptions, st as HackshPromptErrorReason, t as AnyCommandDef, u as SelectOptions, ut as PromptErrorDetails, vt as ExecResult, x as OutputPreference, xt as ExecSuccess, yt as ExecReturn } from "./command-Czf4qXhe.js";
2
2
  import { output } from "zod/v4/core";
3
3
  //#region src/parser.d.ts
4
4
  /**
@@ -39,5 +39,5 @@ interface RunMainOptions extends OutputOptions {
39
39
  }
40
40
  declare function runMain<S extends ArgsSchema>(command: CommandDef<S>, options?: RunMainOptions): Promise<void>;
41
41
  //#endregion
42
- export { type CommandContext, type CommandDef, type ConfirmOptions, type Exec, type ExecCommand, type ExecFailure, type ExecFailureReason, type ExecOptions, type ExecResult, type ExecReturn, type ExecSkipped, type ExecSuccess, HackshError, HackshExecError, HackshExitError, type HackshIssue, HackshPromptError, type HackshPromptErrorReason, HackshUsageError, HackshValidationError, type Logger, type Output, type OutputOptions, type PromptErrorDetails, type PromptKind, type Prompter, type RunCommandOptions, type RunMainOptions, type SelectOption, type SelectOptions, type ShowUsageOptions, type TextOptions, defineCommand, parseArgs, renderUsage, runCommand, runMain, showUsage, summarizeUsage };
42
+ export { type AutocompleteOptions, type CommandContext, type CommandDef, type ConfirmOptions, type Exec, type ExecCommand, type ExecFailure, type ExecFailureReason, type ExecOptions, type ExecResult, type ExecReturn, type ExecSkipped, type ExecSuccess, HackshError, HackshExecError, HackshExitError, type HackshIssue, HackshPromptError, type HackshPromptErrorReason, HackshUsageError, HackshValidationError, type Logger, type Output, type OutputOptions, type PromptErrorDetails, type PromptKind, type Prompter, type RunCommandOptions, type RunMainOptions, type SelectOption, type SelectOptions, type ShowUsageOptions, type TextOptions, defineCommand, parseArgs, renderUsage, runCommand, runMain, showUsage, summarizeUsage };
43
43
  //# sourceMappingURL=index.d.ts.map
package/dist/index.js CHANGED
@@ -1,4 +1,4 @@
1
- import { a as createExec, c as HackshExitError, d as HackshValidationError, f as summarizeError, i as resolveOutputMode, l as HackshPromptError, n as createJsonPrinter, o as HackshError, p as summarizeErrors, r as createPrintQueue, s as HackshExecError, t as createPrettyPrinter, u as HackshUsageError } from "./pretty-Db7-kATM.js";
1
+ import { a as createExec, c as HackshExitError, d as HackshValidationError, f as summarizeError, i as resolveOutputMode, l as HackshPromptError, n as createJsonPrinter, o as HackshError, p as summarizeErrors, r as createPrintQueue, s as HackshExecError, t as createPrettyPrinter, u as HackshUsageError } from "./pretty-BjzGnVLM.js";
2
2
  import { globalRegistry, safeParseAsync } from "zod/v4/core";
3
3
  import { parseArgs as parseArgs$1 } from "node:util";
4
4
  import { readFile, realpath } from "node:fs/promises";
@@ -619,6 +619,44 @@ function promptError(reason, prompt, message, cause) {
619
619
  function validateMessage(kind, message, choices) {
620
620
  if (typeof message !== "string" || message.length === 0) throw promptError("invalid-options", details$1(kind, message, choices), `Invalid ${kind} prompt: message must be a non-empty string`);
621
621
  }
622
+ function validateChoices(kind, options) {
623
+ const choices = Array.isArray(options?.options) ? options.options.map((option) => typeof option === "object" && option !== null && typeof option.value === "string" ? option.value : "[Invalid choice]") : [];
624
+ const prompt = details$1(kind, options?.message, choices);
625
+ validateMessage(kind, options?.message, choices);
626
+ if (!Array.isArray(options.options) || options.options.length === 0) throw promptError("invalid-options", prompt, `A ${kind} prompt requires at least one choice`);
627
+ const seen = /* @__PURE__ */ new Set();
628
+ for (const option of options.options) {
629
+ if (typeof option !== "object" || option === null || typeof option.value !== "string" || option.value.length === 0 || option.label !== void 0 && typeof option.label !== "string" || option.hint !== void 0 && typeof option.hint !== "string") throw promptError("invalid-options", prompt, `${kind === "select" ? "Select" : "Autocomplete"} choices must have valid string values`);
630
+ if (seen.has(option.value)) throw promptError("invalid-options", prompt, `Duplicate ${kind} choice: ${option.value}`);
631
+ seen.add(option.value);
632
+ }
633
+ return prompt;
634
+ }
635
+ function sortedChoices(options, prompt) {
636
+ const { sortBy } = options;
637
+ if (sortBy === void 0) return options.options;
638
+ if (typeof sortBy !== "function") throw promptError("invalid-options", prompt, "sortBy must be a function");
639
+ let keyType;
640
+ const keyed = options.options.map((item) => {
641
+ let key;
642
+ try {
643
+ key = sortBy(item);
644
+ } catch (cause) {
645
+ throw promptError("input-error", prompt, "Choice sort key could not be computed", cause);
646
+ }
647
+ if (typeof key !== "string" && (typeof key !== "number" || Number.isNaN(key))) throw promptError("invalid-options", prompt, "sortBy must return a string or a non-NaN number");
648
+ const type = typeof key;
649
+ if (keyType !== void 0 && keyType !== type) throw promptError("invalid-options", prompt, "sortBy keys must all have the same type");
650
+ keyType = type;
651
+ return {
652
+ item,
653
+ key
654
+ };
655
+ });
656
+ keyed.sort((a, b) => a.key < b.key ? -1 : a.key > b.key ? 1 : 0);
657
+ return keyed.map(({ item }) => item);
658
+ }
659
+ const graphemeSegmenter = new Intl.Segmenter(void 0, { granularity: "grapheme" });
622
660
  function asInput(value, prompt) {
623
661
  if (typeof value !== "object" || value === null || typeof Reflect.get(value, "on") !== "function" || typeof Reflect.get(value, "removeListener") !== "function" || typeof Reflect.get(value, "resume") !== "function" || typeof Reflect.get(value, "pause") !== "function") throw promptError("input-error", prompt, "Interactive input is not a readable stream");
624
662
  return value;
@@ -647,7 +685,7 @@ var PromptController = class {
647
685
  throw promptError("printer-error", prompt, "The prompt could not be displayed", cause);
648
686
  }
649
687
  }
650
- async #run(prompt, initial, accept, update, submitted, closed) {
688
+ async #run(prompt, initial, accept, update, submitted, closed, cancelOnEscape = () => true) {
651
689
  if (!this.#interactive) throw promptError("non-interactive", prompt, `Cannot request ${prompt.kind} input in non-interactive mode: ${prompt.message}`);
652
690
  if (this.#signal.aborted) throw promptError("aborted", prompt, "Prompt aborted", this.#signal.reason);
653
691
  const input = asInput(this.#inputValue, prompt);
@@ -689,7 +727,7 @@ var PromptController = class {
689
727
  };
690
728
  const handleKeypress = async (character, key = {}) => {
691
729
  if (settled) return;
692
- if (key.ctrl && key.name === "c" || key.name === "escape") {
730
+ if (key.ctrl && key.name === "c" || key.name === "escape" && cancelOnEscape()) {
693
731
  fail("cancelled", "Prompt cancelled");
694
732
  return;
695
733
  }
@@ -797,36 +835,71 @@ var PromptController = class {
797
835
  }
798
836
  }
799
837
  async select(options) {
800
- const choices = Array.isArray(options?.options) ? options.options.map((option) => typeof option === "object" && option !== null && typeof option.value === "string" ? option.value : "[Invalid choice]") : [];
801
- const prompt = details$1("select", options?.message, choices);
802
- validateMessage("select", options?.message, choices);
803
- if (!Array.isArray(options.options) || options.options.length === 0) throw promptError("invalid-options", prompt, "A select prompt requires at least one choice");
804
- const seen = /* @__PURE__ */ new Set();
805
- for (const option of options.options) {
806
- if (typeof option !== "object" || option === null || typeof option.value !== "string" || option.value.length === 0 || option.label !== void 0 && typeof option.label !== "string" || option.hint !== void 0 && typeof option.hint !== "string") throw promptError("invalid-options", prompt, "Select choices must have valid string values");
807
- if (seen.has(option.value)) throw promptError("invalid-options", prompt, `Duplicate select choice: ${option.value}`);
808
- seen.add(option.value);
809
- }
810
- let selected = options.initialValue === void 0 ? 0 : choices.indexOf(options.initialValue);
838
+ const prompt = validateChoices("select", options);
839
+ const choices = sortedChoices(options, prompt);
840
+ let selected = options.initialValue === void 0 ? 0 : choices.findIndex(({ value }) => value === options.initialValue);
811
841
  if (selected < 0) throw promptError("invalid-options", prompt, `Unknown initial select value: ${String(options.initialValue)}`);
812
842
  const state = (status = "active") => ({
813
843
  kind: "select",
814
844
  message: options.message,
815
845
  status,
816
- value: options.options[selected].value,
817
- choices: options.options.map(({ value, label, hint }, index) => ({
846
+ value: choices[selected].value,
847
+ choices: choices.map(({ value, label, hint }, index) => ({
818
848
  value,
819
849
  label: label ?? value,
820
850
  ...hint === void 0 ? {} : { hint },
821
851
  selected: index === selected
822
852
  }))
823
853
  });
824
- return await this.#run(prompt, state(), (_character, key) => key.name === "return" || key.name === "enter" ? options.options[selected].value : void 0, (_character, key) => {
854
+ return await this.#run(prompt, state(), (_character, key) => key.name === "return" || key.name === "enter" ? choices[selected].value : void 0, (_character, key) => {
825
855
  if (key.name !== "up" && key.name !== "down") return void 0;
826
- selected = (selected + (key.name === "up" ? -1 : 1) + options.options.length) % options.options.length;
856
+ selected = (selected + (key.name === "up" ? -1 : 1) + choices.length) % choices.length;
827
857
  return state();
828
858
  }, () => state("submitted"), () => state("closed"));
829
859
  }
860
+ async autocomplete(options) {
861
+ const prompt = validateChoices("autocomplete", options);
862
+ if (options.placeholder !== void 0 && typeof options.placeholder !== "string") throw promptError("invalid-options", prompt, "Autocomplete placeholder must be a string");
863
+ const choices = sortedChoices(options, prompt);
864
+ let query = "";
865
+ let selected = -1;
866
+ let filtered = choices;
867
+ const state = (status = "active") => ({
868
+ kind: "autocomplete",
869
+ message: options.message,
870
+ status,
871
+ query,
872
+ phase: selected < 0 ? "input" : "select",
873
+ ...options.placeholder === void 0 ? {} : { placeholder: options.placeholder },
874
+ ...selected < 0 ? {} : { value: filtered[selected].value },
875
+ choices: filtered.map(({ value, label, hint }, index) => ({
876
+ value,
877
+ label: label ?? value,
878
+ ...hint === void 0 ? {} : { hint },
879
+ selected: index === selected
880
+ }))
881
+ });
882
+ return await this.#run(prompt, state(), (_character, key) => selected >= 0 && (key.name === "return" || key.name === "enter") ? filtered[selected].value : void 0, (character, key) => {
883
+ if (key.name === "down") {
884
+ if (selected >= filtered.length - 1) return void 0;
885
+ selected += 1;
886
+ } else if (key.name === "up") {
887
+ if (selected < 0) return void 0;
888
+ selected -= 1;
889
+ } else if (key.name === "escape") selected = -1;
890
+ else {
891
+ if (key.name === "backspace") {
892
+ const last = graphemeSegmenter.segment(query).containing(query.length - 1);
893
+ query = last === void 0 ? "" : query.slice(0, last.index);
894
+ } else if (!key.ctrl && !key.meta && character.length > 0 && !/\p{Cc}/u.test(character)) query += character;
895
+ else return void 0;
896
+ selected = -1;
897
+ const search = query.toLowerCase();
898
+ filtered = search.length === 0 ? choices : choices.filter(({ value, label }) => (label ?? value).toLowerCase().includes(search));
899
+ }
900
+ return state();
901
+ }, () => state("submitted"), () => state("closed"), () => selected < 0);
902
+ }
830
903
  async confirm(options) {
831
904
  const prompt = details$1("confirm", options?.message);
832
905
  validateMessage("confirm", options?.message);