cli-forge 1.2.2 → 1.3.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.
Files changed (36) hide show
  1. package/dist/bin/commands/generate-documentation.js +1 -13
  2. package/dist/bin/commands/generate-documentation.js.map +1 -1
  3. package/dist/index.d.ts +1 -0
  4. package/dist/index.js.map +1 -1
  5. package/dist/lib/internal-cli.d.ts +33 -2
  6. package/dist/lib/internal-cli.js +90 -4
  7. package/dist/lib/internal-cli.js.map +1 -1
  8. package/dist/lib/prompt-types.d.ts +44 -0
  9. package/dist/lib/prompt-types.js +3 -0
  10. package/dist/lib/prompt-types.js.map +1 -0
  11. package/dist/lib/public-api.d.ts +45 -12
  12. package/dist/lib/public-api.js.map +1 -1
  13. package/dist/lib/resolve-prompts.d.ts +13 -0
  14. package/dist/lib/resolve-prompts.js +121 -0
  15. package/dist/lib/resolve-prompts.js.map +1 -0
  16. package/dist/lib/test-harness.js +1 -1
  17. package/dist/lib/test-harness.js.map +1 -1
  18. package/dist/prompt-providers/clack.d.ts +29 -0
  19. package/dist/prompt-providers/clack.js +136 -0
  20. package/dist/prompt-providers/clack.js.map +1 -0
  21. package/package.json +11 -2
  22. package/src/bin/commands/generate-documentation.ts +1 -13
  23. package/src/index.ts +1 -0
  24. package/src/lib/composable-builder.ts +3 -3
  25. package/src/lib/internal-cli.spec.ts +300 -0
  26. package/src/lib/internal-cli.ts +117 -9
  27. package/src/lib/prompt-types.ts +48 -0
  28. package/src/lib/public-api.ts +31 -19
  29. package/src/lib/resolve-prompts.spec.ts +311 -0
  30. package/src/lib/resolve-prompts.ts +156 -0
  31. package/src/lib/test-harness.ts +1 -1
  32. package/src/prompt-providers/clack.spec.ts +376 -0
  33. package/src/prompt-providers/clack.ts +169 -0
  34. package/tsconfig.lib.json.tsbuildinfo +1 -1
  35. package/typedoc.json +10 -0
  36. package/.eslintrc.json +0 -36
@@ -1,6 +1,7 @@
1
1
  import { afterEach, describe, expect, it } from 'vitest';
2
2
  import { InternalCLI } from './internal-cli';
3
3
  import { cli } from './public-api';
4
+ import type { PromptProvider } from './prompt-types';
4
5
 
5
6
  const ORIGINAL_CONSOLE_LOG = console.log;
6
7
 
@@ -1250,4 +1251,303 @@ describe('cliForge', () => {
1250
1251
  expect(result.watch).toBe(true);
1251
1252
  });
1252
1253
  });
1254
+
1255
+ describe('prompt providers', () => {
1256
+ it('should register a prompt provider via withPromptProvider', () => {
1257
+ const provider: PromptProvider = {
1258
+ prompt: async () => 'test-value',
1259
+ };
1260
+ const app = cli('test').withPromptProvider(provider);
1261
+ // Should return CLI for chaining
1262
+ expect(app).toBeDefined();
1263
+ });
1264
+
1265
+ it('should throw if provider has neither prompt nor promptBatch', () => {
1266
+ expect(() => {
1267
+ cli('test').withPromptProvider({} as any);
1268
+ }).toThrow(/must implement at least one of/);
1269
+ });
1270
+
1271
+ it('should store prompt config from option registration', () => {
1272
+ const app = cli('test')
1273
+ .option('name', { type: 'string', prompt: true })
1274
+ .option('age', { type: 'number', prompt: 'How old are you?' })
1275
+ .option('debug', { type: 'boolean' });
1276
+
1277
+ const internal = app as unknown as InternalCLI;
1278
+ expect(internal.promptConfigs.get('name')).toBe(true);
1279
+ expect(internal.promptConfigs.get('age')).toBe('How old are you?');
1280
+ expect(internal.promptConfigs.has('debug')).toBe(false);
1281
+ });
1282
+
1283
+ it('should store prompt config from positional registration', () => {
1284
+ const app = cli('test')
1285
+ .positional('file', { type: 'string', prompt: 'Which file?' });
1286
+
1287
+ const internal = app as unknown as InternalCLI;
1288
+ expect(internal.promptConfigs.get('file')).toBe('Which file?');
1289
+ });
1290
+
1291
+ it('should not store prompt config when prompt is not provided', () => {
1292
+ const app = cli('test')
1293
+ .option('name', { type: 'string' });
1294
+
1295
+ const internal = app as unknown as InternalCLI;
1296
+ expect(internal.promptConfigs.size).toBe(0);
1297
+ });
1298
+
1299
+ it('should store prompt callback config', () => {
1300
+ const promptFn = () => 'Enter value';
1301
+ const app = cli('test')
1302
+ .option('token', { type: 'string', prompt: promptFn });
1303
+
1304
+ const internal = app as unknown as InternalCLI;
1305
+ expect(internal.promptConfigs.get('token')).toBe(promptFn);
1306
+ });
1307
+
1308
+ it('should propagate prompt providers to subcommands', async () => {
1309
+ const prompted: string[] = [];
1310
+ const provider: PromptProvider = {
1311
+ prompt: async (option) => {
1312
+ prompted.push(option.name);
1313
+ return 'value';
1314
+ },
1315
+ };
1316
+
1317
+ const app = cli('test')
1318
+ .withPromptProvider(provider)
1319
+ .command('sub', {
1320
+ builder: (cmd) =>
1321
+ cmd.option('name', { type: 'string', required: true }),
1322
+ handler: () => {},
1323
+ });
1324
+
1325
+ await app.forge(['sub']);
1326
+ expect(prompted).toContain('name');
1327
+ });
1328
+ });
1329
+
1330
+ describe('prompt resolution in forge', () => {
1331
+ it('should prompt for required options with no value when provider exists', async () => {
1332
+ const prompted: string[] = [];
1333
+ const provider: PromptProvider = {
1334
+ prompt: async (option) => {
1335
+ prompted.push(option.name);
1336
+ return option.name === 'name' ? 'Alice' : 42;
1337
+ },
1338
+ };
1339
+
1340
+ const app = cli('test', {
1341
+ handler: () => {},
1342
+ })
1343
+ .option('name', { type: 'string', required: true })
1344
+ .option('age', { type: 'number', required: true })
1345
+ .withPromptProvider(provider);
1346
+
1347
+ await app.forge([]);
1348
+ expect(prompted).toContain('name');
1349
+ expect(prompted).toContain('age');
1350
+ });
1351
+
1352
+ it('should not prompt for options with values already provided', async () => {
1353
+ const prompted: string[] = [];
1354
+ const provider: PromptProvider = {
1355
+ prompt: async (option) => {
1356
+ prompted.push(option.name);
1357
+ return 'value';
1358
+ },
1359
+ };
1360
+
1361
+ const app = cli('test', {
1362
+ handler: () => {},
1363
+ })
1364
+ .option('name', { type: 'string', required: true })
1365
+ .withPromptProvider(provider);
1366
+
1367
+ await app.forge(['--name', 'Bob']);
1368
+ expect(prompted).not.toContain('name');
1369
+ });
1370
+
1371
+ it('should prompt when prompt is true even if not required', async () => {
1372
+ const prompted: string[] = [];
1373
+ const provider: PromptProvider = {
1374
+ prompt: async (option) => {
1375
+ prompted.push(option.name);
1376
+ return 'value';
1377
+ },
1378
+ };
1379
+
1380
+ const app = cli('test', {
1381
+ handler: () => {},
1382
+ })
1383
+ .option('name', { type: 'string', prompt: true })
1384
+ .withPromptProvider(provider);
1385
+
1386
+ await app.forge([]);
1387
+ expect(prompted).toContain('name');
1388
+ });
1389
+
1390
+ it('should still prompt when prompt is true even if value already provided', async () => {
1391
+ const prompted: string[] = [];
1392
+ let handlerArgs: any;
1393
+ const provider: PromptProvider = {
1394
+ prompt: async (option) => {
1395
+ prompted.push(option.name);
1396
+ return 'prompted-value';
1397
+ },
1398
+ };
1399
+
1400
+ const app = cli('test', {
1401
+ handler: (args) => {
1402
+ handlerArgs = args;
1403
+ },
1404
+ })
1405
+ .option('name', { type: 'string', prompt: true })
1406
+ .withPromptProvider(provider);
1407
+
1408
+ await app.forge(['--name', 'cli-value']);
1409
+ expect(prompted).toContain('name');
1410
+ expect(handlerArgs.name).toBe('prompted-value');
1411
+ });
1412
+
1413
+ it('should not prompt when prompt is false even if required', async () => {
1414
+ const prompted: string[] = [];
1415
+ const provider: PromptProvider = {
1416
+ prompt: async (option) => {
1417
+ prompted.push(option.name);
1418
+ return 'value';
1419
+ },
1420
+ };
1421
+
1422
+ const app = cli('test', {
1423
+ handler: () => {},
1424
+ })
1425
+ .option('name', { type: 'string', required: true, prompt: false })
1426
+ .withPromptProvider(provider);
1427
+
1428
+ // This will throw due to required validation, but should not prompt
1429
+ await expect(app.forge([])).rejects.toThrow();
1430
+ expect(prompted).not.toContain('name');
1431
+ });
1432
+
1433
+ it('should use prompt callback to resolve config', async () => {
1434
+ const prompted: string[] = [];
1435
+ const provider: PromptProvider = {
1436
+ prompt: async (option) => {
1437
+ prompted.push(option.name);
1438
+ return 'value';
1439
+ },
1440
+ };
1441
+
1442
+ const app = cli('test', {
1443
+ handler: () => {},
1444
+ })
1445
+ .option('token', {
1446
+ type: 'string',
1447
+ prompt: (args: any) => (args.authFile ? false : 'Enter token'),
1448
+ })
1449
+ .withPromptProvider(provider);
1450
+
1451
+ await app.forge([]);
1452
+ expect(prompted).toContain('token');
1453
+ });
1454
+
1455
+ it('should throw when prompting needed but no provider registered', async () => {
1456
+ const app = cli('test', {
1457
+ handler: () => {},
1458
+ })
1459
+ .option('name', { type: 'string', prompt: true });
1460
+
1461
+ await expect(app.forge([])).rejects.toThrow(/no prompt provider/i);
1462
+ });
1463
+
1464
+ it('should use filtered providers before fallback providers', async () => {
1465
+ const calls: Array<{ provider: string; option: string }> = [];
1466
+ const filteredProvider: PromptProvider = {
1467
+ filter: (name) => name === 'secret',
1468
+ prompt: async (option) => {
1469
+ calls.push({ provider: 'filtered', option: option.name });
1470
+ return 'secret-value';
1471
+ },
1472
+ };
1473
+ const fallbackProvider: PromptProvider = {
1474
+ prompt: async (option) => {
1475
+ calls.push({ provider: 'fallback', option: option.name });
1476
+ return 'fallback-value';
1477
+ },
1478
+ };
1479
+
1480
+ const app = cli('test', {
1481
+ handler: () => {},
1482
+ })
1483
+ .option('name', { type: 'string', prompt: true })
1484
+ .option('secret', { type: 'string', prompt: true })
1485
+ .withPromptProvider(filteredProvider)
1486
+ .withPromptProvider(fallbackProvider);
1487
+
1488
+ await app.forge([]);
1489
+ expect(calls).toContainEqual({ provider: 'filtered', option: 'secret' });
1490
+ expect(calls).toContainEqual({ provider: 'fallback', option: 'name' });
1491
+ });
1492
+
1493
+ it('should prefer promptBatch over prompt when available', async () => {
1494
+ let batchCalled = false;
1495
+ const provider: PromptProvider = {
1496
+ promptBatch: async (options) => {
1497
+ batchCalled = true;
1498
+ const result: Record<string, unknown> = {};
1499
+ for (const opt of options) {
1500
+ result[opt.name] = 'batch-value';
1501
+ }
1502
+ return result;
1503
+ },
1504
+ prompt: async () => {
1505
+ throw new Error('Should not be called when promptBatch exists');
1506
+ },
1507
+ };
1508
+
1509
+ const app = cli('test', {
1510
+ handler: () => {},
1511
+ })
1512
+ .option('a', { type: 'string', prompt: true })
1513
+ .option('b', { type: 'string', prompt: true })
1514
+ .withPromptProvider(provider);
1515
+
1516
+ await app.forge([]);
1517
+ expect(batchCalled).toBe(true);
1518
+ });
1519
+
1520
+ it('should prompt, inject values, and pass validation', async () => {
1521
+ let handlerArgs: any;
1522
+ const provider: PromptProvider = {
1523
+ promptBatch: async (options) => {
1524
+ const results: Record<string, unknown> = {};
1525
+ for (const opt of options) {
1526
+ if (opt.config.type === 'number') {
1527
+ results[opt.name] = 42;
1528
+ } else {
1529
+ results[opt.name] = 'prompted-' + opt.name;
1530
+ }
1531
+ }
1532
+ return results;
1533
+ },
1534
+ };
1535
+
1536
+ const app = cli('test', {
1537
+ handler: (args) => {
1538
+ handlerArgs = args;
1539
+ },
1540
+ })
1541
+ .option('name', { type: 'string', required: true })
1542
+ .option('port', { type: 'number', prompt: 'Which port?' })
1543
+ .option('verbose', { type: 'boolean', default: false })
1544
+ .withPromptProvider(provider);
1545
+
1546
+ await app.forge([]);
1547
+
1548
+ expect(handlerArgs.name).toBe('prompted-name');
1549
+ expect(handlerArgs.port).toBe(42);
1550
+ expect(handlerArgs.verbose).toBe(false); // default, not prompted
1551
+ });
1552
+ });
1253
1553
  });
@@ -1,4 +1,4 @@
1
- /* eslint-disable @typescript-eslint/ban-types */
1
+ /* eslint-disable @typescript-eslint/no-empty-object-type */
2
2
  import {
3
3
  ArgvParser,
4
4
  EnvOptionConfig,
@@ -22,6 +22,8 @@ import {
22
22
  ErrorHandler,
23
23
  SDKCommand,
24
24
  } from './public-api';
25
+ import type { PromptProvider, PromptOptionConfig } from './prompt-types';
26
+ import { resolvePrompts } from './resolve-prompts';
25
27
  import { getCallingFile, getParentPackageJson } from './utils';
26
28
 
27
29
  /**
@@ -43,14 +45,43 @@ import { getCallingFile, getParentPackageJson } from './utils';
43
45
  * }).forge();
44
46
  * ```
45
47
  */
48
+ /**
49
+ * Cross-realm brand symbol used to identify InternalCLI instances across
50
+ * different copies of the cli-forge package (e.g. when pnpm resolves
51
+ * multiple copies due to differing peer dependencies). `Symbol.for()`
52
+ * returns the same symbol globally, so the brand check works even when
53
+ * `instanceof` would fail.
54
+ */
55
+ const CLI_FORGE_BRAND = Symbol.for('cli-forge:InternalCLI');
56
+
46
57
  export class InternalCLI<
47
58
  TArgs extends ParsedArgs = ParsedArgs,
48
59
  THandlerReturn = void,
49
- // eslint-disable-next-line @typescript-eslint/ban-types
60
+ // eslint-disable-next-line @typescript-eslint/no-empty-object-type
50
61
  TChildren = {},
51
62
  TParent = undefined
52
63
  > implements CLI<TArgs, THandlerReturn, TChildren, TParent>
53
64
  {
65
+ /**
66
+ * Cross-realm brand for identifying InternalCLI instances across
67
+ * different package copies. See {@link CLI_FORGE_BRAND}.
68
+ */
69
+ readonly [CLI_FORGE_BRAND] = true;
70
+
71
+ /**
72
+ * Check whether `obj` is an InternalCLI instance, even when it was
73
+ * created by a different copy of the cli-forge package.
74
+ */
75
+ static isInternalCLI(
76
+ obj: unknown
77
+ ): obj is InternalCLI<any, any, any, any> {
78
+ return (
79
+ obj != null &&
80
+ typeof obj === 'object' &&
81
+ CLI_FORGE_BRAND in obj
82
+ );
83
+ }
84
+
54
85
  /**
55
86
  * For internal use only. Stick to properties available on {@link CLI}.
56
87
  */
@@ -93,6 +124,14 @@ export class InternalCLI<
93
124
  (cli: any, args: TArgs) => Promise<void> | void
94
125
  > = [];
95
126
 
127
+ registeredPromptProviders: PromptProvider[] = [];
128
+
129
+ /**
130
+ * Stores prompt config for each option, keyed by option name.
131
+ * Set when .option() is called with a `prompt` property.
132
+ */
133
+ promptConfigs: Map<string, PromptOptionConfig<any>> = new Map();
134
+
96
135
  /**
97
136
  * Set when a `$0` alias replaces the root builder via `.command()`.
98
137
  * The $0 builder should only run if no explicit subcommand is given,
@@ -258,7 +297,7 @@ export class InternalCLI<
258
297
  CLI<TArgs, THandlerReturn, TChildren, TParent>
259
298
  >;
260
299
  }
261
- : // eslint-disable-next-line @typescript-eslint/ban-types
300
+ : // eslint-disable-next-line @typescript-eslint/no-empty-object-type
262
301
  {}),
263
302
  TParent
264
303
  > {
@@ -304,10 +343,16 @@ export class InternalCLI<
304
343
  this.registeredCommands[alias] = cmd;
305
344
  }
306
345
  }
307
- } else if (keyOrCommand instanceof InternalCLI) {
308
- const cmd = keyOrCommand;
346
+ } else if (InternalCLI.isInternalCLI(keyOrCommand)) {
347
+ const cmd = keyOrCommand as InternalCLI<any, any, any, any>;
309
348
  if (cmd.name === '$0') {
310
349
  this.withRootCommandConfiguration(cmd.configuration as any);
350
+ // Copy any commands registered on the $0 instance (e.g. subcommands
351
+ // added via `.commands()` after the $0 CLI was created).
352
+ for (const [key, subcmd] of Object.entries(cmd.registeredCommands)) {
353
+ subcmd._parent = this;
354
+ this.registeredCommands[key] = subcmd;
355
+ }
311
356
  return this as any;
312
357
  }
313
358
  cmd._parent = this;
@@ -337,8 +382,12 @@ export class InternalCLI<
337
382
  option<
338
383
  TOption extends string,
339
384
  const TOptionConfig extends OptionConfig<any, any, any>
340
- >(name: TOption, config: TOptionConfig) {
341
- this.parser.option(name, config);
385
+ >(name: TOption, config: TOptionConfig & { prompt?: PromptOptionConfig<TArgs> }) {
386
+ const { prompt, ...parserConfig } = config;
387
+ if (prompt !== undefined) {
388
+ this.promptConfigs.set(name, prompt);
389
+ }
390
+ this.parser.option(name, parserConfig as TOptionConfig);
342
391
  // Interface modifies the return type to reflect new params, cast is necessay.... I think 🤔
343
392
  return this as any;
344
393
  }
@@ -346,8 +395,12 @@ export class InternalCLI<
346
395
  positional<
347
396
  TOption extends string,
348
397
  const TOptionConfig extends OptionConfig<any, any, any>
349
- >(name: TOption, config: TOptionConfig) {
350
- this.parser.positional(name, config);
398
+ >(name: TOption, config: TOptionConfig & { prompt?: PromptOptionConfig<TArgs> }) {
399
+ const { prompt, ...parserConfig } = config;
400
+ if (prompt !== undefined) {
401
+ this.promptConfigs.set(name, prompt);
402
+ }
403
+ this.parser.positional(name, parserConfig as TOptionConfig);
351
404
  // Interface modifies the return type to reflect new params, cast is necessay.... I think 🤔
352
405
  return this as any;
353
406
  }
@@ -794,6 +847,18 @@ export class InternalCLI<
794
847
  return this as unknown as CLI<TArgs, THandlerReturn, TChildren, TParent>;
795
848
  }
796
849
 
850
+ withPromptProvider(
851
+ provider: PromptProvider
852
+ ): CLI<TArgs, THandlerReturn, TChildren, TParent> {
853
+ if (!provider.prompt && !provider.promptBatch) {
854
+ throw new Error(
855
+ "Prompt provider must implement at least one of 'prompt' or 'promptBatch'"
856
+ );
857
+ }
858
+ this.registeredPromptProviders.push(provider);
859
+ return this as unknown as CLI<TArgs, THandlerReturn, TChildren, TParent>;
860
+ }
861
+
797
862
  group(
798
863
  labelOrConfigObject:
799
864
  | string
@@ -990,6 +1055,47 @@ export class InternalCLI<
990
1055
  // seeded with the accumulated values from the discovery loop.
991
1056
  // The alreadyParsed values ensure proper required-option
992
1057
  // validation and prevent positional re-consumption.
1058
+
1059
+ // Prompt for missing option values before final validation.
1060
+ // Collect prompt providers from the full command chain.
1061
+ const allPromptProviders: PromptProvider[] = [
1062
+ ...this.registeredPromptProviders,
1063
+ ];
1064
+ const allPromptConfigs = new Map(this.promptConfigs);
1065
+ {
1066
+ // eslint-disable-next-line @typescript-eslint/no-this-alias
1067
+ let walkCmd: InternalCLI<any, any, any, any> = this;
1068
+ for (const command of this.commandChain) {
1069
+ walkCmd = walkCmd.registeredCommands[command];
1070
+ for (const p of walkCmd.registeredPromptProviders) {
1071
+ allPromptProviders.push(p);
1072
+ }
1073
+ for (const [k, v] of walkCmd.promptConfigs) {
1074
+ allPromptConfigs.set(k, v);
1075
+ }
1076
+ }
1077
+ }
1078
+
1079
+ if (allPromptProviders.length > 0 || allPromptConfigs.size > 0) {
1080
+ const promptedValues = await resolvePrompts({
1081
+ configuredOptions: this.parser.configuredOptions as Record<
1082
+ string,
1083
+ any
1084
+ >,
1085
+ configuredImplies: this.parser.configuredImplies,
1086
+ promptConfigs: allPromptConfigs,
1087
+ providers: allPromptProviders,
1088
+ currentArgs: mergedArgs,
1089
+ });
1090
+
1091
+ // Inject prompted values into accumulated args
1092
+ for (const [key, value] of Object.entries(promptedValues)) {
1093
+ if (value !== undefined) {
1094
+ mergedArgs[key] = value;
1095
+ }
1096
+ }
1097
+ }
1098
+
993
1099
  try {
994
1100
  argv = this.parser
995
1101
  .clone({
@@ -1044,6 +1150,8 @@ export class InternalCLI<
1044
1150
  }
1045
1151
  clone.commandChain = [...this.commandChain];
1046
1152
  clone.requiresCommand = this.requiresCommand;
1153
+ clone.registeredPromptProviders = [...this.registeredPromptProviders];
1154
+ clone.promptConfigs = new Map(this.promptConfigs);
1047
1155
  return clone;
1048
1156
  }
1049
1157
  }
@@ -0,0 +1,48 @@
1
+ import type { InternalOptionConfig } from '@cli-forge/parser';
2
+
3
+ /**
4
+ * Static prompt configuration for an option.
5
+ * - `true` — always prompt
6
+ * - `string` — always prompt with this label
7
+ * - `false` — never prompt
8
+ */
9
+ export type PromptConfig = boolean | string;
10
+
11
+ /**
12
+ * Full prompt configuration, including dynamic resolution.
13
+ * When a function is provided, it receives accumulated args and returns
14
+ * a static PromptConfig. Returning null/undefined from the callback
15
+ * is treated as falsy (don't prompt).
16
+ */
17
+ export type PromptOptionConfig<TArgs = unknown> =
18
+ | PromptConfig
19
+ | ((args: Partial<TArgs>) => PromptConfig | null | undefined);
20
+
21
+ /**
22
+ * An option that needs prompting, passed to prompt providers.
23
+ */
24
+ export interface PromptOption {
25
+ /** The option name (key) */
26
+ name: string;
27
+ /** The full option config from the parser, with resolved prompt value */
28
+ config: InternalOptionConfig & { prompt?: PromptConfig };
29
+ }
30
+
31
+ /**
32
+ * A prompt provider that can fulfill missing option values interactively.
33
+ */
34
+ export interface PromptProvider {
35
+ /**
36
+ * If provided, this provider only handles options where filter returns true.
37
+ * Providers without filters act as fallbacks.
38
+ */
39
+ filter?: (name: string, config: InternalOptionConfig) => boolean;
40
+ /**
41
+ * Prompt for a single option. Called per-option if promptBatch is not defined.
42
+ */
43
+ prompt?: (option: PromptOption) => Promise<unknown>;
44
+ /**
45
+ * Prompt for multiple options at once. Preferred over prompt when available.
46
+ */
47
+ promptBatch?: (options: PromptOption[]) => Promise<Record<string, unknown>>;
48
+ }