Skip to content

Commit 9659f48

Browse files
clay-goodclaude
andcommitted
fix(completion): stop emitting empty switch blocks that break the PowerShell script
An empty switch body is a parse error in PowerShell, and the generator emitted one for every command whose positionals are all path-typed (18 in the current registry). PowerShell parses the entire file before execution, so the whole completion script failed to load. Skip the positional-index block when no positional produces completions. Fixes #1293 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
1 parent 0a99f41 commit 9659f48

3 files changed

Lines changed: 53 additions & 9 deletions

File tree

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
---
2+
"@fission-ai/openspec": patch
3+
---
4+
5+
fix(completion): make the PowerShell completion script parse and load again
6+
7+
The generated `OpenSpecCompletion.ps1` contained 18 empty `switch ($positionalIndex) { }` blocks — emitted for commands whose positionals are all `path`-typed (PowerShell completes paths natively, so those cases produce no clauses). A switch with no clauses is a PowerShell parse error ("Missing condition in switch statement clause"), and PowerShell parses the whole file before running it, so the script never loaded and completions never registered. The generator now skips the positional-index block entirely when no positional produces completions, so the script parses clean (18 → 0 errors) and tab completion works.

src/core/completions/generators/powershell-generator.ts

Lines changed: 15 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -196,6 +196,20 @@ Register-ArgumentCompleter -CommandName openspec -ScriptBlock $openspecCompleter
196196
firstPositionalTokenIndex: number,
197197
indent: string
198198
): string[] {
199+
const caseLines: string[] = [];
200+
for (const [index, positional] of positionals.entries()) {
201+
const completion = this.generatePositionalCompletion(positional.type, indent + ' ');
202+
if (completion.length === 0) continue;
203+
caseLines.push(`${indent} ${index} {`);
204+
caseLines.push(...completion);
205+
caseLines.push(`${indent} }`);
206+
}
207+
208+
// A switch with no clauses is a PowerShell parse error, so when no
209+
// positional produces completions skip the whole block (it would only
210+
// feed the empty switch anyway).
211+
if (caseLines.length === 0) return [];
212+
199213
const lines: string[] = [];
200214
const valueFlags = this.generateValueFlags(flags);
201215

@@ -228,15 +242,7 @@ Register-ArgumentCompleter -CommandName openspec -ScriptBlock $openspecCompleter
228242
lines.push(`${indent}}`);
229243
lines.push('');
230244
lines.push(`${indent}switch ($positionalIndex) {`);
231-
232-
for (const [index, positional] of positionals.entries()) {
233-
const completion = this.generatePositionalCompletion(positional.type, indent + ' ');
234-
if (completion.length === 0) continue;
235-
lines.push(`${indent} ${index} {`);
236-
lines.push(...completion);
237-
lines.push(`${indent} }`);
238-
}
239-
245+
lines.push(...caseLines);
240246
lines.push(`${indent}}`);
241247

242248
return lines;

test/core/completions/generators/powershell-generator.test.ts

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import { describe, it, expect, beforeEach } from 'vitest';
22
import { PowerShellGenerator } from '../../../../src/core/completions/generators/powershell-generator.js';
3+
import { COMMAND_REGISTRY } from '../../../../src/core/completions/command-registry.js';
34
import { CommandDefinition } from '../../../../src/core/completions/types.js';
45

56
describe('PowerShellGenerator', () => {
@@ -461,6 +462,36 @@ describe('PowerShellGenerator', () => {
461462
expect(script).toContain('Get-OpenSpecSpecs');
462463
});
463464

465+
it('should not emit an empty switch when no positional produces completions', () => {
466+
const commands: CommandDefinition[] = [
467+
{
468+
name: 'init',
469+
description: 'Initialize OpenSpec',
470+
flags: [
471+
{
472+
name: 'tools',
473+
description: 'AI tools to configure',
474+
takesValue: true,
475+
},
476+
],
477+
positionals: [{ name: 'path', type: 'path', optional: true }],
478+
},
479+
];
480+
481+
const script = generator.generate(commands);
482+
483+
// An empty switch body is a PowerShell parse error that aborts the
484+
// entire completion script ("Missing condition in switch statement clause").
485+
expect(script).not.toMatch(/switch \(\$positionalIndex\) \{\s*\}/);
486+
expect(script).not.toContain('$positionalIndex');
487+
});
488+
489+
it('should not emit empty switch blocks for the real command registry', () => {
490+
const script = generator.generate(COMMAND_REGISTRY);
491+
492+
expect(script).not.toMatch(/switch \(\$positionalIndex\) \{\s*\}/);
493+
});
494+
464495
it('should not emit trailing commas in @() arrays', () => {
465496
const commands: CommandDefinition[] = [
466497
{

0 commit comments

Comments
 (0)