From f1c1190e346d268e9eb5b8439605632ee08c978d Mon Sep 17 00:00:00 2001 From: yosuke ota Date: Thu, 11 Dec 2025 17:26:24 +0900 Subject: [PATCH] feat(plugin-renderer): improve multiple positional argument usage display --- .../docs/src/guide/essentials/declarative.md | 2 +- .../src/__snapshots__/usage.test.ts.snap | 24 +++++ packages/plugin-renderer/src/usage.test.ts | 87 +++++++++++++++++++ packages/plugin-renderer/src/usage.ts | 11 ++- 4 files changed, 122 insertions(+), 2 deletions(-) diff --git a/packages/docs/src/guide/essentials/declarative.md b/packages/docs/src/guide/essentials/declarative.md index 6d83fa675..7a55a6e3e 100644 --- a/packages/docs/src/guide/essentials/declarative.md +++ b/packages/docs/src/guide/essentials/declarative.md @@ -165,7 +165,7 @@ Each option can have the following properties: - `description`: A description of what the option does - `default`: Default value if the option is not provided -- `required`: Set to `true` if the option is required (Note: Positional arguments defined with `type: 'positional'` are implicitly required by the parser). +- `required`: Set to `true` if the option is required (Note: Positional arguments defined with `type: 'positional'` without `multiple: true` are implicitly required by the parser). - `multiple`: Set to `true` if multiple option values are allowed - `toKebab`: Set to `true` to convert camelCase argument names to kebab-case in help text and command-line usage - `parse`: A function to parse and validate the argument value. Required when `type` is 'custom' diff --git a/packages/plugin-renderer/src/__snapshots__/usage.test.ts.snap b/packages/plugin-renderer/src/__snapshots__/usage.test.ts.snap index 20fa00fa8..82f02e5d6 100644 --- a/packages/plugin-renderer/src/__snapshots__/usage.test.ts.snap +++ b/packages/plugin-renderer/src/__snapshots__/usage.test.ts.snap @@ -97,6 +97,30 @@ OPTIONS: " `; +exports[`multiple positional arguments 1`] = ` +"A test command + +USAGE: + cmd1 test [ ...] + +ARGUMENTS: + foo The foo argument + bar The bar argument +" +`; + +exports[`multiple positional arguments with required 1`] = ` +"A test command + +USAGE: + cmd1 test [ ...] + +ARGUMENTS: + foo The foo argument + bar The bar argument +" +`; + exports[`no arguments 1`] = ` "A test command diff --git a/packages/plugin-renderer/src/usage.test.ts b/packages/plugin-renderer/src/usage.test.ts index 623aebb24..7287c2c69 100644 --- a/packages/plugin-renderer/src/usage.test.ts +++ b/packages/plugin-renderer/src/usage.test.ts @@ -323,6 +323,93 @@ test('mixed positionals and optionals', async () => { expect(await renderUsage(ctx)).toMatchSnapshot() }) +test('multiple positional arguments', async () => { + const command = { + args: { + foo: { + type: 'positional', + description: 'The foo argument' + }, + bar: { + type: 'positional', + description: 'The bar argument', + multiple: true + } + }, + name: 'test', + description: 'A test command', + run: NOOP + } as Command> + + const ctx = await createCommandContext({ + args: command.args!, + explicit: {}, + values: {}, + positionals: [], + rest: [], + argv: [], + tokens: [], // dummy, due to test + omitted: false, + callMode: 'subCommand', + command, + extensions: { + [i18nPlugin.id]: i18nPlugin.extension, + [rendererPlugin.id]: rendererPlugin.extension + }, + cliOptions: { + cwd: '/path/to/cmd1', + version: '0.0.0', + name: 'cmd1' + } + }) + + expect(await renderUsage(ctx)).toMatchSnapshot() +}) + +test('multiple positional arguments with required', async () => { + const command = { + args: { + foo: { + type: 'positional', + description: 'The foo argument' + }, + bar: { + type: 'positional', + description: 'The bar argument', + multiple: true, + required: true + } + }, + name: 'test', + description: 'A test command', + run: NOOP + } as Command> + + const ctx = await createCommandContext({ + args: command.args!, + explicit: {}, + values: {}, + positionals: [], + rest: [], + argv: [], + tokens: [], // dummy, due to test + omitted: false, + callMode: 'subCommand', + command, + extensions: { + [i18nPlugin.id]: i18nPlugin.extension, + [rendererPlugin.id]: rendererPlugin.extension + }, + cliOptions: { + cwd: '/path/to/cmd1', + version: '0.0.0', + name: 'cmd1' + } + }) + + expect(await renderUsage(ctx)).toMatchSnapshot() +}) + test('no examples', async () => { const command = { args: { diff --git a/packages/plugin-renderer/src/usage.ts b/packages/plugin-renderer/src/usage.ts index 5e2f21d05..7d095dcb4 100644 --- a/packages/plugin-renderer/src/usage.ts +++ b/packages/plugin-renderer/src/usage.ts @@ -552,7 +552,16 @@ async function generatePositionalArgsUsage< function generatePositionalSymbols(args: Args): string { return hasPositionalArgs(args) ? getPositionalArgs(args) - .map(([name]) => `<${name}>`) + .map(([name, arg]) => { + const elements: string[] = [] + if (!arg.multiple || arg.required) { + elements.push(`<${name}>`) + } + if (arg.multiple) { + elements.push(`[<${name}> ...]`) + } + return elements.join(' ') + }) .join(' ') : '' }