@optique/prompt 1.3.0-dev.2456 → 1.3.0-dev.2464

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
@@ -27,7 +27,8 @@ Documentation
27
27
 
28
28
  For full documentation, visit the [prompt integration docs].
29
29
  The guide covers dependency-derived configurations with
30
- `derivePromptConfig()` as well as adapter construction.
30
+ `derivePromptConfig()`, shared validation and retries, abort signals, and
31
+ adapter construction.
31
32
 
32
33
  [prompt integration docs]: https://optique.dev/integrations/prompt
33
34
 
package/dist/index.cjs CHANGED
@@ -179,6 +179,40 @@ function unwrapCompleteResult(result) {
179
179
  value: (0, __optique_core_extension.unwrapInjectedAnnotationState)(result.value)
180
180
  };
181
181
  }
182
+ function throwIfPromptAborted(signal) {
183
+ if (signal?.aborted === true) throw signal.reason;
184
+ }
185
+ function racePromptWorkWithAbort(signal, work) {
186
+ if (signal == null) try {
187
+ return work();
188
+ } catch (error) {
189
+ return Promise.reject(error);
190
+ }
191
+ if (signal.aborted) return Promise.reject(signal.reason);
192
+ return new Promise((resolve, reject) => {
193
+ const onAbort = () => {
194
+ signal.removeEventListener("abort", onAbort);
195
+ reject(signal.reason);
196
+ };
197
+ const cleanup = () => signal.removeEventListener("abort", onAbort);
198
+ signal.addEventListener("abort", onAbort, { once: true });
199
+ let outcome;
200
+ try {
201
+ outcome = work();
202
+ } catch (error) {
203
+ cleanup();
204
+ reject(error);
205
+ return;
206
+ }
207
+ outcome.then((value) => {
208
+ cleanup();
209
+ resolve(value);
210
+ }, (error) => {
211
+ cleanup();
212
+ reject(error);
213
+ });
214
+ });
215
+ }
182
216
  /**
183
217
  * Creates a `prompt()` parser wrapper for a prompt library adapter.
184
218
  *
@@ -190,13 +224,16 @@ function unwrapCompleteResult(result) {
190
224
  *
191
225
  * @typeParam TConfig Prompt configuration accepted by the adapter.
192
226
  * @param adapter Library-specific prompt executor.
193
- * @returns A `prompt(parser, config)` wrapper that always produces an async
194
- * parser. The configuration may be a static `TConfig` or a
227
+ * @returns A `prompt(parser, config, options?)` wrapper that always produces
228
+ * an async parser. The configuration may be a static `TConfig` or a
195
229
  * {@link DerivedPromptConfig} whose resolver returns `TConfig`.
230
+ * @throws {RangeError} If `maxAttempts` is not a positive integer.
196
231
  * @since 1.2.0
197
232
  */
198
233
  function createPromptAdapter(adapter) {
199
- return function prompt(parser, config) {
234
+ return function prompt(parser, config, options = {}) {
235
+ if (options.maxAttempts !== void 0 && (!Number.isInteger(options.maxAttempts) || options.maxAttempts < 1)) throw new RangeError("maxAttempts must be an integer greater than or equal to 1.");
236
+ const { validate, maxAttempts, signal } = options;
200
237
  const promptBindStateKey = Symbol("@optique/prompt/promptState");
201
238
  const completionCacheKeys = /* @__PURE__ */ new Map();
202
239
  const completionCacheSymbolIds = /* @__PURE__ */ new Map();
@@ -246,14 +283,36 @@ function createPromptAdapter(adapter) {
246
283
  success: true,
247
284
  value: config.otherwise
248
285
  };
249
- if (!isDerivedPromptConfig(config)) return adapter.execute(config);
250
- const source = promptedParser.dependencyMetadata?.source;
251
- const resolved = await resolveDerivedPromptConfig(config, exec, source?.sourceId, source?.metavar);
252
- if (!resolved.ok) return {
253
- success: false,
254
- error: resolved.error
255
- };
256
- return adapter.execute(resolved.config);
286
+ throwIfPromptAborted(signal);
287
+ let resolvedConfig;
288
+ if (!isDerivedPromptConfig(config)) resolvedConfig = config;
289
+ else {
290
+ const source = promptedParser.dependencyMetadata?.source;
291
+ const resolved = await resolveDerivedPromptConfig(config, exec, source?.sourceId, source?.metavar);
292
+ throwIfPromptAborted(signal);
293
+ if (!resolved.ok) return {
294
+ success: false,
295
+ error: resolved.error
296
+ };
297
+ resolvedConfig = resolved.config;
298
+ }
299
+ let previousValidationMessage;
300
+ for (let attempt = 1;; attempt++) {
301
+ const context = {
302
+ attempt,
303
+ ...previousValidationMessage === void 0 ? {} : { previousValidationMessage },
304
+ ...signal === void 0 ? {} : { signal }
305
+ };
306
+ const result = await racePromptWorkWithAbort(signal, () => adapter.execute(resolvedConfig, context));
307
+ if (!result.success || validate == null) return result;
308
+ const validationMessage = await racePromptWorkWithAbort(signal, () => Promise.resolve(validate(result.value)));
309
+ if (validationMessage === void 0) return result;
310
+ if (maxAttempts !== void 0 && attempt >= maxAttempts) return {
311
+ success: false,
312
+ error: validationMessage
313
+ };
314
+ previousValidationMessage = validationMessage;
315
+ }
257
316
  }
258
317
  const parserInheritsAnnotations = (0, __optique_core_extension.getTraits)(parser).inheritsAnnotations === true;
259
318
  const promptedParser = {
package/dist/index.d.cts CHANGED
@@ -1,4 +1,5 @@
1
1
  import { AnyDependencySource, DependencyValue, DependencyValues } from "@optique/core/dependency";
2
+ import { Message } from "@optique/core/message";
2
3
  import { FluentParser } from "@optique/core/fluent";
3
4
  import { Mode, Parser } from "@optique/core/parser";
4
5
  import { ValueParserResult } from "@optique/core/valueparser";
@@ -23,13 +24,63 @@ type PromptCondition<TValue> = {
23
24
  readonly when: () => boolean | Promise<boolean>;
24
25
  readonly otherwise: NoInfer<TValue>;
25
26
  };
27
+ /**
28
+ * Validates a value returned by a prompt adapter.
29
+ *
30
+ * Return `undefined` to accept the value, or a structured message to reject it
31
+ * and ask the adapter to try again. The validator receives the prompted value
32
+ * directly; it is not converted back into command-line input or passed through
33
+ * the wrapped value parser.
34
+ *
35
+ * @typeParam TValue Value type produced by the wrapped parser.
36
+ * @since 1.3.0
37
+ */
38
+ type PromptValidator<TValue> = (value: TValue) => Message | undefined | Promise<Message | undefined>;
39
+ /**
40
+ * Shared validation, retry, and cancellation options for a prompt fallback.
41
+ *
42
+ * These options are separate from adapter-native validation fields. They
43
+ * apply only when the interactive fallback runs; CLI values, source-bound
44
+ * values, and skipped runtime conditions do not consult them.
45
+ *
46
+ * @typeParam TValue Value type produced by the wrapped parser.
47
+ * @since 1.3.0
48
+ */
49
+ interface PromptOptions<TValue> {
50
+ /** Validator applied to each value returned by the adapter. */
51
+ readonly validate?: PromptValidator<TValue>;
52
+ /**
53
+ * Maximum number of adapter executions in one completion. Must be a
54
+ * positive integer. When omitted, validation may retry without a limit.
55
+ */
56
+ readonly maxAttempts?: number;
57
+ /**
58
+ * Signal that stops the active adapter execution or validator. Its reason
59
+ * is propagated to the caller without becoming a parse failure.
60
+ */
61
+ readonly signal?: AbortSignal;
62
+ }
63
+ /**
64
+ * Context passed to one execution of a prompt adapter.
65
+ *
66
+ * @since 1.3.0
67
+ */
68
+ interface PromptExecutionContext {
69
+ /** One-based number of the current adapter execution. */
70
+ readonly attempt: number;
71
+ /** Message returned by the validator after the preceding execution. */
72
+ readonly previousValidationMessage?: Message;
73
+ /** Signal supplied through the prompt's shared options, when present. */
74
+ readonly signal?: AbortSignal;
75
+ }
26
76
  /**
27
77
  * Prompt adapter used by {@link createPromptAdapter}.
28
78
  *
29
79
  * The adapter owns library-specific prompt execution and maps the result into
30
80
  * Optique's value-parser result shape. The shared parser wrapping behavior,
31
81
  * including CLI priority, source bindings, deferred completion, suggestions,
32
- * and usage metadata, is handled by *@optique/prompt*.
82
+ * usage metadata, validation retries, and abort handling, is handled by
83
+ * *@optique/prompt*.
33
84
  *
34
85
  * @typeParam TConfig Prompt configuration accepted by the adapter.
35
86
  * @since 1.2.0
@@ -41,10 +92,12 @@ interface PromptAdapter<TConfig> {
41
92
  * @typeParam TValue Value type produced by the wrapped parser.
42
93
  * @param config Prompt configuration supplied to the generated `prompt()`
43
94
  * wrapper.
95
+ * @param context Attempt number, preceding validation message, and optional
96
+ * abort signal for this execution.
44
97
  * @returns The prompted value or a prompt failure.
45
98
  * @throws Any unexpected prompt execution failure.
46
99
  */
47
- readonly execute: <TValue>(config: TConfig) => Promise<ValueParserResult<TValue>>;
100
+ readonly execute: <TValue>(config: TConfig, context: PromptExecutionContext) => Promise<ValueParserResult<TValue>>;
48
101
  /**
49
102
  * Returns a default value from the prompt config for documentation purposes.
50
103
  *
@@ -238,11 +291,12 @@ type PromptConfigInput<TConfig, TValue> = (TConfig & PromptCondition<TValue>) |
238
291
  *
239
292
  * @typeParam TConfig Prompt configuration accepted by the adapter.
240
293
  * @param adapter Library-specific prompt executor.
241
- * @returns A `prompt(parser, config)` wrapper that always produces an async
242
- * parser. The configuration may be a static `TConfig` or a
294
+ * @returns A `prompt(parser, config, options?)` wrapper that always produces
295
+ * an async parser. The configuration may be a static `TConfig` or a
243
296
  * {@link DerivedPromptConfig} whose resolver returns `TConfig`.
297
+ * @throws {RangeError} If `maxAttempts` is not a positive integer.
244
298
  * @since 1.2.0
245
299
  */
246
- declare function createPromptAdapter<TConfig>(adapter: PromptAdapter<TConfig>): <M extends Mode, TValue, TState>(parser: Parser<M, TValue, TState>, config: PromptConfigInput<TConfig, TValue>) => FluentParser<"async", TValue, TState>;
300
+ declare function createPromptAdapter<TConfig>(adapter: PromptAdapter<TConfig>): <M extends Mode, TValue, TState>(parser: Parser<M, TValue, TState>, config: PromptConfigInput<TConfig, TValue>, options?: PromptOptions<NoInfer<TValue>>) => FluentParser<"async", TValue, TState>;
247
301
  //#endregion
248
- export { DerivePromptConfigContext, DerivePromptConfigOptions, DerivePromptConfigsContext, DerivePromptConfigsOptions, DerivedPromptConfig, PromptAdapter, PromptCondition, PromptConfigInput, createPromptAdapter, derivePromptConfig, isDerivedPromptConfig };
302
+ export { DerivePromptConfigContext, DerivePromptConfigOptions, DerivePromptConfigsContext, DerivePromptConfigsOptions, DerivedPromptConfig, PromptAdapter, PromptCondition, PromptConfigInput, PromptExecutionContext, PromptOptions, PromptValidator, createPromptAdapter, derivePromptConfig, isDerivedPromptConfig };
package/dist/index.d.ts CHANGED
@@ -1,4 +1,5 @@
1
1
  import { AnyDependencySource, DependencyValue, DependencyValues } from "@optique/core/dependency";
2
+ import { Message } from "@optique/core/message";
2
3
  import { FluentParser } from "@optique/core/fluent";
3
4
  import { Mode, Parser } from "@optique/core/parser";
4
5
  import { ValueParserResult } from "@optique/core/valueparser";
@@ -23,13 +24,63 @@ type PromptCondition<TValue> = {
23
24
  readonly when: () => boolean | Promise<boolean>;
24
25
  readonly otherwise: NoInfer<TValue>;
25
26
  };
27
+ /**
28
+ * Validates a value returned by a prompt adapter.
29
+ *
30
+ * Return `undefined` to accept the value, or a structured message to reject it
31
+ * and ask the adapter to try again. The validator receives the prompted value
32
+ * directly; it is not converted back into command-line input or passed through
33
+ * the wrapped value parser.
34
+ *
35
+ * @typeParam TValue Value type produced by the wrapped parser.
36
+ * @since 1.3.0
37
+ */
38
+ type PromptValidator<TValue> = (value: TValue) => Message | undefined | Promise<Message | undefined>;
39
+ /**
40
+ * Shared validation, retry, and cancellation options for a prompt fallback.
41
+ *
42
+ * These options are separate from adapter-native validation fields. They
43
+ * apply only when the interactive fallback runs; CLI values, source-bound
44
+ * values, and skipped runtime conditions do not consult them.
45
+ *
46
+ * @typeParam TValue Value type produced by the wrapped parser.
47
+ * @since 1.3.0
48
+ */
49
+ interface PromptOptions<TValue> {
50
+ /** Validator applied to each value returned by the adapter. */
51
+ readonly validate?: PromptValidator<TValue>;
52
+ /**
53
+ * Maximum number of adapter executions in one completion. Must be a
54
+ * positive integer. When omitted, validation may retry without a limit.
55
+ */
56
+ readonly maxAttempts?: number;
57
+ /**
58
+ * Signal that stops the active adapter execution or validator. Its reason
59
+ * is propagated to the caller without becoming a parse failure.
60
+ */
61
+ readonly signal?: AbortSignal;
62
+ }
63
+ /**
64
+ * Context passed to one execution of a prompt adapter.
65
+ *
66
+ * @since 1.3.0
67
+ */
68
+ interface PromptExecutionContext {
69
+ /** One-based number of the current adapter execution. */
70
+ readonly attempt: number;
71
+ /** Message returned by the validator after the preceding execution. */
72
+ readonly previousValidationMessage?: Message;
73
+ /** Signal supplied through the prompt's shared options, when present. */
74
+ readonly signal?: AbortSignal;
75
+ }
26
76
  /**
27
77
  * Prompt adapter used by {@link createPromptAdapter}.
28
78
  *
29
79
  * The adapter owns library-specific prompt execution and maps the result into
30
80
  * Optique's value-parser result shape. The shared parser wrapping behavior,
31
81
  * including CLI priority, source bindings, deferred completion, suggestions,
32
- * and usage metadata, is handled by *@optique/prompt*.
82
+ * usage metadata, validation retries, and abort handling, is handled by
83
+ * *@optique/prompt*.
33
84
  *
34
85
  * @typeParam TConfig Prompt configuration accepted by the adapter.
35
86
  * @since 1.2.0
@@ -41,10 +92,12 @@ interface PromptAdapter<TConfig> {
41
92
  * @typeParam TValue Value type produced by the wrapped parser.
42
93
  * @param config Prompt configuration supplied to the generated `prompt()`
43
94
  * wrapper.
95
+ * @param context Attempt number, preceding validation message, and optional
96
+ * abort signal for this execution.
44
97
  * @returns The prompted value or a prompt failure.
45
98
  * @throws Any unexpected prompt execution failure.
46
99
  */
47
- readonly execute: <TValue>(config: TConfig) => Promise<ValueParserResult<TValue>>;
100
+ readonly execute: <TValue>(config: TConfig, context: PromptExecutionContext) => Promise<ValueParserResult<TValue>>;
48
101
  /**
49
102
  * Returns a default value from the prompt config for documentation purposes.
50
103
  *
@@ -238,11 +291,12 @@ type PromptConfigInput<TConfig, TValue> = (TConfig & PromptCondition<TValue>) |
238
291
  *
239
292
  * @typeParam TConfig Prompt configuration accepted by the adapter.
240
293
  * @param adapter Library-specific prompt executor.
241
- * @returns A `prompt(parser, config)` wrapper that always produces an async
242
- * parser. The configuration may be a static `TConfig` or a
294
+ * @returns A `prompt(parser, config, options?)` wrapper that always produces
295
+ * an async parser. The configuration may be a static `TConfig` or a
243
296
  * {@link DerivedPromptConfig} whose resolver returns `TConfig`.
297
+ * @throws {RangeError} If `maxAttempts` is not a positive integer.
244
298
  * @since 1.2.0
245
299
  */
246
- declare function createPromptAdapter<TConfig>(adapter: PromptAdapter<TConfig>): <M extends Mode, TValue, TState>(parser: Parser<M, TValue, TState>, config: PromptConfigInput<TConfig, TValue>) => FluentParser<"async", TValue, TState>;
300
+ declare function createPromptAdapter<TConfig>(adapter: PromptAdapter<TConfig>): <M extends Mode, TValue, TState>(parser: Parser<M, TValue, TState>, config: PromptConfigInput<TConfig, TValue>, options?: PromptOptions<NoInfer<TValue>>) => FluentParser<"async", TValue, TState>;
247
301
  //#endregion
248
- export { DerivePromptConfigContext, DerivePromptConfigOptions, DerivePromptConfigsContext, DerivePromptConfigsOptions, DerivedPromptConfig, PromptAdapter, PromptCondition, PromptConfigInput, createPromptAdapter, derivePromptConfig, isDerivedPromptConfig };
302
+ export { DerivePromptConfigContext, DerivePromptConfigOptions, DerivePromptConfigsContext, DerivePromptConfigsOptions, DerivedPromptConfig, PromptAdapter, PromptCondition, PromptConfigInput, PromptExecutionContext, PromptOptions, PromptValidator, createPromptAdapter, derivePromptConfig, isDerivedPromptConfig };
package/dist/index.js CHANGED
@@ -156,6 +156,40 @@ function unwrapCompleteResult(result) {
156
156
  value: unwrapInjectedAnnotationState(result.value)
157
157
  };
158
158
  }
159
+ function throwIfPromptAborted(signal) {
160
+ if (signal?.aborted === true) throw signal.reason;
161
+ }
162
+ function racePromptWorkWithAbort(signal, work) {
163
+ if (signal == null) try {
164
+ return work();
165
+ } catch (error) {
166
+ return Promise.reject(error);
167
+ }
168
+ if (signal.aborted) return Promise.reject(signal.reason);
169
+ return new Promise((resolve, reject) => {
170
+ const onAbort = () => {
171
+ signal.removeEventListener("abort", onAbort);
172
+ reject(signal.reason);
173
+ };
174
+ const cleanup = () => signal.removeEventListener("abort", onAbort);
175
+ signal.addEventListener("abort", onAbort, { once: true });
176
+ let outcome;
177
+ try {
178
+ outcome = work();
179
+ } catch (error) {
180
+ cleanup();
181
+ reject(error);
182
+ return;
183
+ }
184
+ outcome.then((value) => {
185
+ cleanup();
186
+ resolve(value);
187
+ }, (error) => {
188
+ cleanup();
189
+ reject(error);
190
+ });
191
+ });
192
+ }
159
193
  /**
160
194
  * Creates a `prompt()` parser wrapper for a prompt library adapter.
161
195
  *
@@ -167,13 +201,16 @@ function unwrapCompleteResult(result) {
167
201
  *
168
202
  * @typeParam TConfig Prompt configuration accepted by the adapter.
169
203
  * @param adapter Library-specific prompt executor.
170
- * @returns A `prompt(parser, config)` wrapper that always produces an async
171
- * parser. The configuration may be a static `TConfig` or a
204
+ * @returns A `prompt(parser, config, options?)` wrapper that always produces
205
+ * an async parser. The configuration may be a static `TConfig` or a
172
206
  * {@link DerivedPromptConfig} whose resolver returns `TConfig`.
207
+ * @throws {RangeError} If `maxAttempts` is not a positive integer.
173
208
  * @since 1.2.0
174
209
  */
175
210
  function createPromptAdapter(adapter) {
176
- return function prompt(parser, config) {
211
+ return function prompt(parser, config, options = {}) {
212
+ if (options.maxAttempts !== void 0 && (!Number.isInteger(options.maxAttempts) || options.maxAttempts < 1)) throw new RangeError("maxAttempts must be an integer greater than or equal to 1.");
213
+ const { validate, maxAttempts, signal } = options;
177
214
  const promptBindStateKey = Symbol("@optique/prompt/promptState");
178
215
  const completionCacheKeys = /* @__PURE__ */ new Map();
179
216
  const completionCacheSymbolIds = /* @__PURE__ */ new Map();
@@ -223,14 +260,36 @@ function createPromptAdapter(adapter) {
223
260
  success: true,
224
261
  value: config.otherwise
225
262
  };
226
- if (!isDerivedPromptConfig(config)) return adapter.execute(config);
227
- const source = promptedParser.dependencyMetadata?.source;
228
- const resolved = await resolveDerivedPromptConfig(config, exec, source?.sourceId, source?.metavar);
229
- if (!resolved.ok) return {
230
- success: false,
231
- error: resolved.error
232
- };
233
- return adapter.execute(resolved.config);
263
+ throwIfPromptAborted(signal);
264
+ let resolvedConfig;
265
+ if (!isDerivedPromptConfig(config)) resolvedConfig = config;
266
+ else {
267
+ const source = promptedParser.dependencyMetadata?.source;
268
+ const resolved = await resolveDerivedPromptConfig(config, exec, source?.sourceId, source?.metavar);
269
+ throwIfPromptAborted(signal);
270
+ if (!resolved.ok) return {
271
+ success: false,
272
+ error: resolved.error
273
+ };
274
+ resolvedConfig = resolved.config;
275
+ }
276
+ let previousValidationMessage;
277
+ for (let attempt = 1;; attempt++) {
278
+ const context = {
279
+ attempt,
280
+ ...previousValidationMessage === void 0 ? {} : { previousValidationMessage },
281
+ ...signal === void 0 ? {} : { signal }
282
+ };
283
+ const result = await racePromptWorkWithAbort(signal, () => adapter.execute(resolvedConfig, context));
284
+ if (!result.success || validate == null) return result;
285
+ const validationMessage = await racePromptWorkWithAbort(signal, () => Promise.resolve(validate(result.value)));
286
+ if (validationMessage === void 0) return result;
287
+ if (maxAttempts !== void 0 && attempt >= maxAttempts) return {
288
+ success: false,
289
+ error: validationMessage
290
+ };
291
+ previousValidationMessage = validationMessage;
292
+ }
234
293
  }
235
294
  const parserInheritsAnnotations = getTraits(parser).inheritsAnnotations === true;
236
295
  const promptedParser = {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@optique/prompt",
3
- "version": "1.3.0-dev.2456",
3
+ "version": "1.3.0-dev.2464",
4
4
  "description": "Generic prompt adapter support for Optique",
5
5
  "keywords": [
6
6
  "CLI",
@@ -60,12 +60,12 @@
60
60
  },
61
61
  "sideEffects": false,
62
62
  "dependencies": {
63
- "@optique/core": "1.3.0-dev.2456+d939eb0a"
63
+ "@optique/core": "1.3.0-dev.2464+e4795e6e"
64
64
  },
65
65
  "devDependencies": {
66
- "@optique/config": "1.3.0-dev.2456+d939eb0a",
67
- "@optique/env": "1.3.0-dev.2456+d939eb0a",
68
- "@optique/run": "1.3.0-dev.2456+d939eb0a",
66
+ "@optique/config": "1.3.0-dev.2464+e4795e6e",
67
+ "@optique/env": "1.3.0-dev.2464+e4795e6e",
68
+ "@optique/run": "1.3.0-dev.2464+e4795e6e",
69
69
  "@types/node": "^24.0.0",
70
70
  "fast-check": "^4.7.0",
71
71
  "tsdown": "^0.13.0",