Compare commits

...
Author SHA1 Message Date
Tabish Bidiwale 2fa07b9216 Merge branch 'main' into TabishB/fix-issue-463 2026-01-09 16:54:04 -08:00
Tabish Bidiwale cc8aa95ebc fix: offer parent flags in Bash and PowerShell completions when subcommands exist
When a command has both flags and subcommands, the Bash and PowerShell
completion generators now check if the user is typing a flag (input
starts with `-`) before offering subcommand completions. This fixes the
issue where parent-level flags were never suggested.

Before: `openspec config --<TAB>` → Only showed subcommands
After: `openspec config --<TAB>` → Shows parent flags when input starts with `-`

Fixes #463
2026-01-09 16:52:16 -08:00
4 changed files with 127 additions and 0 deletions
@@ -83,6 +83,22 @@ complete -F _openspec_completion openspec
// Handle subcommands
if (cmd.subcommands && cmd.subcommands.length > 0) {
// First, check if user is typing a flag for the parent command
if (cmd.flags.length > 0) {
lines.push(`${indent}if [[ "$cur" == -* ]]; then`);
const flags = cmd.flags.map(f => {
const parts: string[] = [];
if (f.short) parts.push(`-${f.short}`);
parts.push(`--${f.name}`);
return parts.join(' ');
}).join(' ');
lines.push(`${indent} local flags="${flags}"`);
lines.push(`${indent} COMPREPLY=($(compgen -W "$flags" -- "$cur"))`);
lines.push(`${indent} return 0`);
lines.push(`${indent}fi`);
lines.push('');
}
lines.push(`${indent}if [[ $cword -eq 2 ]]; then`);
lines.push(`${indent} local subcommands="` + cmd.subcommands.map(s => this.escapeCommandName(s.name)).join(' ') + '"');
lines.push(`${indent} COMPREPLY=($(compgen -W "$subcommands" -- "$cur"))`);
@@ -74,6 +74,29 @@ Register-ArgumentCompleter -CommandName openspec -ScriptBlock $openspecCompleter
const lines: string[] = [];
if (cmd.subcommands && cmd.subcommands.length > 0) {
// First, check if user is typing a flag for the parent command
if (cmd.flags.length > 0) {
lines.push(`${indent}if ($wordToComplete -like "-*") {`);
lines.push(`${indent} $flags = @(`);
for (const flag of cmd.flags) {
const longFlag = `--${flag.name}`;
const shortFlag = flag.short ? `-${flag.short}` : undefined;
if (shortFlag) {
lines.push(`${indent} @{Name="${longFlag}"; Description="${this.escapeDescription(flag.description)}"},`);
lines.push(`${indent} @{Name="${shortFlag}"; Description="${this.escapeDescription(flag.description)}"},`);
} else {
lines.push(`${indent} @{Name="${longFlag}"; Description="${this.escapeDescription(flag.description)}"},`);
}
}
lines.push(`${indent} )`);
lines.push(`${indent} $flags | Where-Object { $_.Name -like "$wordToComplete*" } | ForEach-Object {`);
lines.push(`${indent} [System.Management.Automation.CompletionResult]::new($_.Name, $_.Name, "ParameterName", $_.Description)`);
lines.push(`${indent} }`);
lines.push(`${indent} return`);
lines.push(`${indent}}`);
lines.push('');
}
// Handle subcommands
lines.push(`${indent}if ($commandCount -eq 2 -or ($commandCount -eq 3 -and $wordToComplete)) {`);
lines.push(`${indent} $subcommands = @(`);
@@ -205,6 +205,50 @@ describe('BashGenerator', () => {
expect(script).toContain('list');
});
it('should offer parent flags when command has both flags and subcommands', () => {
const commands: CommandDefinition[] = [
{
name: 'config',
description: 'Manage configuration',
flags: [
{
name: 'scope',
short: 's',
description: 'Configuration scope',
},
{
name: 'json',
description: 'Output as JSON',
},
],
subcommands: [
{
name: 'set',
description: 'Set a config value',
flags: [],
},
{
name: 'get',
description: 'Get a config value',
flags: [],
},
],
},
];
const script = generator.generate(commands);
// Should check for flag prefix before offering subcommands
expect(script).toContain('if [[ "$cur" == -* ]]; then');
// Should include parent command flags
expect(script).toContain('-s');
expect(script).toContain('--scope');
expect(script).toContain('--json');
// Should also include subcommands
expect(script).toContain('set');
expect(script).toContain('get');
});
it('should handle positional arguments for change-id', () => {
const commands: CommandDefinition[] = [
{
@@ -224,6 +224,50 @@ describe('PowerShellGenerator', () => {
expect(script).toContain('List changes');
});
it('should offer parent flags when command has both flags and subcommands', () => {
const commands: CommandDefinition[] = [
{
name: 'config',
description: 'Manage configuration',
flags: [
{
name: 'scope',
short: 's',
description: 'Configuration scope',
},
{
name: 'json',
description: 'Output as JSON',
},
],
subcommands: [
{
name: 'set',
description: 'Set a config value',
flags: [],
},
{
name: 'get',
description: 'Get a config value',
flags: [],
},
],
},
];
const script = generator.generate(commands);
// Should check for flag prefix before offering subcommands
expect(script).toContain('if ($wordToComplete -like "-*")');
// Should include parent command flags
expect(script).toContain('-s');
expect(script).toContain('--scope');
expect(script).toContain('--json');
// Should also include subcommands
expect(script).toContain('"set"');
expect(script).toContain('"get"');
});
it('should handle positional arguments for change-id', () => {
const commands: CommandDefinition[] = [
{