cli-forge 1.2.3 → 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.
- package/dist/index.d.ts +1 -0
- package/dist/index.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/index.ts +1 -0
- package/src/lib/composable-builder.ts +3 -3
- 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
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.ClackPromptProvider = void 0;
|
|
4
|
+
exports.createClackPromptProvider = createClackPromptProvider;
|
|
5
|
+
/**
|
|
6
|
+
* Creates a prompt provider backed by @clack/prompts.
|
|
7
|
+
* Requires `@clack/prompts` as a peer dependency.
|
|
8
|
+
*
|
|
9
|
+
* The provider uses dynamic imports so that `@clack/prompts` is only
|
|
10
|
+
* loaded when prompting actually occurs.
|
|
11
|
+
*/
|
|
12
|
+
function createClackPromptProvider(providerOptions) {
|
|
13
|
+
// Build the common stream options once; spread into every prompt call.
|
|
14
|
+
const streamOpts = {};
|
|
15
|
+
if (providerOptions?.input)
|
|
16
|
+
streamOpts.input = providerOptions.input;
|
|
17
|
+
if (providerOptions?.output)
|
|
18
|
+
streamOpts.output = providerOptions.output;
|
|
19
|
+
return {
|
|
20
|
+
async promptBatch(options) {
|
|
21
|
+
const clack = await import('@clack/prompts');
|
|
22
|
+
const results = {};
|
|
23
|
+
for (const option of options) {
|
|
24
|
+
const message = getLabel(option);
|
|
25
|
+
const defaultValue = getDefault(option.config);
|
|
26
|
+
let value;
|
|
27
|
+
if (option.config.type === 'boolean') {
|
|
28
|
+
value = await clack.confirm({
|
|
29
|
+
...streamOpts,
|
|
30
|
+
message,
|
|
31
|
+
initialValue: typeof defaultValue === 'boolean' ? defaultValue : undefined,
|
|
32
|
+
});
|
|
33
|
+
}
|
|
34
|
+
else if (hasChoices(option.config)) {
|
|
35
|
+
const choices = getChoices(option.config);
|
|
36
|
+
if (option.config.type === 'array') {
|
|
37
|
+
value = await clack.multiselect({
|
|
38
|
+
...streamOpts,
|
|
39
|
+
message,
|
|
40
|
+
options: choices.map((c) => ({ value: c, label: String(c) })),
|
|
41
|
+
});
|
|
42
|
+
}
|
|
43
|
+
else {
|
|
44
|
+
value = await clack.select({
|
|
45
|
+
...streamOpts,
|
|
46
|
+
message,
|
|
47
|
+
options: choices.map((c) => ({ value: c, label: String(c) })),
|
|
48
|
+
});
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
else if (option.config.type === 'number') {
|
|
52
|
+
const raw = await clack.text({
|
|
53
|
+
...streamOpts,
|
|
54
|
+
message,
|
|
55
|
+
placeholder: defaultValue !== undefined ? String(defaultValue) : undefined,
|
|
56
|
+
defaultValue: defaultValue !== undefined ? String(defaultValue) : undefined,
|
|
57
|
+
validate: (val) => {
|
|
58
|
+
if (val && isNaN(Number(val))) {
|
|
59
|
+
return 'Please enter a valid number';
|
|
60
|
+
}
|
|
61
|
+
return undefined;
|
|
62
|
+
},
|
|
63
|
+
});
|
|
64
|
+
if (clack.isCancel(raw)) {
|
|
65
|
+
clack.cancel('Operation cancelled.');
|
|
66
|
+
throw new Error('Prompt cancelled by user');
|
|
67
|
+
}
|
|
68
|
+
value = raw !== undefined && raw !== '' ? Number(raw) : defaultValue;
|
|
69
|
+
}
|
|
70
|
+
else {
|
|
71
|
+
// string, array without choices
|
|
72
|
+
value = await clack.text({
|
|
73
|
+
...streamOpts,
|
|
74
|
+
message,
|
|
75
|
+
placeholder: defaultValue !== undefined ? String(defaultValue) : undefined,
|
|
76
|
+
defaultValue: defaultValue !== undefined ? String(defaultValue) : undefined,
|
|
77
|
+
});
|
|
78
|
+
}
|
|
79
|
+
// clack returns a Symbol when the user cancels (Ctrl+C)
|
|
80
|
+
if (clack.isCancel(value)) {
|
|
81
|
+
clack.cancel('Operation cancelled.');
|
|
82
|
+
throw new Error('Prompt cancelled by user');
|
|
83
|
+
}
|
|
84
|
+
results[option.name] = value;
|
|
85
|
+
}
|
|
86
|
+
return results;
|
|
87
|
+
},
|
|
88
|
+
};
|
|
89
|
+
}
|
|
90
|
+
/**
|
|
91
|
+
* Default clack prompt provider, uses stdin/stdout
|
|
92
|
+
*/
|
|
93
|
+
exports.ClackPromptProvider = createClackPromptProvider();
|
|
94
|
+
/**
|
|
95
|
+
* Determine the label to show for a prompt option.
|
|
96
|
+
* Priority: prompt string > description > option name.
|
|
97
|
+
*/
|
|
98
|
+
function getLabel(option) {
|
|
99
|
+
if (typeof option.config.prompt === 'string') {
|
|
100
|
+
return option.config.prompt;
|
|
101
|
+
}
|
|
102
|
+
return option.config.description ?? option.name;
|
|
103
|
+
}
|
|
104
|
+
/**
|
|
105
|
+
* Extract the default value from an option config.
|
|
106
|
+
* Handles the three forms:
|
|
107
|
+
* - Primitive value directly
|
|
108
|
+
* - `{ value: T; description: string }` object
|
|
109
|
+
* - `{ factory: () => T; description: string }` object
|
|
110
|
+
*/
|
|
111
|
+
function getDefault(config) {
|
|
112
|
+
if (config.default === undefined)
|
|
113
|
+
return undefined;
|
|
114
|
+
if (typeof config.default === 'object' && config.default !== null) {
|
|
115
|
+
if ('factory' in config.default) {
|
|
116
|
+
return config.default.factory();
|
|
117
|
+
}
|
|
118
|
+
if ('value' in config.default) {
|
|
119
|
+
return config.default.value;
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
return config.default;
|
|
123
|
+
}
|
|
124
|
+
function hasChoices(config) {
|
|
125
|
+
return ('choices' in config &&
|
|
126
|
+
config['choices'] !== undefined);
|
|
127
|
+
}
|
|
128
|
+
function getChoices(config) {
|
|
129
|
+
const cfg = config;
|
|
130
|
+
const choices = cfg['choices'];
|
|
131
|
+
if (typeof choices === 'function') {
|
|
132
|
+
return choices();
|
|
133
|
+
}
|
|
134
|
+
return choices ?? [];
|
|
135
|
+
}
|
|
136
|
+
//# sourceMappingURL=clack.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"clack.js","sourceRoot":"","sources":["../../src/prompt-providers/clack.ts"],"names":[],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -1,16 +1,20 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "cli-forge",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.3.0",
|
|
4
4
|
"dependencies": {
|
|
5
5
|
"tslib": "^2.3.0",
|
|
6
|
-
"@cli-forge/parser": "1.
|
|
6
|
+
"@cli-forge/parser": "1.3.0"
|
|
7
7
|
},
|
|
8
8
|
"peerDependencies": {
|
|
9
|
+
"@clack/prompts": "*",
|
|
9
10
|
"markdown-factory": "^0.2.0",
|
|
10
11
|
"tsx": "^4.19.0",
|
|
11
12
|
"zod": "^4.1.13"
|
|
12
13
|
},
|
|
13
14
|
"peerDependenciesMeta": {
|
|
15
|
+
"@clack/prompts": {
|
|
16
|
+
"optional": true
|
|
17
|
+
},
|
|
14
18
|
"markdown-factory": {
|
|
15
19
|
"optional": true,
|
|
16
20
|
"dev": true
|
|
@@ -53,6 +57,11 @@
|
|
|
53
57
|
"require": "./dist/middleware/*.js",
|
|
54
58
|
"types": "./dist/middleware/*.d.ts"
|
|
55
59
|
},
|
|
60
|
+
"./prompt-providers/clack": {
|
|
61
|
+
"require": "./dist/prompt-providers/clack.js",
|
|
62
|
+
"types": "./dist/prompt-providers/clack.d.ts",
|
|
63
|
+
"import": "./dist/prompt-providers/clack.js"
|
|
64
|
+
},
|
|
56
65
|
"./package.json": "./package.json"
|
|
57
66
|
},
|
|
58
67
|
"publishConfig": {
|
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,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
|
});
|
package/src/lib/internal-cli.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
/* eslint-disable @typescript-eslint/
|
|
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
|
/**
|
|
@@ -55,7 +57,7 @@ const CLI_FORGE_BRAND = Symbol.for('cli-forge:InternalCLI');
|
|
|
55
57
|
export class InternalCLI<
|
|
56
58
|
TArgs extends ParsedArgs = ParsedArgs,
|
|
57
59
|
THandlerReturn = void,
|
|
58
|
-
// eslint-disable-next-line @typescript-eslint/
|
|
60
|
+
// eslint-disable-next-line @typescript-eslint/no-empty-object-type
|
|
59
61
|
TChildren = {},
|
|
60
62
|
TParent = undefined
|
|
61
63
|
> implements CLI<TArgs, THandlerReturn, TChildren, TParent>
|
|
@@ -122,6 +124,14 @@ export class InternalCLI<
|
|
|
122
124
|
(cli: any, args: TArgs) => Promise<void> | void
|
|
123
125
|
> = [];
|
|
124
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
|
+
|
|
125
135
|
/**
|
|
126
136
|
* Set when a `$0` alias replaces the root builder via `.command()`.
|
|
127
137
|
* The $0 builder should only run if no explicit subcommand is given,
|
|
@@ -287,7 +297,7 @@ export class InternalCLI<
|
|
|
287
297
|
CLI<TArgs, THandlerReturn, TChildren, TParent>
|
|
288
298
|
>;
|
|
289
299
|
}
|
|
290
|
-
: // eslint-disable-next-line @typescript-eslint/
|
|
300
|
+
: // eslint-disable-next-line @typescript-eslint/no-empty-object-type
|
|
291
301
|
{}),
|
|
292
302
|
TParent
|
|
293
303
|
> {
|
|
@@ -372,8 +382,12 @@ export class InternalCLI<
|
|
|
372
382
|
option<
|
|
373
383
|
TOption extends string,
|
|
374
384
|
const TOptionConfig extends OptionConfig<any, any, any>
|
|
375
|
-
>(name: TOption, config: TOptionConfig) {
|
|
376
|
-
|
|
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);
|
|
377
391
|
// Interface modifies the return type to reflect new params, cast is necessay.... I think 🤔
|
|
378
392
|
return this as any;
|
|
379
393
|
}
|
|
@@ -381,8 +395,12 @@ export class InternalCLI<
|
|
|
381
395
|
positional<
|
|
382
396
|
TOption extends string,
|
|
383
397
|
const TOptionConfig extends OptionConfig<any, any, any>
|
|
384
|
-
>(name: TOption, config: TOptionConfig) {
|
|
385
|
-
|
|
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);
|
|
386
404
|
// Interface modifies the return type to reflect new params, cast is necessay.... I think 🤔
|
|
387
405
|
return this as any;
|
|
388
406
|
}
|
|
@@ -829,6 +847,18 @@ export class InternalCLI<
|
|
|
829
847
|
return this as unknown as CLI<TArgs, THandlerReturn, TChildren, TParent>;
|
|
830
848
|
}
|
|
831
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
|
+
|
|
832
862
|
group(
|
|
833
863
|
labelOrConfigObject:
|
|
834
864
|
| string
|
|
@@ -1025,6 +1055,47 @@ export class InternalCLI<
|
|
|
1025
1055
|
// seeded with the accumulated values from the discovery loop.
|
|
1026
1056
|
// The alreadyParsed values ensure proper required-option
|
|
1027
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
|
+
|
|
1028
1099
|
try {
|
|
1029
1100
|
argv = this.parser
|
|
1030
1101
|
.clone({
|
|
@@ -1079,6 +1150,8 @@ export class InternalCLI<
|
|
|
1079
1150
|
}
|
|
1080
1151
|
clone.commandChain = [...this.commandChain];
|
|
1081
1152
|
clone.requiresCommand = this.requiresCommand;
|
|
1153
|
+
clone.registeredPromptProviders = [...this.registeredPromptProviders];
|
|
1154
|
+
clone.promptConfigs = new Map(this.promptConfigs);
|
|
1082
1155
|
return clone;
|
|
1083
1156
|
}
|
|
1084
1157
|
}
|