@optique/inquirer 1.0.0-dev.0 → 1.0.0-dev.1116

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 ADDED
@@ -0,0 +1,81 @@
1
+ @optique/inquirer
2
+ =================
3
+
4
+ Interactive prompt support for [Optique] via [Inquirer.js].
5
+
6
+ This package wraps any Optique parser with an interactive prompt that fires
7
+ when no CLI value is provided. The fallback priority is:
8
+
9
+ CLI arguments > interactive prompt.
10
+
11
+ Because interactive prompts are inherently asynchronous, the returned parser
12
+ always has `$mode: "async"`.
13
+
14
+ [Optique]: https://optique.dev/
15
+ [Inquirer.js]: https://github.com/SBoudrias/Inquirer.js
16
+
17
+
18
+ Installation
19
+ ------------
20
+
21
+ ~~~~ bash
22
+ deno add jsr:@optique/inquirer
23
+ npm add @optique/inquirer
24
+ pnpm add @optique/inquirer
25
+ yarn add @optique/inquirer
26
+ bun add @optique/inquirer
27
+ ~~~~
28
+
29
+
30
+ Quick start
31
+ -----------
32
+
33
+ ~~~~ typescript
34
+ import { object } from "@optique/core/constructs";
35
+ import { option } from "@optique/core/primitives";
36
+ import { integer, string } from "@optique/core/valueparser";
37
+ import { prompt } from "@optique/inquirer";
38
+ import { run } from "@optique/run";
39
+
40
+ const parser = object({
41
+ name: prompt(option("--name", string()), {
42
+ type: "input",
43
+ message: "Enter your name:",
44
+ }),
45
+ port: prompt(option("--port", integer()), {
46
+ type: "number",
47
+ message: "Enter the port number:",
48
+ default: 3000,
49
+ }),
50
+ });
51
+
52
+ await run(parser);
53
+ ~~~~
54
+
55
+ When `--name` and `--port` are provided on the command line, the prompts are
56
+ skipped. When they are absent, the user sees interactive prompts.
57
+
58
+
59
+ Features
60
+ --------
61
+
62
+ - *Ten prompt types*: `input`, `password`, `number`, `confirm`, `select`,
63
+ `rawlist`, `expand`, `checkbox`, `editor`, and a custom `prompter` for
64
+ testing
65
+ - *Transparent composition* with `bindEnv()` and `bindConfig()` — the
66
+ prompt fires only when no higher-priority source supplies a value
67
+ - *Prompt-only values* via `prompt(fail<T>(), …)` when a value should not
68
+ be exposed as a CLI option
69
+ - *TTY-free testing* via the `prompter` escape hatch on every config type
70
+
71
+
72
+ Documentation
73
+ -------------
74
+
75
+ For full documentation, visit <https://optique.dev/integrations/inquirer>.
76
+
77
+
78
+ License
79
+ -------
80
+
81
+ MIT License. See [LICENSE](../../LICENSE) for details.
package/dist/index.cjs CHANGED
@@ -23,9 +23,87 @@ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__ge
23
23
  //#endregion
24
24
  const __inquirer_prompts = __toESM(require("@inquirer/prompts"));
25
25
  const __optique_core_annotations = __toESM(require("@optique/core/annotations"));
26
+ const __optique_core_context = __toESM(require("@optique/core/context"));
26
27
  const __optique_core_message = __toESM(require("@optique/core/message"));
27
28
 
28
29
  //#region src/index.ts
30
+ const promptFunctionsOverrideSymbol = Symbol.for("@optique/inquirer/prompt-functions");
31
+ const defaultPromptFunctions = {
32
+ confirm: __inquirer_prompts.confirm,
33
+ number: __inquirer_prompts.number,
34
+ input: __inquirer_prompts.input,
35
+ password: __inquirer_prompts.password,
36
+ editor: __inquirer_prompts.editor,
37
+ select: __inquirer_prompts.select,
38
+ rawlist: __inquirer_prompts.rawlist,
39
+ expand: __inquirer_prompts.expand,
40
+ checkbox: __inquirer_prompts.checkbox
41
+ };
42
+ function promptFunctionKeys() {
43
+ return Object.keys(defaultPromptFunctions);
44
+ }
45
+ function assignPromptFunctionOverride(override, key, candidate) {
46
+ if (typeof candidate === "function") override[key] = candidate;
47
+ }
48
+ /**
49
+ * Extracts valid prompt function overrides from an arbitrary value.
50
+ */
51
+ function getPromptFunctionsOverride(value) {
52
+ if (typeof value !== "object" || value == null) return void 0;
53
+ const override = {};
54
+ for (const key of promptFunctionKeys()) assignPromptFunctionOverride(override, key, Reflect.get(value, key));
55
+ return override;
56
+ }
57
+ /**
58
+ * Returns the active prompt function set, applying any global test overrides.
59
+ */
60
+ function getPromptFunctions() {
61
+ const override = getPromptFunctionsOverride(Reflect.get(globalThis, promptFunctionsOverrideSymbol));
62
+ return override != null ? {
63
+ ...defaultPromptFunctions,
64
+ ...override
65
+ } : defaultPromptFunctions;
66
+ }
67
+ /**
68
+ * Determines whether an error came from an interrupted Inquirer prompt.
69
+ */
70
+ function isExitPromptError(error) {
71
+ return typeof error === "object" && error != null && "name" in error && error.name === "ExitPromptError";
72
+ }
73
+ const inheritParentAnnotationsKey = Symbol.for("@optique/core/inheritParentAnnotations");
74
+ var DeferredPromptValue = class {
75
+ [__optique_core_context.placeholder] = true;
76
+ constructor() {}
77
+ };
78
+ function shouldDeferPrompt(parser, state) {
79
+ return typeof parser.shouldDeferCompletion === "function" && parser.shouldDeferCompletion(state) === true;
80
+ }
81
+ function deferredPromptResult() {
82
+ return {
83
+ success: true,
84
+ value: new DeferredPromptValue()
85
+ };
86
+ }
87
+ function withAnnotationView(state, annotations, run) {
88
+ const annotatedState = new Proxy(state, {
89
+ get(target, key) {
90
+ if (key === __optique_core_annotations.annotationKey) return annotations;
91
+ const value = Reflect.get(target, key, target);
92
+ return typeof value === "function" ? value.bind(target) : value;
93
+ },
94
+ has(target, key) {
95
+ return key === __optique_core_annotations.annotationKey || Reflect.has(target, key);
96
+ }
97
+ });
98
+ return run(annotatedState);
99
+ }
100
+ function withAnnotatedInnerState(sourceState, innerState, run) {
101
+ const annotations = (0, __optique_core_annotations.getAnnotations)(sourceState);
102
+ if (annotations == null || innerState == null || typeof innerState !== "object" || typeof innerState === "object" && __optique_core_annotations.annotationKey in innerState) return run(innerState);
103
+ const inheritedState = (0, __optique_core_annotations.inheritAnnotations)(sourceState, innerState);
104
+ if (inheritedState !== innerState) return run(inheritedState);
105
+ return withAnnotationView(innerState, annotations, (annotatedState) => run(annotatedState));
106
+ }
29
107
  /**
30
108
  * Wraps a parser with an interactive Inquirer.js prompt fallback.
31
109
  *
@@ -52,6 +130,8 @@ const __optique_core_message = __toESM(require("@optique/core/message"));
52
130
  * @param parser Inner parser that reads CLI values.
53
131
  * @param config Type-safe Inquirer.js prompt configuration.
54
132
  * @returns A parser with interactive prompt fallback, always in async mode.
133
+ * @throws {Error} If prompt execution fails with an unexpected error or if
134
+ * the inner parser throws while parsing or completing.
55
135
  * @since 1.0.0
56
136
  */
57
137
  function prompt(parser, config) {
@@ -64,116 +144,170 @@ function prompt(parser, config) {
64
144
  [promptBindStateKey] = true;
65
145
  hasCliValue = false;
66
146
  };
67
- const promptBindInitialState = new PromptBindInitialStateClass();
68
147
  let promptCache = null;
69
- async function executePrompt() {
70
- if ("prompter" in cfg && cfg.prompter != null) {
71
- const value = await cfg.prompter();
72
- if (cfg.type === "number" && value === void 0) return {
73
- success: false,
74
- error: __optique_core_message.message`No number provided.`
75
- };
76
- return {
77
- success: true,
78
- value
79
- };
80
- }
81
- switch (cfg.type) {
82
- case "confirm": return {
83
- success: true,
84
- value: await (0, __inquirer_prompts.confirm)({
85
- message: cfg.message,
86
- ...cfg.default !== void 0 ? { default: cfg.default } : {}
87
- })
88
- };
89
- case "number": {
90
- const numResult = await (0, __inquirer_prompts.number)({
91
- message: cfg.message,
92
- ...cfg.default !== void 0 ? { default: cfg.default } : {},
93
- ...cfg.min !== void 0 ? { min: cfg.min } : {},
94
- ...cfg.max !== void 0 ? { max: cfg.max } : {},
95
- ...cfg.step !== void 0 ? { step: cfg.step } : {}
96
- });
97
- if (numResult === void 0) return {
148
+ function shouldAttemptInnerCompletion(cliState, state) {
149
+ if (cliState == null || cliState instanceof PromptBindInitialStateClass) return false;
150
+ const cliStateHasAnnotations = typeof cliState === "object" && __optique_core_annotations.annotationKey in cliState;
151
+ if (cliStateHasAnnotations) return true;
152
+ if ((0, __optique_core_annotations.getAnnotations)(state) == null || typeof cliState !== "object") return false;
153
+ if ("hasCliValue" in cliState) return true;
154
+ if (Array.isArray(cliState)) return typeof parser.shouldDeferCompletion === "function";
155
+ const prototype = Object.getPrototypeOf(cliState);
156
+ return prototype !== Object.prototype && prototype !== null;
157
+ }
158
+ /**
159
+ * Executes the configured prompt and normalizes its result.
160
+ *
161
+ * Converts `ExitPromptError` into a parse failure and returns prompt values
162
+ * in Optique's `ValueParserResult` shape.
163
+ *
164
+ * @returns The normalized prompt result.
165
+ * @throws {Error} Rethrows unexpected prompt failures after converting
166
+ * `ExitPromptError` cancellations into parse failures.
167
+ */
168
+ function validatePromptedValue(result) {
169
+ return result;
170
+ }
171
+ const validPromptTypes = new Set([
172
+ "confirm",
173
+ "number",
174
+ "input",
175
+ "password",
176
+ "editor",
177
+ "select",
178
+ "rawlist",
179
+ "expand",
180
+ "checkbox"
181
+ ]);
182
+ async function executePromptRaw() {
183
+ const prompts = getPromptFunctions();
184
+ try {
185
+ if (!validPromptTypes.has(cfg.type)) throw new TypeError(`Unsupported prompt type: ${cfg.type}`);
186
+ if ("prompter" in cfg && cfg.prompter != null) {
187
+ const value = await cfg.prompter();
188
+ if (cfg.type === "number" && value === void 0) return {
98
189
  success: false,
99
190
  error: __optique_core_message.message`No number provided.`
100
191
  };
101
192
  return {
102
193
  success: true,
103
- value: numResult
194
+ value
104
195
  };
105
196
  }
106
- case "input": return {
107
- success: true,
108
- value: await (0, __inquirer_prompts.input)({
109
- message: cfg.message,
110
- ...cfg.default !== void 0 ? { default: cfg.default } : {},
111
- ...cfg.validate !== void 0 ? { validate: cfg.validate } : {}
112
- })
113
- };
114
- case "password": return {
115
- success: true,
116
- value: await (0, __inquirer_prompts.password)({
117
- message: cfg.message,
118
- ...cfg.mask !== void 0 ? { mask: cfg.mask } : {},
119
- ...cfg.validate !== void 0 ? { validate: cfg.validate } : {}
120
- })
121
- };
122
- case "editor": return {
123
- success: true,
124
- value: await (0, __inquirer_prompts.editor)({
125
- message: cfg.message,
126
- ...cfg.default !== void 0 ? { default: cfg.default } : {},
127
- ...cfg.validate !== void 0 ? { validate: cfg.validate } : {}
128
- })
129
- };
130
- case "select": return {
131
- success: true,
132
- value: await (0, __inquirer_prompts.select)({
133
- message: cfg.message,
134
- choices: normalizeChoices(cfg.choices),
135
- ...cfg.default !== void 0 ? { default: cfg.default } : {}
136
- })
137
- };
138
- case "rawlist": return {
139
- success: true,
140
- value: await (0, __inquirer_prompts.rawlist)({
141
- message: cfg.message,
142
- choices: normalizeChoices(cfg.choices),
143
- ...cfg.default !== void 0 ? { default: cfg.default } : {}
144
- })
145
- };
146
- case "expand": return {
147
- success: true,
148
- value: await (0, __inquirer_prompts.expand)({
149
- message: cfg.message,
150
- choices: cfg.choices,
151
- ...cfg.default !== void 0 ? { default: cfg.default } : {}
152
- })
153
- };
154
- case "checkbox": return {
155
- success: true,
156
- value: await (0, __inquirer_prompts.checkbox)({
157
- message: cfg.message,
158
- choices: normalizeChoices(cfg.choices)
159
- })
197
+ switch (cfg.type) {
198
+ case "confirm": return {
199
+ success: true,
200
+ value: await prompts.confirm({
201
+ message: cfg.message,
202
+ ...cfg.default !== void 0 ? { default: cfg.default } : {}
203
+ })
204
+ };
205
+ case "number": {
206
+ const numResult = await prompts.number({
207
+ message: cfg.message,
208
+ ...cfg.default !== void 0 ? { default: cfg.default } : {},
209
+ ...cfg.min !== void 0 ? { min: cfg.min } : {},
210
+ ...cfg.max !== void 0 ? { max: cfg.max } : {},
211
+ ...cfg.step !== void 0 ? { step: cfg.step } : {}
212
+ });
213
+ if (numResult === void 0) return {
214
+ success: false,
215
+ error: __optique_core_message.message`No number provided.`
216
+ };
217
+ return {
218
+ success: true,
219
+ value: numResult
220
+ };
221
+ }
222
+ case "input": return {
223
+ success: true,
224
+ value: await prompts.input({
225
+ message: cfg.message,
226
+ ...cfg.default !== void 0 ? { default: cfg.default } : {},
227
+ ...cfg.validate !== void 0 ? { validate: cfg.validate } : {}
228
+ })
229
+ };
230
+ case "password": return {
231
+ success: true,
232
+ value: await prompts.password({
233
+ message: cfg.message,
234
+ ...cfg.mask !== void 0 ? { mask: cfg.mask } : {},
235
+ ...cfg.validate !== void 0 ? { validate: cfg.validate } : {}
236
+ })
237
+ };
238
+ case "editor": return {
239
+ success: true,
240
+ value: await prompts.editor({
241
+ message: cfg.message,
242
+ ...cfg.default !== void 0 ? { default: cfg.default } : {},
243
+ ...cfg.validate !== void 0 ? { validate: cfg.validate } : {}
244
+ })
245
+ };
246
+ case "select": return {
247
+ success: true,
248
+ value: await prompts.select({
249
+ message: cfg.message,
250
+ choices: normalizeChoices(cfg.choices),
251
+ ...cfg.default !== void 0 ? { default: cfg.default } : {}
252
+ })
253
+ };
254
+ case "rawlist": return {
255
+ success: true,
256
+ value: await prompts.rawlist({
257
+ message: cfg.message,
258
+ choices: normalizeChoices(cfg.choices),
259
+ ...cfg.default !== void 0 ? { default: cfg.default } : {}
260
+ })
261
+ };
262
+ case "expand": return {
263
+ success: true,
264
+ value: await prompts.expand({
265
+ message: cfg.message,
266
+ choices: cfg.choices,
267
+ ...cfg.default !== void 0 ? { default: cfg.default } : {}
268
+ })
269
+ };
270
+ case "checkbox": return {
271
+ success: true,
272
+ value: await prompts.checkbox({
273
+ message: cfg.message,
274
+ choices: normalizeChoices(cfg.choices)
275
+ })
276
+ };
277
+ }
278
+ } catch (error) {
279
+ if (isExitPromptError(error)) return {
280
+ success: false,
281
+ error: __optique_core_message.message`Prompt cancelled.`
160
282
  };
283
+ throw error;
161
284
  }
162
285
  }
163
- return {
286
+ async function executePrompt() {
287
+ const result = await executePromptRaw();
288
+ return validatePromptedValue(result);
289
+ }
290
+ function usePromptOrDefer(state, result) {
291
+ if (result.success) return Promise.resolve(result);
292
+ return shouldDeferPrompt(parser, state) ? Promise.resolve(deferredPromptResult()) : executePrompt();
293
+ }
294
+ const promptedParser = {
164
295
  $mode: "async",
165
296
  $valueType: parser.$valueType,
166
297
  $stateType: parser.$stateType,
167
298
  priority: parser.priority,
168
- usage: [{
299
+ [inheritParentAnnotationsKey]: true,
300
+ usage: parser.usage.length === 1 && parser.usage[0].type === "optional" ? parser.usage : [{
169
301
  type: "optional",
170
302
  terms: parser.usage
171
303
  }],
172
- initialState: promptBindInitialState,
304
+ get initialState() {
305
+ return new PromptBindInitialStateClass();
306
+ },
173
307
  parse: (context) => {
174
308
  const annotations = (0, __optique_core_annotations.getAnnotations)(context.state);
175
309
  const innerState = isPromptBindState(context.state) ? context.state.hasCliValue ? context.state.cliState : parser.initialState : context.state;
176
- const innerContext = innerState !== context.state ? {
310
+ const baseInnerContext = innerState !== context.state ? {
177
311
  ...context,
178
312
  state: innerState
179
313
  } : context;
@@ -204,32 +338,98 @@ function prompt(parser, config) {
204
338
  return {
205
339
  success: true,
206
340
  next: {
207
- ...innerContext,
341
+ ...baseInnerContext,
208
342
  state: nextState
209
343
  },
210
344
  consumed: []
211
345
  };
212
346
  };
213
- const result = parser.parse(innerContext);
347
+ const result = withAnnotatedInnerState(context.state, innerState, (annotatedInnerState) => {
348
+ const innerContext = annotatedInnerState !== context.state ? {
349
+ ...context,
350
+ state: annotatedInnerState
351
+ } : context;
352
+ return parser.parse(innerContext);
353
+ });
214
354
  if (result instanceof Promise) return result.then(processResult);
215
355
  return Promise.resolve(processResult(result));
216
356
  },
217
357
  complete: (state) => {
218
358
  if (isPromptBindState(state) && state.hasCliValue) {
219
- const r = parser.complete(state.cliState);
359
+ const r = withAnnotatedInnerState(state, state.cliState, (annotatedInnerState) => parser.complete(annotatedInnerState));
220
360
  if (r instanceof Promise) return r;
221
361
  return Promise.resolve(r);
222
362
  }
223
- if (state === promptBindInitialState) {
224
- if (promptCache !== null) {
225
- const cached = promptCache;
363
+ if (state instanceof PromptBindInitialStateClass) {
364
+ if (promptCache?.state === state) {
365
+ const cached = promptCache.result;
226
366
  promptCache = null;
227
367
  return cached;
228
368
  }
229
- promptCache = executePrompt();
230
- return promptCache;
369
+ const hasDeferHook = typeof parser.shouldDeferCompletion === "function";
370
+ const annotations = (0, __optique_core_annotations.getAnnotations)(state);
371
+ const innerInitialState = parser.initialState;
372
+ const effectiveInitialState = annotations != null && innerInitialState == null ? (0, __optique_core_annotations.injectAnnotations)(innerInitialState, annotations) : innerInitialState;
373
+ if (hasDeferHook) {
374
+ const annotatedR = withAnnotatedInnerState(state, effectiveInitialState, (annotatedInnerState) => parser.complete(annotatedInnerState));
375
+ const usePromptOrDeferSentinel = (res) => {
376
+ if (res.success && res.value === void 0) return usePromptOrDefer(state, {
377
+ success: false,
378
+ error: []
379
+ });
380
+ return usePromptOrDefer(state, res);
381
+ };
382
+ const cachedResult$1 = annotatedR instanceof Promise ? annotatedR.then(usePromptOrDeferSentinel) : usePromptOrDeferSentinel(annotatedR);
383
+ promptCache = {
384
+ state,
385
+ result: cachedResult$1
386
+ };
387
+ return cachedResult$1;
388
+ }
389
+ const simParseR = withAnnotatedInnerState(state, effectiveInitialState, (annotatedState) => parser.parse({
390
+ buffer: [],
391
+ state: annotatedState,
392
+ optionsTerminated: false,
393
+ usage: parser.usage
394
+ }));
395
+ const decideFromParse = (parseResult) => {
396
+ const consumed = parseResult.success ? parseResult.consumed.length : 0;
397
+ const cliState$1 = parseResult.success && consumed === 0 ? parseResult.next.state : void 0;
398
+ const hasSourceBindingMarker = (s) => s != null && typeof s === "object" && "hasCliValue" in s && Object.getOwnPropertySymbols(s).length > 0;
399
+ const cliStateIsPassthrough = cliState$1 != null && typeof cliState$1 === "object" && (0, __optique_core_annotations.unwrapInjectedAnnotationWrapper)(cliState$1) !== cliState$1;
400
+ const isSourceBinding = shouldAttemptInnerCompletion(cliState$1, state) && !cliStateIsPassthrough || hasSourceBindingMarker(cliState$1) || Array.isArray(cliState$1) && cliState$1.length === 1 && (hasSourceBindingMarker(cliState$1[0]) || typeof cliState$1[0] === "object" && cliState$1[0] != null && __optique_core_annotations.annotationKey in cliState$1[0]);
401
+ if (isSourceBinding) {
402
+ const cliStateIsInjected = cliState$1 != null && typeof cliState$1 === "object" && (0, __optique_core_annotations.unwrapInjectedAnnotationWrapper)(cliState$1) !== cliState$1;
403
+ const handleCompleteResult = (res) => {
404
+ if (res.success && res.value === void 0 && cliStateIsInjected) return executePrompt();
405
+ return usePromptOrDefer(state, res);
406
+ };
407
+ const completeState = parseResult.success ? parseResult.next.state : effectiveInitialState;
408
+ const completeR = parser.complete(completeState);
409
+ if (completeR instanceof Promise) return completeR.then(handleCompleteResult);
410
+ return handleCompleteResult(completeR);
411
+ }
412
+ return executePrompt();
413
+ };
414
+ const cachedResult = simParseR instanceof Promise ? simParseR.then(decideFromParse) : decideFromParse(simParseR);
415
+ promptCache = {
416
+ state,
417
+ result: cachedResult
418
+ };
419
+ return cachedResult;
420
+ }
421
+ const cliState = isPromptBindState(state) ? state.cliState : void 0;
422
+ const cliStateIsInjectedAnnotationWrapper = cliState != null && typeof cliState === "object" && (0, __optique_core_annotations.unwrapInjectedAnnotationWrapper)(cliState) !== cliState;
423
+ if (shouldAttemptInnerCompletion(cliState, state)) {
424
+ const useCompleteResultOrPrompt = (result) => {
425
+ if (result.success && result.value === void 0 && cliStateIsInjectedAnnotationWrapper) return executePrompt();
426
+ return usePromptOrDefer(state, result);
427
+ };
428
+ const r = withAnnotatedInnerState(state, cliState, (annotatedInnerState) => parser.complete(annotatedInnerState));
429
+ if (r instanceof Promise) return r.then(useCompleteResultOrPrompt);
430
+ return useCompleteResultOrPrompt(r);
231
431
  }
232
- return executePrompt();
432
+ return shouldDeferPrompt(parser, state) ? Promise.resolve(deferredPromptResult()) : executePrompt();
233
433
  },
234
434
  suggest: (context, prefix) => {
235
435
  const innerState = isPromptBindState(context.state) ? context.state.hasCliValue ? context.state.cliState : parser.initialState : context.state;
@@ -248,6 +448,7 @@ function prompt(parser, config) {
248
448
  return parser.getDocFragments(state, defaultValue);
249
449
  }
250
450
  };
451
+ return promptedParser;
251
452
  }
252
453
  /** Normalize choices to the format Inquirer.js expects. */
253
454
  function normalizeChoices(choices) {
package/dist/index.d.cts CHANGED
@@ -347,6 +347,8 @@ type BasePromptConfig<T> = T extends boolean ? ConfirmConfig : T extends number
347
347
  * @param parser Inner parser that reads CLI values.
348
348
  * @param config Type-safe Inquirer.js prompt configuration.
349
349
  * @returns A parser with interactive prompt fallback, always in async mode.
350
+ * @throws {Error} If prompt execution fails with an unexpected error or if
351
+ * the inner parser throws while parsing or completing.
350
352
  * @since 1.0.0
351
353
  */
352
354
  declare function prompt<M extends Mode, TValue, TState>(parser: Parser<M, TValue, TState>, config: PromptConfig<TValue>): Parser<"async", TValue, TState>;
package/dist/index.d.ts CHANGED
@@ -347,6 +347,8 @@ type BasePromptConfig<T> = T extends boolean ? ConfirmConfig : T extends number
347
347
  * @param parser Inner parser that reads CLI values.
348
348
  * @param config Type-safe Inquirer.js prompt configuration.
349
349
  * @returns A parser with interactive prompt fallback, always in async mode.
350
+ * @throws {Error} If prompt execution fails with an unexpected error or if
351
+ * the inner parser throws while parsing or completing.
350
352
  * @since 1.0.0
351
353
  */
352
354
  declare function prompt<M extends Mode, TValue, TState>(parser: Parser<M, TValue, TState>, config: PromptConfig<TValue>): Parser<"async", TValue, TState>;
package/dist/index.js CHANGED
@@ -1,8 +1,86 @@
1
1
  import { Separator, checkbox, confirm, editor, expand, input, number, password, rawlist, select } from "@inquirer/prompts";
2
- import { annotationKey, getAnnotations } from "@optique/core/annotations";
2
+ import { annotationKey, getAnnotations, inheritAnnotations, injectAnnotations, unwrapInjectedAnnotationWrapper } from "@optique/core/annotations";
3
+ import { placeholder } from "@optique/core/context";
3
4
  import { message } from "@optique/core/message";
4
5
 
5
6
  //#region src/index.ts
7
+ const promptFunctionsOverrideSymbol = Symbol.for("@optique/inquirer/prompt-functions");
8
+ const defaultPromptFunctions = {
9
+ confirm,
10
+ number,
11
+ input,
12
+ password,
13
+ editor,
14
+ select,
15
+ rawlist,
16
+ expand,
17
+ checkbox
18
+ };
19
+ function promptFunctionKeys() {
20
+ return Object.keys(defaultPromptFunctions);
21
+ }
22
+ function assignPromptFunctionOverride(override, key, candidate) {
23
+ if (typeof candidate === "function") override[key] = candidate;
24
+ }
25
+ /**
26
+ * Extracts valid prompt function overrides from an arbitrary value.
27
+ */
28
+ function getPromptFunctionsOverride(value) {
29
+ if (typeof value !== "object" || value == null) return void 0;
30
+ const override = {};
31
+ for (const key of promptFunctionKeys()) assignPromptFunctionOverride(override, key, Reflect.get(value, key));
32
+ return override;
33
+ }
34
+ /**
35
+ * Returns the active prompt function set, applying any global test overrides.
36
+ */
37
+ function getPromptFunctions() {
38
+ const override = getPromptFunctionsOverride(Reflect.get(globalThis, promptFunctionsOverrideSymbol));
39
+ return override != null ? {
40
+ ...defaultPromptFunctions,
41
+ ...override
42
+ } : defaultPromptFunctions;
43
+ }
44
+ /**
45
+ * Determines whether an error came from an interrupted Inquirer prompt.
46
+ */
47
+ function isExitPromptError(error) {
48
+ return typeof error === "object" && error != null && "name" in error && error.name === "ExitPromptError";
49
+ }
50
+ const inheritParentAnnotationsKey = Symbol.for("@optique/core/inheritParentAnnotations");
51
+ var DeferredPromptValue = class {
52
+ [placeholder] = true;
53
+ constructor() {}
54
+ };
55
+ function shouldDeferPrompt(parser, state) {
56
+ return typeof parser.shouldDeferCompletion === "function" && parser.shouldDeferCompletion(state) === true;
57
+ }
58
+ function deferredPromptResult() {
59
+ return {
60
+ success: true,
61
+ value: new DeferredPromptValue()
62
+ };
63
+ }
64
+ function withAnnotationView(state, annotations, run) {
65
+ const annotatedState = new Proxy(state, {
66
+ get(target, key) {
67
+ if (key === annotationKey) return annotations;
68
+ const value = Reflect.get(target, key, target);
69
+ return typeof value === "function" ? value.bind(target) : value;
70
+ },
71
+ has(target, key) {
72
+ return key === annotationKey || Reflect.has(target, key);
73
+ }
74
+ });
75
+ return run(annotatedState);
76
+ }
77
+ function withAnnotatedInnerState(sourceState, innerState, run) {
78
+ const annotations = getAnnotations(sourceState);
79
+ if (annotations == null || innerState == null || typeof innerState !== "object" || typeof innerState === "object" && annotationKey in innerState) return run(innerState);
80
+ const inheritedState = inheritAnnotations(sourceState, innerState);
81
+ if (inheritedState !== innerState) return run(inheritedState);
82
+ return withAnnotationView(innerState, annotations, (annotatedState) => run(annotatedState));
83
+ }
6
84
  /**
7
85
  * Wraps a parser with an interactive Inquirer.js prompt fallback.
8
86
  *
@@ -29,6 +107,8 @@ import { message } from "@optique/core/message";
29
107
  * @param parser Inner parser that reads CLI values.
30
108
  * @param config Type-safe Inquirer.js prompt configuration.
31
109
  * @returns A parser with interactive prompt fallback, always in async mode.
110
+ * @throws {Error} If prompt execution fails with an unexpected error or if
111
+ * the inner parser throws while parsing or completing.
32
112
  * @since 1.0.0
33
113
  */
34
114
  function prompt(parser, config) {
@@ -41,116 +121,170 @@ function prompt(parser, config) {
41
121
  [promptBindStateKey] = true;
42
122
  hasCliValue = false;
43
123
  };
44
- const promptBindInitialState = new PromptBindInitialStateClass();
45
124
  let promptCache = null;
46
- async function executePrompt() {
47
- if ("prompter" in cfg && cfg.prompter != null) {
48
- const value = await cfg.prompter();
49
- if (cfg.type === "number" && value === void 0) return {
50
- success: false,
51
- error: message`No number provided.`
52
- };
53
- return {
54
- success: true,
55
- value
56
- };
57
- }
58
- switch (cfg.type) {
59
- case "confirm": return {
60
- success: true,
61
- value: await confirm({
62
- message: cfg.message,
63
- ...cfg.default !== void 0 ? { default: cfg.default } : {}
64
- })
65
- };
66
- case "number": {
67
- const numResult = await number({
68
- message: cfg.message,
69
- ...cfg.default !== void 0 ? { default: cfg.default } : {},
70
- ...cfg.min !== void 0 ? { min: cfg.min } : {},
71
- ...cfg.max !== void 0 ? { max: cfg.max } : {},
72
- ...cfg.step !== void 0 ? { step: cfg.step } : {}
73
- });
74
- if (numResult === void 0) return {
125
+ function shouldAttemptInnerCompletion(cliState, state) {
126
+ if (cliState == null || cliState instanceof PromptBindInitialStateClass) return false;
127
+ const cliStateHasAnnotations = typeof cliState === "object" && annotationKey in cliState;
128
+ if (cliStateHasAnnotations) return true;
129
+ if (getAnnotations(state) == null || typeof cliState !== "object") return false;
130
+ if ("hasCliValue" in cliState) return true;
131
+ if (Array.isArray(cliState)) return typeof parser.shouldDeferCompletion === "function";
132
+ const prototype = Object.getPrototypeOf(cliState);
133
+ return prototype !== Object.prototype && prototype !== null;
134
+ }
135
+ /**
136
+ * Executes the configured prompt and normalizes its result.
137
+ *
138
+ * Converts `ExitPromptError` into a parse failure and returns prompt values
139
+ * in Optique's `ValueParserResult` shape.
140
+ *
141
+ * @returns The normalized prompt result.
142
+ * @throws {Error} Rethrows unexpected prompt failures after converting
143
+ * `ExitPromptError` cancellations into parse failures.
144
+ */
145
+ function validatePromptedValue(result) {
146
+ return result;
147
+ }
148
+ const validPromptTypes = new Set([
149
+ "confirm",
150
+ "number",
151
+ "input",
152
+ "password",
153
+ "editor",
154
+ "select",
155
+ "rawlist",
156
+ "expand",
157
+ "checkbox"
158
+ ]);
159
+ async function executePromptRaw() {
160
+ const prompts = getPromptFunctions();
161
+ try {
162
+ if (!validPromptTypes.has(cfg.type)) throw new TypeError(`Unsupported prompt type: ${cfg.type}`);
163
+ if ("prompter" in cfg && cfg.prompter != null) {
164
+ const value = await cfg.prompter();
165
+ if (cfg.type === "number" && value === void 0) return {
75
166
  success: false,
76
167
  error: message`No number provided.`
77
168
  };
78
169
  return {
79
170
  success: true,
80
- value: numResult
171
+ value
81
172
  };
82
173
  }
83
- case "input": return {
84
- success: true,
85
- value: await input({
86
- message: cfg.message,
87
- ...cfg.default !== void 0 ? { default: cfg.default } : {},
88
- ...cfg.validate !== void 0 ? { validate: cfg.validate } : {}
89
- })
90
- };
91
- case "password": return {
92
- success: true,
93
- value: await password({
94
- message: cfg.message,
95
- ...cfg.mask !== void 0 ? { mask: cfg.mask } : {},
96
- ...cfg.validate !== void 0 ? { validate: cfg.validate } : {}
97
- })
98
- };
99
- case "editor": return {
100
- success: true,
101
- value: await editor({
102
- message: cfg.message,
103
- ...cfg.default !== void 0 ? { default: cfg.default } : {},
104
- ...cfg.validate !== void 0 ? { validate: cfg.validate } : {}
105
- })
106
- };
107
- case "select": return {
108
- success: true,
109
- value: await select({
110
- message: cfg.message,
111
- choices: normalizeChoices(cfg.choices),
112
- ...cfg.default !== void 0 ? { default: cfg.default } : {}
113
- })
114
- };
115
- case "rawlist": return {
116
- success: true,
117
- value: await rawlist({
118
- message: cfg.message,
119
- choices: normalizeChoices(cfg.choices),
120
- ...cfg.default !== void 0 ? { default: cfg.default } : {}
121
- })
122
- };
123
- case "expand": return {
124
- success: true,
125
- value: await expand({
126
- message: cfg.message,
127
- choices: cfg.choices,
128
- ...cfg.default !== void 0 ? { default: cfg.default } : {}
129
- })
130
- };
131
- case "checkbox": return {
132
- success: true,
133
- value: await checkbox({
134
- message: cfg.message,
135
- choices: normalizeChoices(cfg.choices)
136
- })
174
+ switch (cfg.type) {
175
+ case "confirm": return {
176
+ success: true,
177
+ value: await prompts.confirm({
178
+ message: cfg.message,
179
+ ...cfg.default !== void 0 ? { default: cfg.default } : {}
180
+ })
181
+ };
182
+ case "number": {
183
+ const numResult = await prompts.number({
184
+ message: cfg.message,
185
+ ...cfg.default !== void 0 ? { default: cfg.default } : {},
186
+ ...cfg.min !== void 0 ? { min: cfg.min } : {},
187
+ ...cfg.max !== void 0 ? { max: cfg.max } : {},
188
+ ...cfg.step !== void 0 ? { step: cfg.step } : {}
189
+ });
190
+ if (numResult === void 0) return {
191
+ success: false,
192
+ error: message`No number provided.`
193
+ };
194
+ return {
195
+ success: true,
196
+ value: numResult
197
+ };
198
+ }
199
+ case "input": return {
200
+ success: true,
201
+ value: await prompts.input({
202
+ message: cfg.message,
203
+ ...cfg.default !== void 0 ? { default: cfg.default } : {},
204
+ ...cfg.validate !== void 0 ? { validate: cfg.validate } : {}
205
+ })
206
+ };
207
+ case "password": return {
208
+ success: true,
209
+ value: await prompts.password({
210
+ message: cfg.message,
211
+ ...cfg.mask !== void 0 ? { mask: cfg.mask } : {},
212
+ ...cfg.validate !== void 0 ? { validate: cfg.validate } : {}
213
+ })
214
+ };
215
+ case "editor": return {
216
+ success: true,
217
+ value: await prompts.editor({
218
+ message: cfg.message,
219
+ ...cfg.default !== void 0 ? { default: cfg.default } : {},
220
+ ...cfg.validate !== void 0 ? { validate: cfg.validate } : {}
221
+ })
222
+ };
223
+ case "select": return {
224
+ success: true,
225
+ value: await prompts.select({
226
+ message: cfg.message,
227
+ choices: normalizeChoices(cfg.choices),
228
+ ...cfg.default !== void 0 ? { default: cfg.default } : {}
229
+ })
230
+ };
231
+ case "rawlist": return {
232
+ success: true,
233
+ value: await prompts.rawlist({
234
+ message: cfg.message,
235
+ choices: normalizeChoices(cfg.choices),
236
+ ...cfg.default !== void 0 ? { default: cfg.default } : {}
237
+ })
238
+ };
239
+ case "expand": return {
240
+ success: true,
241
+ value: await prompts.expand({
242
+ message: cfg.message,
243
+ choices: cfg.choices,
244
+ ...cfg.default !== void 0 ? { default: cfg.default } : {}
245
+ })
246
+ };
247
+ case "checkbox": return {
248
+ success: true,
249
+ value: await prompts.checkbox({
250
+ message: cfg.message,
251
+ choices: normalizeChoices(cfg.choices)
252
+ })
253
+ };
254
+ }
255
+ } catch (error) {
256
+ if (isExitPromptError(error)) return {
257
+ success: false,
258
+ error: message`Prompt cancelled.`
137
259
  };
260
+ throw error;
138
261
  }
139
262
  }
140
- return {
263
+ async function executePrompt() {
264
+ const result = await executePromptRaw();
265
+ return validatePromptedValue(result);
266
+ }
267
+ function usePromptOrDefer(state, result) {
268
+ if (result.success) return Promise.resolve(result);
269
+ return shouldDeferPrompt(parser, state) ? Promise.resolve(deferredPromptResult()) : executePrompt();
270
+ }
271
+ const promptedParser = {
141
272
  $mode: "async",
142
273
  $valueType: parser.$valueType,
143
274
  $stateType: parser.$stateType,
144
275
  priority: parser.priority,
145
- usage: [{
276
+ [inheritParentAnnotationsKey]: true,
277
+ usage: parser.usage.length === 1 && parser.usage[0].type === "optional" ? parser.usage : [{
146
278
  type: "optional",
147
279
  terms: parser.usage
148
280
  }],
149
- initialState: promptBindInitialState,
281
+ get initialState() {
282
+ return new PromptBindInitialStateClass();
283
+ },
150
284
  parse: (context) => {
151
285
  const annotations = getAnnotations(context.state);
152
286
  const innerState = isPromptBindState(context.state) ? context.state.hasCliValue ? context.state.cliState : parser.initialState : context.state;
153
- const innerContext = innerState !== context.state ? {
287
+ const baseInnerContext = innerState !== context.state ? {
154
288
  ...context,
155
289
  state: innerState
156
290
  } : context;
@@ -181,32 +315,98 @@ function prompt(parser, config) {
181
315
  return {
182
316
  success: true,
183
317
  next: {
184
- ...innerContext,
318
+ ...baseInnerContext,
185
319
  state: nextState
186
320
  },
187
321
  consumed: []
188
322
  };
189
323
  };
190
- const result = parser.parse(innerContext);
324
+ const result = withAnnotatedInnerState(context.state, innerState, (annotatedInnerState) => {
325
+ const innerContext = annotatedInnerState !== context.state ? {
326
+ ...context,
327
+ state: annotatedInnerState
328
+ } : context;
329
+ return parser.parse(innerContext);
330
+ });
191
331
  if (result instanceof Promise) return result.then(processResult);
192
332
  return Promise.resolve(processResult(result));
193
333
  },
194
334
  complete: (state) => {
195
335
  if (isPromptBindState(state) && state.hasCliValue) {
196
- const r = parser.complete(state.cliState);
336
+ const r = withAnnotatedInnerState(state, state.cliState, (annotatedInnerState) => parser.complete(annotatedInnerState));
197
337
  if (r instanceof Promise) return r;
198
338
  return Promise.resolve(r);
199
339
  }
200
- if (state === promptBindInitialState) {
201
- if (promptCache !== null) {
202
- const cached = promptCache;
340
+ if (state instanceof PromptBindInitialStateClass) {
341
+ if (promptCache?.state === state) {
342
+ const cached = promptCache.result;
203
343
  promptCache = null;
204
344
  return cached;
205
345
  }
206
- promptCache = executePrompt();
207
- return promptCache;
346
+ const hasDeferHook = typeof parser.shouldDeferCompletion === "function";
347
+ const annotations = getAnnotations(state);
348
+ const innerInitialState = parser.initialState;
349
+ const effectiveInitialState = annotations != null && innerInitialState == null ? injectAnnotations(innerInitialState, annotations) : innerInitialState;
350
+ if (hasDeferHook) {
351
+ const annotatedR = withAnnotatedInnerState(state, effectiveInitialState, (annotatedInnerState) => parser.complete(annotatedInnerState));
352
+ const usePromptOrDeferSentinel = (res) => {
353
+ if (res.success && res.value === void 0) return usePromptOrDefer(state, {
354
+ success: false,
355
+ error: []
356
+ });
357
+ return usePromptOrDefer(state, res);
358
+ };
359
+ const cachedResult$1 = annotatedR instanceof Promise ? annotatedR.then(usePromptOrDeferSentinel) : usePromptOrDeferSentinel(annotatedR);
360
+ promptCache = {
361
+ state,
362
+ result: cachedResult$1
363
+ };
364
+ return cachedResult$1;
365
+ }
366
+ const simParseR = withAnnotatedInnerState(state, effectiveInitialState, (annotatedState) => parser.parse({
367
+ buffer: [],
368
+ state: annotatedState,
369
+ optionsTerminated: false,
370
+ usage: parser.usage
371
+ }));
372
+ const decideFromParse = (parseResult) => {
373
+ const consumed = parseResult.success ? parseResult.consumed.length : 0;
374
+ const cliState$1 = parseResult.success && consumed === 0 ? parseResult.next.state : void 0;
375
+ const hasSourceBindingMarker = (s) => s != null && typeof s === "object" && "hasCliValue" in s && Object.getOwnPropertySymbols(s).length > 0;
376
+ const cliStateIsPassthrough = cliState$1 != null && typeof cliState$1 === "object" && unwrapInjectedAnnotationWrapper(cliState$1) !== cliState$1;
377
+ const isSourceBinding = shouldAttemptInnerCompletion(cliState$1, state) && !cliStateIsPassthrough || hasSourceBindingMarker(cliState$1) || Array.isArray(cliState$1) && cliState$1.length === 1 && (hasSourceBindingMarker(cliState$1[0]) || typeof cliState$1[0] === "object" && cliState$1[0] != null && annotationKey in cliState$1[0]);
378
+ if (isSourceBinding) {
379
+ const cliStateIsInjected = cliState$1 != null && typeof cliState$1 === "object" && unwrapInjectedAnnotationWrapper(cliState$1) !== cliState$1;
380
+ const handleCompleteResult = (res) => {
381
+ if (res.success && res.value === void 0 && cliStateIsInjected) return executePrompt();
382
+ return usePromptOrDefer(state, res);
383
+ };
384
+ const completeState = parseResult.success ? parseResult.next.state : effectiveInitialState;
385
+ const completeR = parser.complete(completeState);
386
+ if (completeR instanceof Promise) return completeR.then(handleCompleteResult);
387
+ return handleCompleteResult(completeR);
388
+ }
389
+ return executePrompt();
390
+ };
391
+ const cachedResult = simParseR instanceof Promise ? simParseR.then(decideFromParse) : decideFromParse(simParseR);
392
+ promptCache = {
393
+ state,
394
+ result: cachedResult
395
+ };
396
+ return cachedResult;
397
+ }
398
+ const cliState = isPromptBindState(state) ? state.cliState : void 0;
399
+ const cliStateIsInjectedAnnotationWrapper = cliState != null && typeof cliState === "object" && unwrapInjectedAnnotationWrapper(cliState) !== cliState;
400
+ if (shouldAttemptInnerCompletion(cliState, state)) {
401
+ const useCompleteResultOrPrompt = (result) => {
402
+ if (result.success && result.value === void 0 && cliStateIsInjectedAnnotationWrapper) return executePrompt();
403
+ return usePromptOrDefer(state, result);
404
+ };
405
+ const r = withAnnotatedInnerState(state, cliState, (annotatedInnerState) => parser.complete(annotatedInnerState));
406
+ if (r instanceof Promise) return r.then(useCompleteResultOrPrompt);
407
+ return useCompleteResultOrPrompt(r);
208
408
  }
209
- return executePrompt();
409
+ return shouldDeferPrompt(parser, state) ? Promise.resolve(deferredPromptResult()) : executePrompt();
210
410
  },
211
411
  suggest: (context, prefix) => {
212
412
  const innerState = isPromptBindState(context.state) ? context.state.hasCliValue ? context.state.cliState : parser.initialState : context.state;
@@ -225,6 +425,7 @@ function prompt(parser, config) {
225
425
  return parser.getDocFragments(state, defaultValue);
226
426
  }
227
427
  };
428
+ return promptedParser;
228
429
  }
229
430
  /** Normalize choices to the format Inquirer.js expects. */
230
431
  function normalizeChoices(choices) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@optique/inquirer",
3
- "version": "1.0.0-dev.0",
3
+ "version": "1.0.0-dev.1116+6084dd3a",
4
4
  "description": "Interactive prompt support for Optique via Inquirer.js",
5
5
  "keywords": [
6
6
  "CLI",
@@ -56,12 +56,13 @@
56
56
  "sideEffects": false,
57
57
  "dependencies": {
58
58
  "@inquirer/prompts": "^8.3.0",
59
- "@optique/core": "1.0.0"
59
+ "@optique/core": "1.0.0-dev.1116+6084dd3a"
60
60
  },
61
61
  "devDependencies": {
62
62
  "@types/node": "^20.19.9",
63
63
  "tsdown": "^0.13.0",
64
- "typescript": "^5.8.3"
64
+ "typescript": "^5.8.3",
65
+ "@optique/env": "1.0.0-dev.1116+6084dd3a"
65
66
  },
66
67
  "scripts": {
67
68
  "build": "tsdown",