@@ -15,31 +15,25 @@ export class FishGenerator implements CompletionGenerator {
1515 * @returns Fish completion script as a string
1616 */
1717 generate ( commands : CommandDefinition [ ] ) : string {
18- // Build top-level commands using push() for loop clarity
1918 const topLevelLines : string [ ] = [ ] ;
2019 for ( const cmd of commands ) {
2120 topLevelLines . push ( `# ${ cmd . name } command` ) ;
2221 topLevelLines . push (
23- `complete -c openspec -n '__fish_openspec_no_subcommand' -a '${ cmd . name } ' -d '${ this . escapeDescription ( cmd . description ) } '`
22+ `complete -c openspec -n '__fish_openspec_no_subcommand' -f - a '${ cmd . name } ' -d '${ this . escapeDescription ( cmd . description ) } '`
2423 ) ;
2524 }
2625 const topLevelCommands = topLevelLines . join ( '\n' ) ;
2726
28- // Build command-specific completions using push() for loop clarity
2927 const commandCompletionLines : string [ ] = [ ] ;
3028 for ( const cmd of commands ) {
3129 commandCompletionLines . push ( ...this . generateCommandCompletions ( cmd ) ) ;
3230 commandCompletionLines . push ( '' ) ;
3331 }
3432 const commandCompletions = commandCompletionLines . join ( '\n' ) ;
3533
36- // Static helper functions from template
3734 const helperFunctions = FISH_STATIC_HELPERS ;
38-
39- // Dynamic completion helpers from template
4035 const dynamicHelpers = FISH_DYNAMIC_HELPERS ;
4136
42- // Assemble final script with template literal
4337 return `# Fish completion script for OpenSpec CLI
4438# Auto-generated - do not edit manually
4539
@@ -56,42 +50,63 @@ ${commandCompletions}`;
5650 private generateCommandCompletions ( cmd : CommandDefinition ) : string [ ] {
5751 const lines : string [ ] = [ ] ;
5852
59- // If command has subcommands
6053 if ( cmd . subcommands && cmd . subcommands . length > 0 ) {
61- // Add subcommand completions
6254 for ( const subcmd of cmd . subcommands ) {
6355 lines . push (
64- `complete -c openspec -n '__fish_openspec_using_subcommand ${ cmd . name } ; and not __fish_openspec_using_subcommand ${ subcmd . name } ' -a '${ subcmd . name } ' -d '${ this . escapeDescription ( subcmd . description ) } '`
56+ `complete -c openspec -n '__fish_openspec_using_subcommand ${ cmd . name } ; and not __fish_openspec_using_subcommand ${ subcmd . name } ' -f - a '${ subcmd . name } ' -d '${ this . escapeDescription ( subcmd . description ) } '`
6557 ) ;
6658 }
6759 lines . push ( '' ) ;
6860
69- // Add flags for parent command
7061 for ( const flag of cmd . flags ) {
7162 lines . push ( ...this . generateFlagCompletion ( flag , `__fish_openspec_using_subcommand ${ cmd . name } ` ) ) ;
7263 }
7364
74- // Add completions for each subcommand
7565 for ( const subcmd of cmd . subcommands ) {
7666 lines . push ( `# ${ cmd . name } ${ subcmd . name } flags` ) ;
7767 for ( const flag of subcmd . flags ) {
78- lines . push ( ...this . generateFlagCompletion ( flag , `__fish_openspec_using_subcommand ${ cmd . name } ; and __fish_openspec_using_subcommand ${ subcmd . name } ` ) ) ;
68+ lines . push (
69+ ...this . generateFlagCompletion (
70+ flag ,
71+ `__fish_openspec_using_subcommand ${ cmd . name } ; and __fish_openspec_using_subcommand ${ subcmd . name } `
72+ )
73+ ) ;
7974 }
8075
81- // Add positional completions for subcommand
82- if ( subcmd . acceptsPositional ) {
83- lines . push ( ...this . generatePositionalCompletion ( subcmd . positionalType , `__fish_openspec_using_subcommand ${ cmd . name } ; and __fish_openspec_using_subcommand ${ subcmd . name } ` ) ) ;
76+ if ( subcmd . positionals ?. length ) {
77+ lines . push (
78+ ...this . generateIndexedPositionalCompletions (
79+ subcmd . positionals ,
80+ `__fish_openspec_using_subcommand ${ cmd . name } ; and __fish_openspec_using_subcommand ${ subcmd . name } ` ,
81+ this . collectValueFlags ( cmd . flags , subcmd . flags ) ,
82+ 2
83+ )
84+ ) ;
85+ } else if ( subcmd . acceptsPositional ) {
86+ lines . push (
87+ ...this . generatePositionalCompletion (
88+ subcmd . positionalType ,
89+ `__fish_openspec_using_subcommand ${ cmd . name } ; and __fish_openspec_using_subcommand ${ subcmd . name } `
90+ )
91+ ) ;
8492 }
8593 }
8694 } else {
87- // Command without subcommands
8895 lines . push ( `# ${ cmd . name } flags` ) ;
8996 for ( const flag of cmd . flags ) {
9097 lines . push ( ...this . generateFlagCompletion ( flag , `__fish_openspec_using_subcommand ${ cmd . name } ` ) ) ;
9198 }
9299
93- // Add positional completions
94- if ( cmd . acceptsPositional ) {
100+ if ( cmd . positionals ?. length ) {
101+ lines . push (
102+ ...this . generateIndexedPositionalCompletions (
103+ cmd . positionals ,
104+ `__fish_openspec_using_subcommand ${ cmd . name } ` ,
105+ this . collectValueFlags ( cmd . flags ) ,
106+ 1
107+ )
108+ ) ;
109+ } else if ( cmd . acceptsPositional ) {
95110 lines . push ( ...this . generatePositionalCompletion ( cmd . positionalType , `__fish_openspec_using_subcommand ${ cmd . name } ` ) ) ;
96111 }
97112 }
@@ -104,44 +119,23 @@ ${commandCompletions}`;
104119 */
105120 private generateFlagCompletion ( flag : FlagDefinition , condition : string ) : string [ ] {
106121 const lines : string [ ] = [ ] ;
107- const longFlag = `--${ flag . name } ` ;
108- const shortFlag = flag . short ? `-${ flag . short } ` : undefined ;
122+ const description = this . escapeDescription ( flag . description ) ;
123+ const shortFlag = flag . short ? `-s ${ flag . short } ` : '' ;
124+ const flagOptions = `${ shortFlag } -l ${ flag . name } ` ;
125+ const fileFallback = flag . completionType === 'path' ? '-r' : '-r -f' ;
109126
110127 if ( flag . takesValue && flag . values ) {
111- // Flag with enum values
112128 for ( const value of flag . values ) {
113- if ( shortFlag ) {
114- lines . push (
115- `complete -c openspec -n '${ condition } ' -s ${ flag . short } -l ${ flag . name } -a '${ value } ' -d '${ this . escapeDescription ( flag . description ) } '`
116- ) ;
117- } else {
118- lines . push (
119- `complete -c openspec -n '${ condition } ' -l ${ flag . name } -a '${ value } ' -d '${ this . escapeDescription ( flag . description ) } '`
120- ) ;
121- }
122- }
123- } else if ( flag . takesValue ) {
124- // Flag that takes a value but no specific values defined
125- if ( shortFlag ) {
126- lines . push (
127- `complete -c openspec -n '${ condition } ' -s ${ flag . short } -l ${ flag . name } -r -d '${ this . escapeDescription ( flag . description ) } '`
128- ) ;
129- } else {
130129 lines . push (
131- `complete -c openspec -n '${ condition } ' -l ${ flag . name } -r - d '${ this . escapeDescription ( flag . description ) } '`
130+ `complete -c openspec -n '${ condition } ' ${ flagOptions } -f -a ' ${ value } ' - d '${ description } '`
132131 ) ;
133132 }
133+ } else if ( flag . takesValue ) {
134+ lines . push (
135+ `complete -c openspec -n '${ condition } ' ${ flagOptions } ${ fileFallback } -d '${ description } '`
136+ ) ;
134137 } else {
135- // Boolean flag
136- if ( shortFlag ) {
137- lines . push (
138- `complete -c openspec -n '${ condition } ' -s ${ flag . short } -l ${ flag . name } -d '${ this . escapeDescription ( flag . description ) } '`
139- ) ;
140- } else {
141- lines . push (
142- `complete -c openspec -n '${ condition } ' -l ${ flag . name } -d '${ this . escapeDescription ( flag . description ) } '`
143- ) ;
144- }
138+ lines . push ( `complete -c openspec -n '${ condition } ' ${ flagOptions } -f -d '${ description } '` ) ;
145139 }
146140
147141 return lines ;
@@ -170,22 +164,65 @@ ${commandCompletions}`;
170164 lines . push ( `complete -c openspec -n '${ condition } ' -a 'zsh bash fish powershell' -f` ) ;
171165 break ;
172166 case 'path' :
173- // Fish automatically completes files, no need to specify
167+ // Fish automatically completes files for path arguments; no rule needed.
168+ break ;
169+ default :
170+ lines . push ( `complete -c openspec -n '${ condition } ' -f` ) ;
174171 break ;
175172 }
176173
177174 return lines ;
178175 }
179176
177+ /**
178+ * Generate indexed positional completions.
179+ */
180+ private generateIndexedPositionalCompletions (
181+ positionals : NonNullable < CommandDefinition [ 'positionals' ] > ,
182+ condition : string ,
183+ valueFlags : string [ ] ,
184+ depth : number
185+ ) : string [ ] {
186+ const lines : string [ ] = [ ] ;
187+
188+ for ( const [ index , positional ] of positionals . entries ( ) ) {
189+ const indexCondition = `${ condition } ; and __fish_openspec_positional_index ${ index } ${ depth } ${ valueFlags . length ? ` ${ valueFlags . join ( ' ' ) } ` : '' } ` ;
190+ lines . push ( ...this . generatePositionalCompletion ( positional . type , indexCondition ) ) ;
191+ }
192+
193+ return lines ;
194+ }
195+
196+ /**
197+ * Collect the long and short names for flags that consume the next token.
198+ */
199+ private collectValueFlags ( ...flagGroups : FlagDefinition [ ] [ ] ) : string [ ] {
200+ const flags = new Set < string > ( ) ;
201+
202+ for ( const group of flagGroups ) {
203+ for ( const flag of group ) {
204+ if ( ! flag . takesValue ) {
205+ continue ;
206+ }
207+
208+ flags . add ( `--${ flag . name } ` ) ;
209+ if ( flag . short ) {
210+ flags . add ( `-${ flag . short } ` ) ;
211+ }
212+ }
213+ }
214+
215+ return [ ...flags ] ;
216+ }
180217
181218 /**
182219 * Escape description text for Fish
183220 */
184221 private escapeDescription ( description : string ) : string {
185222 return description
186- . replace ( / \\ / g, '\\\\' ) // Backslashes first
187- . replace ( / ' / g, "\\'" ) // Single quotes
188- . replace ( / \$ / g, '\\$' ) // Dollar signs (prevents $())
189- . replace ( / ` / g, '\\`' ) ; // Backticks
223+ . replace ( / \\ / g, '\\\\' ) // Backslashes first
224+ . replace ( / ' / g, "\\'" ) // Single quotes
225+ . replace ( / \$ / g, '\\$' ) // Dollar signs (prevents $())
226+ . replace ( / ` / g, '\\`' ) ; // Backticks
190227 }
191228}
0 commit comments