commander-wizard 0.0.1

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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 commander-wizard contributors
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,165 @@
1
+ # commander-wizard
2
+
3
+ Add a [Clack](https://github.com/bombshell-dev/clack) wizard to your
4
+ [Commander](https://github.com/tj/commander.js) CLI. Users can fill in missing
5
+ inputs, review their choices, and copy a command to run without prompts.
6
+
7
+ **Requires:** Commander 14 or 15, Node >=22.12.0, and ESM. Wizard mode needs a terminal
8
+ for both stdin and stdout. Rerun commands use POSIX shell syntax.
9
+
10
+ ## Quick start
11
+
12
+ ```sh
13
+ nub add commander commander-wizard
14
+ ```
15
+
16
+ Save as `cli.mjs`:
17
+
18
+ ```js
19
+ import { Command } from 'commander';
20
+ import { addWizard, WizardCancelledError } from 'commander-wizard';
21
+
22
+ const program = new Command('deploy-cli');
23
+ const deploy = program.command('deploy')
24
+ .argument('<environment>')
25
+ .requiredOption('--service <name>')
26
+ .option('--region <name>', 'AWS region', 'us-east-1')
27
+ .option('--force', 'skip safety checks')
28
+ .action((environment, options) => console.log({ environment, ...options }));
29
+
30
+ // Add your commands and options before calling addWizard.
31
+ addWizard(program, { invocation: ['node', 'cli.mjs'] });
32
+
33
+ try {
34
+ await program.parseAsync();
35
+ } catch (error) {
36
+ if (!(error instanceof WizardCancelledError)) throw error;
37
+ }
38
+ ```
39
+
40
+ ```sh
41
+ node cli.mjs deploy --wizard # prompt for inputs
42
+ node cli.mjs deploy dev --service api --wizard # keep supplied inputs
43
+ node cli.mjs deploy dev --service api # ordinary Commander invocation
44
+ ```
45
+
46
+ Use `parseAsync()` for wizard invocations, or
47
+ `parseAsync(args, { from: 'user' })` with an argument array.
48
+
49
+ `addWizard()` decorates the root and every leaf subcommand, including nested
50
+ ones. Only the selected leaf command and supported ancestor options are
51
+ prompted.
52
+
53
+ ## Using the wizard
54
+
55
+ You keep values supplied on the command line and answer prompts for the rest:
56
+
57
+ - Boolean flags: Yes/No, with No as the default for a plain flag such as `--force`.
58
+ - Choices: select one, or use multiselect for variadic choices.
59
+ - Text inputs: the default is prefilled; press Enter to accept it or edit it.
60
+ Variadics without choices collect one value per line; an empty line finishes the
61
+ list.
62
+
63
+ You review **raw CLI inputs** and a rerun command. Select **Edit …** to revisit a
64
+ prompt with your previous answer prefilled; other answers stay intact. Repeat as
65
+ needed, then choose **Continue to confirmation** (final confirmation defaults to
66
+ No). CLI-supplied inputs remain unchanged and are not offered for editing.
67
+ After confirmation, Commander applies parsers, requirements, conflicts, and
68
+ implications before running your action. Restart the wizard to correct invalid
69
+ inputs; custom parsers do not run during prompting.
70
+
71
+ Declining or pressing Ctrl-C throws `WizardCancelledError` without running your
72
+ hooks or action. Catch it as in the example. For Commander errors, configure
73
+ `exitOverride()` if you need to catch them instead of exiting.
74
+
75
+ You keep Commander's parsing and validation for ordinary invocations. Your
76
+ action receives no wizard-trigger option after a wizard run.
77
+
78
+ **Do not use the wizard for secrets. You expose inputs in review and rerun output.**
79
+
80
+ ## Custom flags
81
+
82
+ The default is `--wizard`, with no short alias. Set `flags` to a Commander boolean
83
+ flag declaration to replace it:
84
+
85
+ ```js
86
+ addWizard(program, { flags: '-i, --interactive' });
87
+ ```
88
+
89
+ You can use a short flag alone, a long flag alone, or both. Flags that take values
90
+ and negated flags (`--no-…`) are not supported. The configured flags and their
91
+ Commander option attribute must not collide with existing options, including help.
92
+
93
+ ## Rerun commands and defaults
94
+
95
+ Set `invocation` to executable tokens, such as `['node', 'cli.mjs']` or
96
+ `['your-installed-cli']`. Without it, you get the current Node executable,
97
+ execution flags, and script path. Specify it for custom launchers. Rerun from
98
+ the same directory with the same application configuration. Use a POSIX shell;
99
+ PowerShell and cmd.exe use different quoting.
100
+
101
+ You can accept string, numeric, and choice defaults. You get those values in the
102
+ rerun command, except for empty variadics and false booleans without a negative
103
+ flag. Declare a negative form such as `--no-color` to express false by name.
104
+ Inputs with defaults are prefilled — press Enter to accept. Clearing the input
105
+ is refused, because omitting the flag would restore the default.
106
+
107
+ For a custom parser's default, provide the raw CLI spelling with `rawDefaults`.
108
+ For example, add this to the quick start **before parsing**, replacing its
109
+ `addWizard()` call:
110
+
111
+ ```js
112
+ import { Option } from 'commander';
113
+
114
+ const replicas = new Option('--replicas <count>')
115
+ .argParser(Number)
116
+ .default(3);
117
+ deploy.addOption(replicas);
118
+
119
+ addWizard(program, {
120
+ invocation: ['node', 'cli.mjs'],
121
+ rawDefaults: new Map([[replicas, ['3']]]),
122
+ });
123
+ ```
124
+
125
+ Key `rawDefaults` by the `Option` or `Argument` object. Supply raw strings, one
126
+ for a scalar input, that produce the intended value with your parser's
127
+ previous/default argument. Define a parser to convert CLI strings to numbers;
128
+ a numeric default alone does not perform that conversion.
129
+
130
+ ## Compatibility limits
131
+
132
+ Use root-only or nested leaf actions with global scalar options, short flags,
133
+ positive/negative boolean pairs, positional arguments, choices, and leaf variadics.
134
+
135
+ In wizard mode, put the full command path before flags:
136
+ `cli group command --wizard --flag=value`. Keep short flags separate; avoid `-abc` and
137
+ `-n3`. Put option-like positional values after `--`, including a literal `--wizard`.
138
+
139
+ You cannot use these Commander features in wizard mode:
140
+
141
+ - Executable subcommands, legacy command listeners, actions on commands with
142
+ children, or implicit/default subcommands.
143
+ - Ancestor positional arguments or variadic options, positional/pass-through
144
+ option modes, or shadowed global option names/flags.
145
+ - Environment-bound options, optional option values (`--color [value]`), presets,
146
+ custom boolean parsers, or options stored as command properties.
147
+ - Electron argv or piped input/output.
148
+
149
+ Configure your tree before calling `addWizard()` on its root. Reserve the
150
+ configured flags and option attribute (`--wizard` and `wizard` by default). Repeat calls keep the first configuration. Do not add commands afterward
151
+ or decorate overlapping trees.
152
+
153
+ ## Development
154
+
155
+ ```sh
156
+ nub install
157
+ nub run typecheck
158
+ nub run test # regression tests and built-package import check
159
+ nub run test:smoke # terminal test; requires expect and stty
160
+ nub example.ts deploy --wizard
161
+ nub pack --dry-run
162
+ ```
163
+
164
+ Build with `nub run build`; pack the library and declarations from `dist/`.
165
+ MIT licensed. See `LICENSE`.
@@ -0,0 +1,2 @@
1
+ export { addWizard, WizardCancelledError } from './wizard.js';
2
+ export type { WizardOptions } from './wizard.js';
package/dist/index.js ADDED
@@ -0,0 +1 @@
1
+ export { addWizard, WizardCancelledError } from './wizard.js';
@@ -0,0 +1,14 @@
1
+ import type { Argument, Command, Option } from 'commander';
2
+ export declare class WizardCancelledError extends Error {
3
+ constructor();
4
+ }
5
+ export interface WizardOptions {
6
+ /** Commander boolean flag declaration. Defaults to --wizard. */
7
+ flags?: string;
8
+ /** Executable and prefix arguments, e.g. ['nub', 'example.ts']. Never a shell fragment. */
9
+ invocation?: readonly string[];
10
+ /** Raw CLI spellings for defaults processed by custom parsers. Key by Option/Argument identity. */
11
+ rawDefaults?: ReadonlyMap<Option | Argument, readonly string[]>;
12
+ }
13
+ /** Decorate an already-configured program. No global/prototype patching; use parseAsync for wizard mode. */
14
+ export declare function addWizard<T extends Command>(program: T, config?: WizardOptions): T;
package/dist/wizard.js ADDED
@@ -0,0 +1,351 @@
1
+ import * as p from '@clack/prompts';
2
+ import { inspect } from 'node:util';
3
+ export class WizardCancelledError extends Error {
4
+ constructor() { super('Wizard cancelled.'); this.name = 'WizardCancelledError'; }
5
+ }
6
+ const installed = new WeakSet();
7
+ const fail = (message) => { throw new Error(`Wizard: ${message}`); };
8
+ /** Decorate an already-configured program. No global/prototype patching; use parseAsync for wizard mode. */
9
+ export function addWizard(program, config = {}) {
10
+ if (installed.has(program))
11
+ return program;
12
+ if (config.invocation && (!config.invocation.length || !config.invocation.every(v => typeof v === 'string') || !config.invocation[0]))
13
+ fail('invocation must contain an executable followed by string arguments.');
14
+ const flags = config.flags ?? '--wizard';
15
+ if (typeof flags !== 'string' || !/^(?:-[a-zA-Z0-9](?:[ ,|]+--[a-zA-Z0-9][a-zA-Z0-9-]*)?|--[a-zA-Z0-9][a-zA-Z0-9-]*)$/.test(flags))
16
+ fail('flags must declare a boolean flag, e.g. --wizard or -i, --interactive.');
17
+ const flagOption = program.createOption(flags);
18
+ if (flagOption.negate)
19
+ fail('flags must not use a negated --no- flag.');
20
+ const markers = new Set([flagOption.short, flagOption.long].filter((flag) => flag !== undefined));
21
+ const wizardKey = flagOption.attributeName();
22
+ const commands = [];
23
+ const visit = (cmd) => { commands.push(cmd); cmd.commands.forEach(visit); };
24
+ visit(program);
25
+ // Preflight the whole tree before changing any command.
26
+ for (const cmd of commands) {
27
+ const help = cmd.createHelp().visibleOptions(cmd);
28
+ if ([...cmd.options, ...help].some(o => markers.has(o.short ?? '') || markers.has(o.long ?? '') || o.attributeName() === wizardKey))
29
+ fail(`flag conflict on ${cmd.name() || 'program'}: ${flags} is reserved.`);
30
+ if (installed.has(cmd))
31
+ fail('overlapping decorated command trees are unsupported.');
32
+ }
33
+ const parseAsync = program.parseAsync;
34
+ for (const cmd of commands.filter(cmd => cmd.commands.length === 0)) {
35
+ cmd.option(flags, 'collect command inputs interactively');
36
+ // Never let a wizard flag that escaped our bounded scanner dispatch an action.
37
+ cmd.on(`option:${flagOption.name()}`, () => fail('use parseAsync() with an explicit command path and unbundled flags for wizard mode.'));
38
+ }
39
+ program.parseAsync = async function (argv, options) {
40
+ const args = userArgs(argv, options?.from);
41
+ if (!requested(args, markers))
42
+ return await parseAsync.call(this, argv, options);
43
+ if (options?.from === 'electron' || (!argv && process.versions.electron))
44
+ fail('Electron wizard invocations are unsupported; pass explicit user arguments.');
45
+ let input;
46
+ try {
47
+ input = scan(program, args, markers);
48
+ }
49
+ catch {
50
+ // Commander can distinguish reserved text used as data in grammars we do not support.
51
+ // An actual wizard option is stopped by the option listener above.
52
+ return await parseAsync.call(this, argv, options);
53
+ }
54
+ // A marker consumed as an option value is data, not a wizard request.
55
+ if (!input.wizard)
56
+ return await parseAsync.call(this, argv, options);
57
+ checkLayout(input.chain, wizardKey);
58
+ if (!process.stdin.isTTY || !process.stdout.isTTY)
59
+ fail('interactive input requires a TTY.');
60
+ const completed = await collect(input, config, wizardKey);
61
+ // Commander alone owns coercion, validation, hooks, and action dispatch.
62
+ return await parseAsync.call(this, completed, { from: 'user' });
63
+ };
64
+ installed.add(program);
65
+ return program;
66
+ }
67
+ function userArgs(argv, from) {
68
+ if (from === 'user')
69
+ return [...(argv ?? process.argv)];
70
+ return (argv ?? process.argv).slice(!argv && Reflect.get(process, '_eval') !== undefined ? 1 : 2);
71
+ }
72
+ function requested(args, markers) {
73
+ const end = args.indexOf('--');
74
+ return args.slice(0, end < 0 ? args.length : end).some(a => markers.has(a));
75
+ }
76
+ /** Deliberately bounded wizard grammar: explicit command path first, unbundled options. */
77
+ function scan(root, args, markers) {
78
+ const chain = [root];
79
+ let index = 0;
80
+ let cmd = root;
81
+ while (index < args.length) {
82
+ const child = cmd.commands.find(c => c.name() === args[index] || c.aliases().includes(args[index]));
83
+ if (!child)
84
+ break;
85
+ chain.push(child);
86
+ cmd = child;
87
+ index++;
88
+ }
89
+ const result = { chain, options: new Map(), positionals: [], supplied: [], wizard: false };
90
+ const options = chain.flatMap(c => c.options);
91
+ while (index < args.length) {
92
+ const token = args[index++];
93
+ if (token === '--') {
94
+ result.positionals.push(...args.slice(index));
95
+ break;
96
+ }
97
+ if (markers.has(token)) {
98
+ result.wizard = true;
99
+ continue;
100
+ }
101
+ if (!token.startsWith('-') || token === '-') {
102
+ result.positionals.push(token);
103
+ continue;
104
+ }
105
+ const equal = token.startsWith('--') ? token.indexOf('=') : -1;
106
+ const flag = equal < 0 ? token : token.slice(0, equal);
107
+ const opt = options.find(o => o.long === flag || o.short === flag);
108
+ if (!opt)
109
+ return fail(`unknown or bundled option ${flag}. Put the full command path first; use separate flags.`);
110
+ const raw = [];
111
+ raw.push(token);
112
+ if (opt.required || opt.optional) {
113
+ if (equal < 0) {
114
+ if (index >= args.length || (opt.optional && args[index].startsWith('-')))
115
+ fail(`supply a value for ${flag} in wizard mode.`);
116
+ raw.push(args[index++]);
117
+ }
118
+ if (opt.variadic && equal < 0)
119
+ while (index < args.length && !args[index].startsWith('-'))
120
+ raw.push(args[index++]);
121
+ }
122
+ else if (equal >= 0)
123
+ fail(`${flag} does not take a value.`);
124
+ result.options.set(opt, [...(result.options.get(opt) ?? []), ...raw]);
125
+ result.supplied.push({ option: opt, tokens: raw });
126
+ }
127
+ return result;
128
+ }
129
+ function checkLayout(chain, wizardKey) {
130
+ const keys = new Map();
131
+ for (const [index, cmd] of chain.entries()) {
132
+ // Commander 15 has no public capability getters. Keep these checks in one place.
133
+ for (const field of ['_executableHandler', '_passThroughOptions', '_enablePositionalOptions', '_defaultCommandName', '_storeOptionsAsProperties'])
134
+ if (Reflect.get(cmd, field))
135
+ fail(`${field} is unsupported in wizard mode.`);
136
+ if (index < chain.length - 1 && cmd.registeredArguments.length)
137
+ fail('ancestor positional arguments are unsupported in wizard mode.');
138
+ for (const opt of cmd.options) {
139
+ if (opt.attributeName() === wizardKey)
140
+ continue;
141
+ for (const key of [opt.attributeName(), opt.long, opt.short].filter((v) => v !== undefined)) {
142
+ if (keys.has(key) && keys.get(key) !== index)
143
+ fail(`shadowed global option ${key}.`);
144
+ keys.set(key, index);
145
+ }
146
+ const siblings = cmd.options.filter(o => o.attributeName() === opt.attributeName());
147
+ if (siblings.length > 1 && !(siblings.length === 2 && siblings.some(o => o.negate) && siblings.some(o => o.isBoolean())))
148
+ fail(`ambiguous option attribute ${opt.attributeName()}.`);
149
+ if (index < chain.length - 1 && opt.variadic)
150
+ fail('ancestor variadic options are unsupported in wizard mode.');
151
+ if (opt.isBoolean() && opt.parseArg)
152
+ fail('boolean custom parsers are unsupported in wizard mode.');
153
+ if (opt.optional || opt.envVar || opt.presetArg !== undefined)
154
+ fail(`${opt.flags}: optional values, env bindings, and presets are unsupported in wizard mode.`);
155
+ }
156
+ }
157
+ const leaf = chain.at(-1);
158
+ if (leaf.commands.length || !Reflect.get(leaf, '_actionHandler'))
159
+ fail('select an explicit in-process action command. Legacy listeners are unsupported.');
160
+ }
161
+ function defaults(owner, config) {
162
+ if (owner.defaultValue === undefined)
163
+ return [];
164
+ const supplied = config.rawDefaults?.get(owner);
165
+ if (supplied) {
166
+ if (!supplied.every(v => typeof v === 'string') || (!owner.variadic && supplied.length !== 1))
167
+ fail('rawDefaults must contain raw strings (one for a scalar input).');
168
+ return [...supplied];
169
+ }
170
+ // choices() installs a parser, but its default strings remain CLI spellings.
171
+ if (owner.parseArg && !owner.argChoices)
172
+ fail(`provide rawDefaults for ${'flags' in owner ? owner.flags : owner.name()}; custom parsers are not reversible.`);
173
+ const values = Array.isArray(owner.defaultValue) ? owner.defaultValue : [owner.defaultValue];
174
+ if (!values.every(v => typeof v === 'string' || typeof v === 'number'))
175
+ fail('non-text defaults require rawDefaults.');
176
+ return values.map(String);
177
+ }
178
+ async function ask(owner, required, def, editing = false) {
179
+ const label = 'flags' in owner ? owner.flags : owner.name();
180
+ const hasDefault = owner.defaultValue !== undefined &&
181
+ (Array.isArray(owner.defaultValue) ? owner.defaultValue.length > 0 : owner.defaultValue !== '');
182
+ // Omitting tokens would restore Commander's default, not clear it.
183
+ required ||= hasDefault;
184
+ const emptyError = hasDefault ? 'Enter a value; omission restores the default.' : 'Required';
185
+ const message = `${label}${owner.description ? ` — ${owner.description}` : ''}`;
186
+ if (owner.argChoices?.length) {
187
+ const options = owner.argChoices.map(value => ({ value }));
188
+ if (owner.variadic)
189
+ return unwrap(await p.multiselect({
190
+ message: hasDefault ? `${message} (select at least one; omission restores the default)` : message,
191
+ options, required, initialValues: def,
192
+ }));
193
+ // Optional choices must be skippable rather than silently selecting the first entry.
194
+ if (!required && !def.length && !unwrap(await p.confirm({ message: `Set ${label}?`, initialValue: false })))
195
+ return [];
196
+ return [unwrap(await p.select({ message, options, ...(def[0] === undefined ? {} : { initialValue: def[0] }) }))];
197
+ }
198
+ if (owner.variadic) {
199
+ // ponytail: one value per line; empty value as a list item is not expressible, fine for CLI inputs.
200
+ const values = [];
201
+ const note = def.length ? `${editing ? ' (current' : ' (default'}: ${def.join(', ')})` : '';
202
+ while (true) {
203
+ const soFar = values.length ? ` — added: ${values.join(', ')}` : '';
204
+ const value = unwrap(await p.text({
205
+ message: `${message}${note}${soFar} (empty line to finish)`,
206
+ validate: v => !v?.trim() && required && !values.length ? 'Enter at least one value' : undefined,
207
+ }));
208
+ if (!value.trim())
209
+ break;
210
+ values.push(value.trim());
211
+ }
212
+ return values;
213
+ }
214
+ const value = unwrap(await p.text({
215
+ message,
216
+ // Prefill the default so Enter accepts it visibly; clearing is refused below.
217
+ ...(def.length ? { initialValue: def[0] } : {}),
218
+ validate(value) {
219
+ if (!value)
220
+ return required ? emptyError : undefined;
221
+ return undefined;
222
+ },
223
+ }));
224
+ return value === '' ? [] : [value];
225
+ }
226
+ function optionTokens(opt, values) {
227
+ const flag = opt.long ?? opt.short;
228
+ // Repeated long assignments protect leading '-' values and terminate each variadic occurrence.
229
+ if (opt.long)
230
+ return values.map(value => `${flag}=${value}`);
231
+ return values.flatMap(value => [flag, value]);
232
+ }
233
+ async function collect(input, config, wizardKey) {
234
+ const leaf = input.chain.at(-1);
235
+ const answers = new Map();
236
+ const booleans = new Map();
237
+ p.intro(`${leaf.name()} · wizard`);
238
+ while (true) {
239
+ const argv = [];
240
+ const summary = [];
241
+ const editable = [];
242
+ const collectValue = async (owner, required) => {
243
+ const values = answers.get(owner) ?? await ask(owner, required, defaults(owner, config));
244
+ answers.set(owner, values);
245
+ editable.push({
246
+ label: 'flags' in owner ? owner.flags : owner.name(),
247
+ edit: async () => { answers.set(owner, await ask(owner, required, values, true)); },
248
+ });
249
+ return values;
250
+ };
251
+ for (const [index, cmd] of input.chain.entries()) {
252
+ if (index)
253
+ argv.push(cmd.name());
254
+ for (const entry of input.supplied)
255
+ if (cmd.options.includes(entry.option))
256
+ argv.push(...entry.tokens);
257
+ const seen = new Set();
258
+ for (const opt of cmd.options) {
259
+ const key = opt.attributeName();
260
+ if ([wizardKey, 'help', 'version'].includes(key) || seen.has(key))
261
+ continue;
262
+ seen.add(key);
263
+ const group = cmd.options.filter(o => o.attributeName() === key);
264
+ const provided = group.flatMap(o => input.options.get(o) ?? []);
265
+ if (provided.length) {
266
+ summary.push(`${key}: ${inspect(provided)}`);
267
+ continue;
268
+ }
269
+ const positive = group.find(o => !o.negate);
270
+ const negative = group.find(o => o.negate);
271
+ if (opt.isBoolean() || opt.negate) {
272
+ const def = group.reduce((value, option) => option.defaultValue === undefined ? value : Boolean(option.defaultValue), !positive);
273
+ // No invented --no-x: restrict answers to states the CLI can actually express.
274
+ const fixed = (positive && def && !negative) || (!positive && !def);
275
+ const message = `${key} — ${opt.description}`;
276
+ const value = booleans.get(opt) ?? (fixed ? def : unwrap(await p.confirm({ message, initialValue: def })));
277
+ booleans.set(opt, value);
278
+ if (!fixed)
279
+ editable.push({
280
+ label: opt.flags,
281
+ edit: async () => { booleans.set(opt, unwrap(await p.confirm({ message, initialValue: value }))); },
282
+ });
283
+ if (value && positive)
284
+ argv.push(positive.long ?? positive.short);
285
+ else if (!value && negative)
286
+ argv.push(negative.long ?? negative.short);
287
+ summary.push(`${key}: ${value}`);
288
+ }
289
+ else {
290
+ const values = await collectValue(opt, opt.mandatory);
291
+ argv.push(...optionTokens(opt, values));
292
+ summary.push(`${key}: ${inspect(values.length ? values : undefined)}`);
293
+ }
294
+ }
295
+ }
296
+ const positional = [];
297
+ let cursor = 0;
298
+ let omitted = false;
299
+ for (const arg of leaf.registeredArguments) {
300
+ let values = arg.variadic ? input.positionals.slice(cursor) : input.positionals.slice(cursor, cursor + 1);
301
+ cursor += values.length;
302
+ if (!values.length)
303
+ values = await collectValue(arg, arg.required);
304
+ if (omitted && values.length)
305
+ fail('cannot supply a positional argument after an omitted argument.');
306
+ if (!values.length && !arg.variadic)
307
+ omitted = true;
308
+ positional.push(...values);
309
+ summary.push(`${arg.name()}: ${inspect(arg.variadic ? values : values[0])}`);
310
+ }
311
+ positional.push(...input.positionals.slice(cursor)); // let Commander report excess arguments
312
+ argv.push('--', ...positional);
313
+ const invocation = config.invocation ?? [process.execPath, ...process.execArgv, process.argv[1] ?? fail('provide invocation.')];
314
+ const tokens = [...invocation, ...argv];
315
+ if (tokens.some(token => token.includes('\0')))
316
+ fail('NUL bytes cannot be represented in shell arguments.');
317
+ const command = tokens.map(shellQuote).join(' ');
318
+ p.note(`${summary.join('\n')}\n\nrerun non-interactively:\n${command}`, 'Review CLI inputs (Commander validates after confirmation)');
319
+ if (editable.length) {
320
+ const selected = unwrap(await p.select({
321
+ message: 'Continue or edit an input?',
322
+ options: [
323
+ { value: -1, label: 'Continue to confirmation' },
324
+ ...editable.map((field, value) => ({ value, label: `Edit ${field.label}` })),
325
+ ],
326
+ initialValue: -1,
327
+ }));
328
+ if (selected !== -1) {
329
+ await editable[selected].edit();
330
+ continue;
331
+ }
332
+ }
333
+ if (!unwrap(await p.confirm({ message: 'Run with these settings?', initialValue: false }))) {
334
+ p.cancel('Cancelled — nothing ran.');
335
+ throw new WizardCancelledError();
336
+ }
337
+ p.outro(`Running ${leaf.name()}…`);
338
+ return argv;
339
+ }
340
+ }
341
+ function unwrap(value) {
342
+ if (p.isCancel(value)) {
343
+ p.cancel('Wizard cancelled.');
344
+ throw new WizardCancelledError();
345
+ }
346
+ return value;
347
+ }
348
+ /** POSIX shell quoting. Windows shells are not supported. */
349
+ function shellQuote(value) {
350
+ return /^[\w.,:/@%+=-]+$/.test(value) ? value : `'${value.replaceAll("'", "'\\''")}'`;
351
+ }
package/package.json ADDED
@@ -0,0 +1,65 @@
1
+ {
2
+ "name": "commander-wizard",
3
+ "version": "0.0.1",
4
+ "description": "Interactive wizard mode for commander CLIs — collect, review, and rerun command inputs",
5
+ "keywords": [
6
+ "commander",
7
+ "cli",
8
+ "wizard",
9
+ "interactive",
10
+ "prompts",
11
+ "clack",
12
+ "typescript"
13
+ ],
14
+ "author": "jaydenfyi",
15
+ "license": "MIT",
16
+ "repository": {
17
+ "type": "git",
18
+ "url": "git+https://github.com/jaydenfyi/commander-wizard.git"
19
+ },
20
+ "homepage": "https://github.com/jaydenfyi/commander-wizard#readme",
21
+ "bugs": {
22
+ "url": "https://github.com/jaydenfyi/commander-wizard/issues"
23
+ },
24
+ "type": "module",
25
+ "packageManager": "nub@0.6.0",
26
+ "devEngines": {
27
+ "packageManager": {
28
+ "name": "nub",
29
+ "version": "^0.6.0",
30
+ "onFail": "warn"
31
+ }
32
+ },
33
+ "scripts": {
34
+ "start": "nub example.ts",
35
+ "build": "tsc -p tsconfig.build.json",
36
+ "typecheck": "tsc --noEmit",
37
+ "test": "nub run build && tsc -p tsconfig.test.json && node --experimental-test-module-mocks --test dist-test/wizard.test.js",
38
+ "test:smoke": "expect wizard-smoke.test.tcl",
39
+ "prepack": "nub run build"
40
+ },
41
+ "devDependencies": {
42
+ "@types/node": "^26",
43
+ "commander": "^15.0.0",
44
+ "typescript": "^7"
45
+ },
46
+ "dependencies": {
47
+ "@clack/prompts": "^1.7.0"
48
+ },
49
+ "peerDependencies": {
50
+ "commander": "^14.0.0 || ^15.0.0"
51
+ },
52
+ "exports": {
53
+ ".": {
54
+ "types": "./dist/index.d.ts",
55
+ "import": "./dist/index.js"
56
+ }
57
+ },
58
+ "types": "./dist/index.d.ts",
59
+ "files": [
60
+ "dist"
61
+ ],
62
+ "engines": {
63
+ "node": ">=22.12.0"
64
+ }
65
+ }