cli-forge 1.2.3 → 1.4.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/dist/bin/cli.d.ts +1 -1
- package/dist/bin/commands/generate-documentation.d.ts +2 -2
- package/dist/bin/commands/generate-documentation.js +36 -6
- package/dist/bin/commands/generate-documentation.js.map +1 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.js.map +1 -1
- package/dist/lib/configuration-providers.d.ts +3 -2
- package/dist/lib/configuration-providers.js +38 -2
- package/dist/lib/configuration-providers.js.map +1 -1
- package/dist/lib/documentation.d.ts +6 -1
- package/dist/lib/documentation.js +4 -0
- package/dist/lib/documentation.js.map +1 -1
- package/dist/lib/format-help.js +9 -0
- package/dist/lib/format-help.js.map +1 -1
- package/dist/lib/internal-cli.d.ts +14 -2
- package/dist/lib/internal-cli.js +61 -3
- package/dist/lib/internal-cli.js.map +1 -1
- package/dist/lib/prompt-types.d.ts +44 -0
- package/dist/lib/prompt-types.js +3 -0
- package/dist/lib/prompt-types.js.map +1 -0
- package/dist/lib/public-api.d.ts +45 -12
- package/dist/lib/public-api.js.map +1 -1
- package/dist/lib/resolve-prompts.d.ts +13 -0
- package/dist/lib/resolve-prompts.js +121 -0
- package/dist/lib/resolve-prompts.js.map +1 -0
- package/dist/prompt-providers/clack.d.ts +29 -0
- package/dist/prompt-providers/clack.js +136 -0
- package/dist/prompt-providers/clack.js.map +1 -0
- package/package.json +11 -2
- package/src/bin/commands/generate-documentation.ts +70 -9
- package/src/index.ts +1 -0
- package/src/lib/composable-builder.ts +3 -3
- package/src/lib/configuration-providers.ts +53 -4
- package/src/lib/documentation.ts +11 -0
- package/src/lib/format-help.ts +10 -0
- package/src/lib/internal-cli.spec.ts +300 -0
- package/src/lib/internal-cli.ts +80 -7
- package/src/lib/prompt-types.ts +48 -0
- package/src/lib/public-api.ts +31 -19
- package/src/lib/resolve-prompts.spec.ts +311 -0
- package/src/lib/resolve-prompts.ts +156 -0
- package/src/prompt-providers/clack.spec.ts +376 -0
- package/src/prompt-providers/clack.ts +169 -0
- package/tsconfig.lib.json.tsbuildinfo +1 -1
- package/typedoc.json +10 -0
- package/.eslintrc.json +0 -36
|
@@ -7,8 +7,8 @@ import { pathToFileURL } from 'node:url';
|
|
|
7
7
|
|
|
8
8
|
import cli, { ArgumentsOf, CLI } from '../..';
|
|
9
9
|
import { Documentation, generateDocumentation } from '../../lib/documentation';
|
|
10
|
-
import { ensureDirSync } from '../utils/fs';
|
|
11
10
|
import { InternalCLI } from '../../lib/internal-cli';
|
|
11
|
+
import { ensureDirSync } from '../utils/fs';
|
|
12
12
|
|
|
13
13
|
type mdfactory = typeof import('markdown-factory');
|
|
14
14
|
|
|
@@ -68,6 +68,10 @@ export const generateDocumentationCommand = cli('generate-documentation', {
|
|
|
68
68
|
const documentation = generateDocumentation(cli);
|
|
69
69
|
if (args.format === 'md') {
|
|
70
70
|
await generateMarkdownDocumentation(documentation, args);
|
|
71
|
+
|
|
72
|
+
if (args.llms) {
|
|
73
|
+
generateLlmsTxt(documentation, args);
|
|
74
|
+
}
|
|
71
75
|
} else if (args.format === 'json') {
|
|
72
76
|
const outfile = args.output.endsWith('json')
|
|
73
77
|
? args.output
|
|
@@ -76,10 +80,6 @@ export const generateDocumentationCommand = cli('generate-documentation', {
|
|
|
76
80
|
ensureDirSync(outdir);
|
|
77
81
|
writeFileSync(outfile, JSON.stringify(documentation, null, 2));
|
|
78
82
|
}
|
|
79
|
-
|
|
80
|
-
if (args.llms) {
|
|
81
|
-
generateLlmsTxt(documentation, args);
|
|
82
|
-
}
|
|
83
83
|
},
|
|
84
84
|
});
|
|
85
85
|
|
|
@@ -116,7 +116,9 @@ function generateLlmsTxtContent(
|
|
|
116
116
|
lines.push(docs.description);
|
|
117
117
|
lines.push('');
|
|
118
118
|
}
|
|
119
|
-
lines.push(
|
|
119
|
+
lines.push(
|
|
120
|
+
'This document describes the CLI commands and options for AI agent consumption.'
|
|
121
|
+
);
|
|
120
122
|
lines.push('');
|
|
121
123
|
} else {
|
|
122
124
|
lines.push(`${indent}## ${fullCommand}`);
|
|
@@ -154,7 +156,9 @@ function generateLlmsTxtContent(
|
|
|
154
156
|
for (const [, opt] of optionEntries) {
|
|
155
157
|
const typeStr = formatOptionType(opt);
|
|
156
158
|
const aliasStr = opt.alias?.length
|
|
157
|
-
? ` (aliases: ${opt.alias
|
|
159
|
+
? ` (aliases: ${opt.alias
|
|
160
|
+
.map((a) => (a.length === 1 ? `-${a}` : `--${a}`))
|
|
161
|
+
.join(', ')})`
|
|
158
162
|
: '';
|
|
159
163
|
const reqStr =
|
|
160
164
|
opt.required && opt.default === undefined ? ' [required]' : '';
|
|
@@ -184,7 +188,9 @@ function generateLlmsTxtContent(
|
|
|
184
188
|
for (const opt of group.keys) {
|
|
185
189
|
const typeStr = formatOptionType(opt);
|
|
186
190
|
const aliasStr = opt.alias?.length
|
|
187
|
-
? ` (aliases: ${opt.alias
|
|
191
|
+
? ` (aliases: ${opt.alias
|
|
192
|
+
.map((a) => (a.length === 1 ? `-${a}` : `--${a}`))
|
|
193
|
+
.join(', ')})`
|
|
188
194
|
: '';
|
|
189
195
|
const reqStr =
|
|
190
196
|
opt.required && opt.default === undefined ? ' [required]' : '';
|
|
@@ -200,6 +206,16 @@ function generateLlmsTxtContent(
|
|
|
200
206
|
}
|
|
201
207
|
}
|
|
202
208
|
|
|
209
|
+
// Configuration sources
|
|
210
|
+
if (docs.configurationSources && docs.configurationSources.length > 0) {
|
|
211
|
+
lines.push(`${indent}Configuration:`);
|
|
212
|
+
for (const section of docs.configurationSources) {
|
|
213
|
+
lines.push(`${indent} ${section.heading}`);
|
|
214
|
+
lines.push(`${indent} ${section.body}`);
|
|
215
|
+
}
|
|
216
|
+
lines.push('');
|
|
217
|
+
}
|
|
218
|
+
|
|
203
219
|
// Examples
|
|
204
220
|
if (docs.examples.length > 0) {
|
|
205
221
|
lines.push(`${indent}Examples:`);
|
|
@@ -214,7 +230,9 @@ function generateLlmsTxtContent(
|
|
|
214
230
|
lines.push(`${indent}Subcommands:`);
|
|
215
231
|
for (const sub of docs.subcommands) {
|
|
216
232
|
lines.push(
|
|
217
|
-
`${indent} ${sub.name}${
|
|
233
|
+
`${indent} ${sub.name}${
|
|
234
|
+
sub.description ? ` - ${sub.description}` : ''
|
|
235
|
+
}`
|
|
218
236
|
);
|
|
219
237
|
}
|
|
220
238
|
lines.push('');
|
|
@@ -269,12 +287,19 @@ async function generateMarkdownForSingleCommand(
|
|
|
269
287
|
md
|
|
270
288
|
)
|
|
271
289
|
),
|
|
290
|
+
getConfigurationSourcesLink(
|
|
291
|
+
docs.configurationSources,
|
|
292
|
+
outdir,
|
|
293
|
+
docsRoot,
|
|
294
|
+
md
|
|
295
|
+
),
|
|
272
296
|
getSubcommandsFragment(docs.subcommands, outdir, docsRoot, md),
|
|
273
297
|
getExamplesFragment(docs.examples, md),
|
|
274
298
|
getEpilogueFragment(docs.epilogue, md),
|
|
275
299
|
].filter(isTruthy)
|
|
276
300
|
)
|
|
277
301
|
);
|
|
302
|
+
writeConfigurationSourcesFile(docs.configurationSources, outdir, md);
|
|
278
303
|
for (const subcommand of docs.subcommands) {
|
|
279
304
|
await generateMarkdownForSingleCommand(
|
|
280
305
|
subcommand,
|
|
@@ -347,6 +372,42 @@ function getFlagArgsFragment(
|
|
|
347
372
|
);
|
|
348
373
|
}
|
|
349
374
|
|
|
375
|
+
function getConfigurationSourcesLink(
|
|
376
|
+
sources: Documentation['configurationSources'],
|
|
377
|
+
outdir: string,
|
|
378
|
+
docsRoot: string,
|
|
379
|
+
md: mdfactory
|
|
380
|
+
) {
|
|
381
|
+
if (!sources || sources.length === 0) {
|
|
382
|
+
return undefined;
|
|
383
|
+
}
|
|
384
|
+
const linkPath =
|
|
385
|
+
'./' +
|
|
386
|
+
joinPathFragments(
|
|
387
|
+
normalize(relative(docsRoot, outdir)),
|
|
388
|
+
'configuration.md'
|
|
389
|
+
);
|
|
390
|
+
return md.h2('Configuration', md.link(linkPath, 'Configuration'));
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
function writeConfigurationSourcesFile(
|
|
394
|
+
sources: Documentation['configurationSources'],
|
|
395
|
+
outdir: string,
|
|
396
|
+
md: mdfactory
|
|
397
|
+
) {
|
|
398
|
+
if (!sources || sources.length === 0) {
|
|
399
|
+
return;
|
|
400
|
+
}
|
|
401
|
+
ensureDirSync(outdir);
|
|
402
|
+
writeFileSync(
|
|
403
|
+
join(outdir, 'configuration.md'),
|
|
404
|
+
md.h1(
|
|
405
|
+
'Configuration',
|
|
406
|
+
...sources.map((section) => md.h2(section.heading, section.body))
|
|
407
|
+
)
|
|
408
|
+
);
|
|
409
|
+
}
|
|
410
|
+
|
|
350
411
|
function getSubcommandsFragment(
|
|
351
412
|
subcommands: Documentation['subcommands'],
|
|
352
413
|
outdir: string,
|
package/src/index.ts
CHANGED
|
@@ -9,5 +9,6 @@ export type {
|
|
|
9
9
|
ExtractChildren,
|
|
10
10
|
} from './lib/composable-builder';
|
|
11
11
|
export type { ArgumentsOf } from './lib/utils';
|
|
12
|
+
export type { PromptConfig, PromptOptionConfig, PromptOption, PromptProvider } from './lib/prompt-types';
|
|
12
13
|
export { ConfigurationProviders } from './lib/configuration-providers';
|
|
13
14
|
export type { LocalizationDictionary, LocalizationFunction } from '@cli-forge/parser';
|
|
@@ -19,7 +19,7 @@ export type ExtractArgs<T> = T extends CLI<infer A, any, any, any> ? A : never;
|
|
|
19
19
|
*/
|
|
20
20
|
export type ComposableBuilder<
|
|
21
21
|
TArgs2 extends ParsedArgs,
|
|
22
|
-
// eslint-disable-next-line @typescript-eslint/
|
|
22
|
+
// eslint-disable-next-line @typescript-eslint/no-empty-object-type
|
|
23
23
|
TAddedChildren = {}
|
|
24
24
|
> = <TInit extends ParsedArgs, THandlerReturn, TChildren, TParent>(
|
|
25
25
|
init: CLI<TInit, THandlerReturn, TChildren, TParent>
|
|
@@ -39,11 +39,11 @@ export type ComposableBuilder<
|
|
|
39
39
|
*/
|
|
40
40
|
export function makeComposableBuilder<
|
|
41
41
|
TArgs2 extends ParsedArgs,
|
|
42
|
-
// eslint-disable-next-line @typescript-eslint/
|
|
42
|
+
// eslint-disable-next-line @typescript-eslint/no-empty-object-type
|
|
43
43
|
TChildren2 = {}
|
|
44
44
|
>(
|
|
45
45
|
fn: (
|
|
46
|
-
// eslint-disable-next-line @typescript-eslint/
|
|
46
|
+
// eslint-disable-next-line @typescript-eslint/no-empty-object-type
|
|
47
47
|
init: CLI<ParsedArgs, any, {}, any>
|
|
48
48
|
) => CLI<TArgs2, any, TChildren2, any>
|
|
49
49
|
) {
|
|
@@ -1,5 +1,13 @@
|
|
|
1
1
|
import { ConfigurationFiles } from '@cli-forge/parser';
|
|
2
2
|
|
|
3
|
+
let md: typeof import('markdown-factory') | undefined;
|
|
4
|
+
try {
|
|
5
|
+
// markdown-factory is an optional peer dependency
|
|
6
|
+
md = require('markdown-factory');
|
|
7
|
+
} catch {
|
|
8
|
+
// not available
|
|
9
|
+
}
|
|
10
|
+
|
|
3
11
|
/**
|
|
4
12
|
* A collection of built-in configuration provider factories. These should be invoked and passed to
|
|
5
13
|
* {@link CLI.config} to load configuration from various sources. For custom configuration providers, see
|
|
@@ -25,12 +33,53 @@ export const ConfigurationProviders = {
|
|
|
25
33
|
/**
|
|
26
34
|
* Load configuration from a JSON file.
|
|
27
35
|
*
|
|
28
|
-
* @param filename The filename of the JSON file to load.
|
|
36
|
+
* @param filename The filename (or array of possible filenames) of the JSON file to load.
|
|
37
|
+
* When an array is provided, the nearest matching file wins.
|
|
29
38
|
* @param key The key in the JSON file to load as configuration. By default, the entire JSON object is loaded.
|
|
30
39
|
*/
|
|
31
|
-
JsonFile<T>(filename: string, key?: string) {
|
|
32
|
-
|
|
33
|
-
|
|
40
|
+
JsonFile<T>(filename: string | string[], key?: string) {
|
|
41
|
+
const loader = ConfigurationFiles.getJsonFileConfigLoader<T>(
|
|
42
|
+
filename,
|
|
43
|
+
key ? (json) => json[key] : undefined,
|
|
44
|
+
key ? (json, config) => ({ ...json, [key]: config }) : undefined
|
|
34
45
|
);
|
|
46
|
+
if (key) {
|
|
47
|
+
const filenames = Array.isArray(filename) ? filename : [filename];
|
|
48
|
+
const fileList = filenames.join(', ');
|
|
49
|
+
loader.describeConfig = () => {
|
|
50
|
+
const heading = `JSON File: ${fileList} (key: "${key}")`;
|
|
51
|
+
if (md) {
|
|
52
|
+
return {
|
|
53
|
+
heading,
|
|
54
|
+
body: md.lines(
|
|
55
|
+
filenames.length > 1
|
|
56
|
+
? `Searches for one of: ${filenames.map((f) => md.code(f)).join(', ')}`
|
|
57
|
+
: `Searches for ${md.code(filenames[0])}`,
|
|
58
|
+
'Resolution walks up the directory tree from the working directory, using the nearest match.',
|
|
59
|
+
`Reads the ${md.code(`"${key}"`)} key from the JSON file.`,
|
|
60
|
+
`Supports ${md.code('"extends"')} for configuration inheritance.`,
|
|
61
|
+
'',
|
|
62
|
+
md.bold('Example:'),
|
|
63
|
+
md.codeBlock(
|
|
64
|
+
JSON.stringify({ [key]: { option: 'value' } }, null, 2),
|
|
65
|
+
'json'
|
|
66
|
+
)
|
|
67
|
+
),
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
return {
|
|
71
|
+
heading,
|
|
72
|
+
body: [
|
|
73
|
+
filenames.length > 1
|
|
74
|
+
? `Searches for one of: ${fileList}`
|
|
75
|
+
: `Searches for ${filenames[0]}`,
|
|
76
|
+
'Resolution walks up the directory tree from the working directory, using the nearest match.',
|
|
77
|
+
`Reads the "${key}" key from the JSON file.`,
|
|
78
|
+
'Supports "extends" for configuration inheritance.',
|
|
79
|
+
].join('\n\n'),
|
|
80
|
+
};
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
return loader;
|
|
35
84
|
},
|
|
36
85
|
};
|
package/src/lib/documentation.ts
CHANGED
|
@@ -3,6 +3,7 @@ import {
|
|
|
3
3
|
OptionConfigToType,
|
|
4
4
|
readDefaultValue,
|
|
5
5
|
LocalizationDictionary,
|
|
6
|
+
ConfigurationFiles,
|
|
6
7
|
} from '@cli-forge/parser';
|
|
7
8
|
import { InternalCLI } from './internal-cli';
|
|
8
9
|
import { CLI } from './public-api';
|
|
@@ -20,6 +21,11 @@ export type Documentation = {
|
|
|
20
21
|
keys: Array<NormalizedOptionConfig>;
|
|
21
22
|
}>;
|
|
22
23
|
subcommands: Documentation[];
|
|
24
|
+
/**
|
|
25
|
+
* Describes how configuration is loaded for this command.
|
|
26
|
+
* Each section is produced by a provider's `describeConfig` method.
|
|
27
|
+
*/
|
|
28
|
+
configurationSources?: ConfigurationFiles.ConfigurationDocSection[];
|
|
23
29
|
/**
|
|
24
30
|
* Localized keys for options and commands. Maps from default key to full localization entry.
|
|
25
31
|
* Only present if localization is configured.
|
|
@@ -146,6 +152,11 @@ export function generateDocumentation(
|
|
|
146
152
|
subcommands,
|
|
147
153
|
};
|
|
148
154
|
|
|
155
|
+
const configDocs = parser.getConfigurationDocs();
|
|
156
|
+
if (configDocs.length > 0) {
|
|
157
|
+
result.configurationSources = configDocs;
|
|
158
|
+
}
|
|
159
|
+
|
|
149
160
|
if (localizedKeys) {
|
|
150
161
|
result.localizedKeys = localizedKeys;
|
|
151
162
|
}
|
package/src/lib/format-help.ts
CHANGED
|
@@ -76,6 +76,16 @@ export function formatHelp(parentCLI: InternalCLI<any>): string {
|
|
|
76
76
|
}
|
|
77
77
|
}
|
|
78
78
|
|
|
79
|
+
const configDocs = command.parser.getConfigurationDocs();
|
|
80
|
+
if (configDocs.length > 0) {
|
|
81
|
+
help.push('');
|
|
82
|
+
help.push('Configuration:');
|
|
83
|
+
for (const section of configDocs) {
|
|
84
|
+
help.push(` ${section.heading}`);
|
|
85
|
+
help.push(` ${section.body}`);
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
79
89
|
if (Object.keys(command.registeredCommands).length > 0) {
|
|
80
90
|
help.push(' ');
|
|
81
91
|
help.push(
|
|
@@ -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
|
});
|