feat-forge 1.0.3 → 1.2.3

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.
@@ -1,5 +1,3 @@
1
- import { readdir } from 'fs/promises';
2
- import { pathExists } from '../lib/fs.js';
3
1
  import { AbstractCommands } from './AbstractCommands.js';
4
2
  /**
5
3
  * Commands for managing shell completion/autocomplete
@@ -13,12 +11,6 @@ export class CompletionCommands extends AbstractCommands {
13
11
  // ============================================================================
14
12
  // PUBLIC COMMAND METHODS
15
13
  // ============================================================================
16
- /**
17
- * Generate and display shell completion script for the specified shell.
18
- * Outputs only the script to stdout for piping or sourcing.
19
- *
20
- * @param shell - The target shell type (bash, zsh, or fish)
21
- */
22
14
  /**
23
15
  * Generate and display shell completion script for the specified shell.
24
16
  * Outputs only the script to stdout for piping or sourcing.
@@ -34,9 +26,6 @@ export class CompletionCommands extends AbstractCommands {
34
26
  // ============================================================================
35
27
  /**
36
28
  * Generate the appropriate completion script based on shell type.
37
- *
38
- * @param shell - The target shell type
39
- * @returns The generated completion script as a string
40
29
  */
41
30
  async generateCompletionScript(shell) {
42
31
  switch (shell) {
@@ -53,33 +42,8 @@ export class CompletionCommands extends AbstractCommands {
53
42
  throw new Error(`Unsupported shell: ${shell}`);
54
43
  }
55
44
  }
56
- /**
57
- * Get list of available feature slugs for contextual completion.
58
- * Returns empty array if features directory doesn't exist or if there's an error.
59
- *
60
- * @returns Array of feature slugs
61
- */
62
- async getAvailableFeatures() {
63
- try {
64
- if (!(await pathExists(this.context.paths.worktreesRoot))) {
65
- return [];
66
- }
67
- const entries = await readdir(this.context.paths.worktreesRoot, { withFileTypes: true });
68
- return entries
69
- .filter((entry) => entry.isDirectory())
70
- .map((entry) => entry.name)
71
- .sort();
72
- }
73
- catch {
74
- return [];
75
- }
76
- }
77
45
  /**
78
46
  * Extract command information from Commander.js program.
79
- * Recursively extracts all commands and their subcommands.
80
- *
81
- * @param command - Commander.js Command object
82
- * @returns Structured command information
83
47
  */
84
48
  extractCommandInfo(command) {
85
49
  const name = command.name();
@@ -90,82 +54,167 @@ export class CompletionCommands extends AbstractCommands {
90
54
  }
91
55
  /**
92
56
  * Get all main commands from the program.
93
- *
94
- * @returns Array of command information
95
57
  */
96
58
  getMainCommands() {
97
59
  return this.program.commands.filter((cmd) => !cmd.name().includes('help')).map((cmd) => this.extractCommandInfo(cmd));
98
60
  }
99
61
  /**
100
62
  * Find a specific command by name.
101
- *
102
- * @param commandName - Name of the command to find
103
- * @returns Command information or undefined
104
63
  */
105
64
  findCommand(commandName) {
106
65
  return this.getMainCommands().find((cmd) => cmd.name === commandName);
107
66
  }
108
- escapeDoubleQuotes(value) {
109
- return value.replace(/\\/g, '\\\\').replace(/"/g, '\\"');
67
+ /**
68
+ * Centralized extraction of all completion metadata.
69
+ */
70
+ getCompletionData() {
71
+ const mainCommands = this.getMainCommands();
72
+ return {
73
+ mainCommands,
74
+ featureCmd: this.findCommand('feature'),
75
+ fixCmd: this.findCommand('fix'),
76
+ releaseCmd: this.findCommand('release'),
77
+ servicesCmd: this.findCommand('services'),
78
+ envCmd: this.findCommand('env'),
79
+ maintenanceCmd: this.findCommand('maintenance'),
80
+ modeCmd: this.findCommand('mode'),
81
+ agentCmd: this.findCommand('agent'),
82
+ slugSubcommands: ['stop', 'start', 'archive', 'resync', 'merge', 'rebase', 'open', 'path'],
83
+ rootSlugCommands: ['stop', 'start', 'archive', 'resync', 'merge', 'rebase', 'open', 'path'],
84
+ };
110
85
  }
111
- escapeSingleQuotes(value) {
112
- return value.replace(/'/g, "''");
86
+ /**
87
+ * Generate the Node.js config-reading snippet used by bash/zsh.
88
+ * Returns variables: _FORGE_WORKTREES_ROOT, _FORGE_FEATURE_PREFIX, _FORGE_FIX_PREFIX, _FORGE_RELEASE_PREFIX, _FORGE_MODES
89
+ */
90
+ nodeConfigSnippet() {
91
+ return `'const fs=require("fs");const path=require("path");
92
+ try{
93
+ const file=process.argv[1];
94
+ const configRoot=path.dirname(file);
95
+ const data=JSON.parse(fs.readFileSync(file,"utf8"));
96
+ let rootDir=data.rootDir;
97
+ if(rootDir){ if(!path.isAbsolute(rootDir)) rootDir=path.join(configRoot, rootDir); }
98
+ else { rootDir=configRoot; }
99
+ const opts=data.options||{};
100
+ const folders=opts.folders||data.folders||{};
101
+ const git=opts.git||data.git||{};
102
+ const worktrees=folders.worktrees||"worktrees";
103
+ const fp=git.featureBranchPrefix||"feature/";
104
+ const xp=git.fixBranchPrefix||"fix/";
105
+ const rp=git.releaseBranchPrefix||"release/";
106
+ const modes=(data.modes||[]).map(m=>m.name||path.basename(m.agentFile||"",".md")).filter(Boolean).join(" ");
107
+ process.stdout.write("_FORGE_WORKTREES_ROOT="+path.join(rootDir, worktrees)+"\\n");
108
+ process.stdout.write("_FORGE_FEATURE_PREFIX="+fp+"\\n");
109
+ process.stdout.write("_FORGE_FIX_PREFIX="+xp+"\\n");
110
+ process.stdout.write("_FORGE_RELEASE_PREFIX="+rp+"\\n");
111
+ process.stdout.write("_FORGE_MODES="+modes+"\\n");
112
+ }catch(e){}'`;
113
113
  }
114
114
  /**
115
- * Get list of command names that have a required slug argument.
116
- *
117
- * @returns Array of command names
115
+ * Generate the Node.js config-reading snippet for fish shell.
116
+ * Outputs `set -g` commands instead of VAR= assignments.
118
117
  */
119
- getCommandsWithSlug() {
120
- const commands = [];
121
- const checkCommand = (cmd, parentName) => {
122
- const fullName = parentName ? `${parentName}|${cmd.name}` : cmd.name;
123
- if (cmd.hasSlugArgument) {
124
- commands.push(cmd.name);
125
- }
126
- cmd.subcommands.forEach((sub) => checkCommand(sub, cmd.name));
127
- };
128
- this.getMainCommands().forEach((cmd) => checkCommand(cmd));
129
- return commands;
118
+ nodeConfigSnippetFish() {
119
+ return `'const fs=require("fs");const path=require("path");
120
+ try{
121
+ const file=process.argv[1];
122
+ const configRoot=path.dirname(file);
123
+ const data=JSON.parse(fs.readFileSync(file,"utf8"));
124
+ let rootDir=data.rootDir;
125
+ if(rootDir){ if(!path.isAbsolute(rootDir)) rootDir=path.join(configRoot, rootDir); }
126
+ else { rootDir=configRoot; }
127
+ const opts=data.options||{};
128
+ const folders=opts.folders||data.folders||{};
129
+ const git=opts.git||data.git||{};
130
+ const worktrees=folders.worktrees||"worktrees";
131
+ const fp=git.featureBranchPrefix||"feature/";
132
+ const xp=git.fixBranchPrefix||"fix/";
133
+ const rp=git.releaseBranchPrefix||"release/";
134
+ const modes=(data.modes||[]).map(m=>m.name||path.basename(m.agentFile||"",".md")).filter(Boolean).join(" ");
135
+ process.stdout.write("set -g _FORGE_WORKTREES_ROOT "+path.join(rootDir, worktrees)+"\\n");
136
+ process.stdout.write("set -g _FORGE_FEATURE_PREFIX "+fp+"\\n");
137
+ process.stdout.write("set -g _FORGE_FIX_PREFIX "+xp+"\\n");
138
+ process.stdout.write("set -g _FORGE_RELEASE_PREFIX "+rp+"\\n");
139
+ process.stdout.write("set -g _FORGE_MODES "+modes+"\\n");
140
+ }catch(e){}'`;
130
141
  }
131
142
  /**
132
- * Generate bash completion script.
133
- *
134
- * @returns Bash completion script content
143
+ * Generate the Node.js config-reading snippet for PowerShell.
144
+ * Outputs a JSON object parsable with ConvertFrom-Json.
135
145
  */
146
+ nodeConfigSnippetPowerShell() {
147
+ return `'const fs=require("fs");const path=require("path");
148
+ try{
149
+ const file=process.argv[1];
150
+ const configRoot=path.dirname(file);
151
+ const data=JSON.parse(fs.readFileSync(file,"utf8"));
152
+ let rootDir=data.rootDir;
153
+ if(rootDir){ if(!path.isAbsolute(rootDir)) rootDir=path.join(configRoot, rootDir); }
154
+ else { rootDir=configRoot; }
155
+ const opts=data.options||{};
156
+ const folders=opts.folders||data.folders||{};
157
+ const git=opts.git||data.git||{};
158
+ const worktrees=folders.worktrees||"worktrees";
159
+ const fp=git.featureBranchPrefix||"feature/";
160
+ const xp=git.fixBranchPrefix||"fix/";
161
+ const rp=git.releaseBranchPrefix||"release/";
162
+ const modes=(data.modes||[]).map(m=>m.name||path.basename(m.agentFile||"",".md")).filter(Boolean);
163
+ process.stdout.write(JSON.stringify({worktreesRoot:path.join(rootDir,worktrees),featurePrefix:fp,fixPrefix:xp,releasePrefix:rp,modes:modes}));
164
+ }catch(e){process.stdout.write("{}");}'`;
165
+ }
166
+ // ============================================================================
167
+ // BASH COMPLETION
168
+ // ============================================================================
136
169
  generateBashCompletion() {
137
- const mainCommands = this.getMainCommands();
138
- const featureCmd = this.findCommand('feature');
139
- const modeCmd = this.findCommand('mode');
140
- const agentCmd = this.findCommand('agent');
141
- // Build command lists
142
- const commands = mainCommands.map((cmd) => cmd.name).join(' ');
143
- const featureCommands = featureCmd?.subcommands.map((cmd) => cmd.name).join(' ') || '';
144
- const modeCommands = modeCmd?.subcommands.map((cmd) => cmd.name).join(' ') || '';
145
- const agentCommands = agentCmd?.subcommands.map((cmd) => cmd.name).join(' ') || '';
146
- // Limit slug completions to active feature commands only
147
- const activeFeatureSlugCommands = ['stop', 'archive', 'resync', 'merge', 'rebase', 'open'];
148
- const featureWithActiveSlug = featureCmd?.subcommands.filter((cmd) => activeFeatureSlugCommands.includes(cmd.name)).map((cmd) => cmd.name) || [];
149
- const mainWithActiveSlug = mainCommands.filter((cmd) => ['merge', 'rebase', 'open'].includes(cmd.name)).map((cmd) => cmd.name);
150
- const featureSlugCase = featureWithActiveSlug.length > 0
151
- ? ` ${featureWithActiveSlug.join('|')})
152
- # Suggest available features
170
+ const data = this.getCompletionData();
171
+ const commands = data.mainCommands.map((cmd) => cmd.name).join(' ');
172
+ const featureCommands = data.featureCmd?.subcommands.map((cmd) => cmd.name).join(' ') || '';
173
+ const fixCommands = data.fixCmd?.subcommands.map((cmd) => cmd.name).join(' ') || '';
174
+ const releaseCommands = data.releaseCmd?.subcommands.map((cmd) => cmd.name).join(' ') || '';
175
+ const servicesCommands = data.servicesCmd?.subcommands.map((cmd) => cmd.name).join(' ') || '';
176
+ const envCommands = data.envCmd?.subcommands.map((cmd) => cmd.name).join(' ') || '';
177
+ const maintenanceCommands = data.maintenanceCmd?.subcommands.map((cmd) => cmd.name).join(' ') || '';
178
+ const agentCommands = data.agentCmd?.subcommands.map((cmd) => cmd.name).join(' ') || '';
179
+ const featureSlugCmds = data.featureCmd?.subcommands.filter((cmd) => data.slugSubcommands.includes(cmd.name)).map((cmd) => cmd.name) || [];
180
+ const fixSlugCmds = data.fixCmd?.subcommands.filter((cmd) => data.slugSubcommands.includes(cmd.name)).map((cmd) => cmd.name) || [];
181
+ const releaseSlugCmds = data.releaseCmd?.subcommands.filter((cmd) => data.slugSubcommands.includes(cmd.name)).map((cmd) => cmd.name) || [];
182
+ const mainSlugCmds = data.mainCommands.filter((cmd) => data.rootSlugCommands.includes(cmd.name)).map((cmd) => cmd.name);
183
+ const featureSlugCase = featureSlugCmds.length > 0
184
+ ? ` ${featureSlugCmds.join('|')})
185
+ if [[ \${cword} -eq 3 ]]; then
186
+ local slugs="\$(_forge_feature_slugs)"
187
+ COMPREPLY=( \$(compgen -W "\${slugs}" -- "\${cur}") )
188
+ return 0
189
+ fi
190
+ ;;
191
+ `
192
+ : '';
193
+ const fixSlugCase = fixSlugCmds.length > 0
194
+ ? ` ${fixSlugCmds.join('|')})
195
+ if [[ \${cword} -eq 3 ]]; then
196
+ local slugs="\$(_forge_fix_slugs)"
197
+ COMPREPLY=( \$(compgen -W "\${slugs}" -- "\${cur}") )
198
+ return 0
199
+ fi
200
+ ;;
201
+ `
202
+ : '';
203
+ const releaseSlugCase = releaseSlugCmds.length > 0
204
+ ? ` ${releaseSlugCmds.join('|')})
153
205
  if [[ \${cword} -eq 3 ]]; then
154
- local worktrees_root="\$(_forge_worktrees_root)"
155
- local features=\$(find "\${worktrees_root}" -mindepth 1 -maxdepth 1 -type d -exec basename {} \\; 2>/dev/null)
156
- COMPREPLY=( \$(compgen -W "\${features}" -- "\${cur}") )
206
+ local slugs="\$(_forge_release_slugs)"
207
+ COMPREPLY=( \$(compgen -W "\${slugs}" -- "\${cur}") )
157
208
  return 0
158
209
  fi
159
210
  ;;
160
211
  `
161
212
  : '';
162
- const mainSlugCase = mainWithActiveSlug.length > 0
163
- ? ` ${mainWithActiveSlug.join('|')})
164
- # Suggest available features for commands with slug argument
213
+ const mainSlugCase = mainSlugCmds.length > 0
214
+ ? ` ${mainSlugCmds.join('|')})
165
215
  if [[ \${cword} -eq 2 ]]; then
166
- local worktrees_root="\$(_forge_worktrees_root)"
167
- local features=\$(find "\${worktrees_root}" -mindepth 1 -maxdepth 1 -type d -exec basename {} \\; 2>/dev/null)
168
- COMPREPLY=( \$(compgen -W "\${features}" -- "\${cur}") )
216
+ local branches="\$(_forge_all_branches)"
217
+ COMPREPLY=( \$(compgen -W "\${branches}" -- "\${cur}") )
169
218
  return 0
170
219
  fi
171
220
  ;;
@@ -185,57 +234,125 @@ _forge_find_config() {
185
234
  return 1
186
235
  }
187
236
 
188
- _forge_worktrees_root() {
189
- if [[ -n "\${FORGE_WORKTREES_ROOT}" ]]; then
190
- echo "\${FORGE_WORKTREES_ROOT}"
237
+ # Load forge config once and cache variables for the current completion
238
+ _forge_config_loaded=0
239
+ _forge_load_config() {
240
+ if [[ \${_forge_config_loaded} -eq 1 ]]; then
191
241
  return 0
192
242
  fi
243
+ _FORGE_WORKTREES_ROOT=""
244
+ _FORGE_FEATURE_PREFIX="feature/"
245
+ _FORGE_FIX_PREFIX="fix/"
246
+ _FORGE_RELEASE_PREFIX="release/"
247
+ _FORGE_MODES=""
248
+
249
+ if [[ -n "\${FORGE_WORKTREES_ROOT}" ]]; then
250
+ _FORGE_WORKTREES_ROOT="\${FORGE_WORKTREES_ROOT}"
251
+ fi
252
+
193
253
  local start="\${PWD}"
194
254
  local config_root
195
255
  config_root="\$(_forge_find_config "\${start}")" || true
196
256
  if [[ -n "\${config_root}" ]]; then
197
- local worktrees
198
- worktrees=\$(node -e 'const fs=require("fs");const path=require("path");
199
- try{
200
- const file=process.argv[1];
201
- const configRoot=path.dirname(file);
202
- const data=JSON.parse(fs.readFileSync(file,"utf8"));
203
- let rootDir=data.rootDir;
204
- if(rootDir){ if(!path.isAbsolute(rootDir)) rootDir=path.join(configRoot, rootDir); }
205
- else { rootDir=configRoot; }
206
- const folders=(data.options && data.options.folders) || data.folders || {};
207
- const worktrees=folders.worktrees || "features";
208
- process.stdout.write(path.join(rootDir, worktrees));
209
- }catch(e){}
210
- ' "\${config_root}/.feat-forge.json" 2>/dev/null)
211
- if [[ -n "\${worktrees}" ]]; then
212
- echo "\${worktrees}"
213
- return 0
257
+ local output
258
+ output=\$(node -e ${this.nodeConfigSnippet()} "\${config_root}/.feat-forge.json" 2>/dev/null)
259
+ if [[ -n "\${output}" ]]; then
260
+ eval "\${output}"
261
+ elif [[ -z "\${_FORGE_WORKTREES_ROOT}" ]]; then
262
+ _FORGE_WORKTREES_ROOT="\${config_root}/worktrees"
214
263
  fi
215
- echo "\${config_root}/features"
216
- return 0
264
+ elif [[ -z "\${_FORGE_WORKTREES_ROOT}" ]]; then
265
+ _FORGE_WORKTREES_ROOT="worktrees"
217
266
  fi
218
- echo "features"
267
+ _forge_config_loaded=1
268
+ }
269
+
270
+ _forge_feature_slugs() {
271
+ _forge_load_config
272
+ local prefix_dir="\${_FORGE_FEATURE_PREFIX%/}"
273
+ local dir="\${_FORGE_WORKTREES_ROOT}/\${prefix_dir}"
274
+ if [[ -d "\${dir}" ]]; then
275
+ find "\${dir}" -mindepth 1 -maxdepth 1 -type d -exec basename {} \\; 2>/dev/null
276
+ fi
277
+ }
278
+
279
+ _forge_fix_slugs() {
280
+ _forge_load_config
281
+ local prefix_dir="\${_FORGE_FIX_PREFIX%/}"
282
+ local dir="\${_FORGE_WORKTREES_ROOT}/\${prefix_dir}"
283
+ if [[ -d "\${dir}" ]]; then
284
+ find "\${dir}" -mindepth 1 -maxdepth 1 -type d -exec basename {} \\; 2>/dev/null
285
+ fi
286
+ }
287
+
288
+ _forge_release_slugs() {
289
+ _forge_load_config
290
+ local prefix_dir="\${_FORGE_RELEASE_PREFIX%/}"
291
+ local dir="\${_FORGE_WORKTREES_ROOT}/\${prefix_dir}"
292
+ if [[ -d "\${dir}" ]]; then
293
+ find "\${dir}" -mindepth 1 -maxdepth 1 -type d -exec basename {} \\; 2>/dev/null
294
+ fi
295
+ }
296
+
297
+ _forge_all_branches() {
298
+ _forge_load_config
299
+ local root="\${_FORGE_WORKTREES_ROOT}"
300
+ if [[ ! -d "\${root}" ]]; then
301
+ return
302
+ fi
303
+ local prefix_dirs=()
304
+ for p in "\${_FORGE_FEATURE_PREFIX%/}" "\${_FORGE_FIX_PREFIX%/}" "\${_FORGE_RELEASE_PREFIX%/}"; do
305
+ if [[ -n "\${p}" ]]; then
306
+ prefix_dirs+=("\${p}")
307
+ fi
308
+ done
309
+ for entry in "\${root}"/*/; do
310
+ [[ -d "\${entry}" ]] || continue
311
+ local name="\$(basename "\${entry}")"
312
+ local is_prefix=0
313
+ for pd in "\${prefix_dirs[@]}"; do
314
+ if [[ "\${name}" == "\${pd}" ]]; then
315
+ is_prefix=1
316
+ break
317
+ fi
318
+ done
319
+ if [[ \${is_prefix} -eq 1 ]]; then
320
+ for sub in "\${entry}"*/; do
321
+ [[ -d "\${sub}" ]] || continue
322
+ echo "\${name}/\$(basename "\${sub}")"
323
+ done
324
+ else
325
+ echo "\${name}"
326
+ fi
327
+ done
328
+ }
329
+
330
+ _forge_modes() {
331
+ _forge_load_config
332
+ echo "\${_FORGE_MODES}"
219
333
  }
220
334
 
221
335
  _forge_completion() {
222
336
  local cur prev words cword
223
337
  _init_completion || return
224
338
 
225
- # Main commands available at root level
339
+ # Reset config cache for each completion
340
+ _forge_config_loaded=0
341
+
226
342
  local commands="${commands}"
227
343
 
228
- # Subcommands for each main command
229
344
  local feature_commands="${featureCommands}"
230
- local mode_commands="${modeCommands}"
345
+ local fix_commands="${fixCommands}"
346
+ local release_commands="${releaseCommands}"
347
+ local services_commands="${servicesCommands}"
348
+ local env_commands="${envCommands}"
349
+ local maintenance_commands="${maintenanceCommands}"
231
350
  local agent_commands="${agentCommands}"
232
351
 
233
- # Get previous word for context
234
352
  case "\${words[1]}" in
235
353
  feature)
236
354
  case "\${words[2]}" in
237
355
  ${featureSlugCase} *)
238
- # Suggest feature subcommands
239
356
  if [[ \${cword} -eq 2 ]]; then
240
357
  COMPREPLY=( \$(compgen -W "\${feature_commands}" -- "\${cur}") )
241
358
  return 0
@@ -243,29 +360,75 @@ ${featureSlugCase} *)
243
360
  ;;
244
361
  esac
245
362
  ;;
363
+ fix)
364
+ case "\${words[2]}" in
365
+ ${fixSlugCase} *)
366
+ if [[ \${cword} -eq 2 ]]; then
367
+ COMPREPLY=( \$(compgen -W "\${fix_commands}" -- "\${cur}") )
368
+ return 0
369
+ fi
370
+ ;;
371
+ esac
372
+ ;;
373
+ release)
374
+ case "\${words[2]}" in
375
+ ${releaseSlugCase} *)
376
+ if [[ \${cword} -eq 2 ]]; then
377
+ COMPREPLY=( \$(compgen -W "\${release_commands}" -- "\${cur}") )
378
+ return 0
379
+ fi
380
+ ;;
381
+ esac
382
+ ;;
383
+ services)
384
+ if [[ \${cword} -eq 2 ]]; then
385
+ COMPREPLY=( \$(compgen -W "\${services_commands}" -- "\${cur}") )
386
+ return 0
387
+ fi
388
+ ;;
389
+ env)
390
+ if [[ \${cword} -eq 2 ]]; then
391
+ COMPREPLY=( \$(compgen -W "\${env_commands}" -- "\${cur}") )
392
+ return 0
393
+ fi
394
+ ;;
395
+ maintenance)
396
+ if [[ \${cword} -eq 2 ]]; then
397
+ COMPREPLY=( \$(compgen -W "\${maintenance_commands}" -- "\${cur}") )
398
+ return 0
399
+ fi
400
+ if [[ \${cword} -eq 3 ]]; then
401
+ local branches="\$(_forge_all_branches)"
402
+ COMPREPLY=( \$(compgen -W "\${branches}" -- "\${cur}") )
403
+ return 0
404
+ fi
405
+ ;;
246
406
  mode)
247
- # Suggest mode subcommands
248
407
  if [[ \${cword} -eq 2 ]]; then
249
- COMPREPLY=( \$(compgen -W "\${mode_commands}" -- "\${cur}") )
408
+ local modes="\$(_forge_modes)"
409
+ COMPREPLY=( \$(compgen -W "\${modes}" -- "\${cur}") )
250
410
  return 0
251
411
  fi
252
412
  ;;
253
413
  agent)
254
- # Suggest agent subcommands
255
414
  if [[ \${cword} -eq 2 ]]; then
256
415
  COMPREPLY=( \$(compgen -W "\${agent_commands}" -- "\${cur}") )
257
416
  return 0
258
417
  fi
259
418
  ;;
260
419
  ${mainSlugCase} completion)
261
- # Suggest shell types for completion
420
+ if [[ \${cword} -eq 2 ]]; then
421
+ COMPREPLY=( \$(compgen -W "bash zsh fish powershell pwsh" -- "\${cur}") )
422
+ return 0
423
+ fi
424
+ ;;
425
+ alias)
262
426
  if [[ \${cword} -eq 2 ]]; then
263
427
  COMPREPLY=( \$(compgen -W "bash zsh fish powershell pwsh" -- "\${cur}") )
264
428
  return 0
265
429
  fi
266
430
  ;;
267
431
  *)
268
- # Suggest main commands at root level
269
432
  if [[ \${cword} -eq 1 ]]; then
270
433
  COMPREPLY=( \$(compgen -W "\${commands}" -- "\${cur}") )
271
434
  return 0
@@ -284,44 +447,60 @@ complete -F _forge_completion forge
284
447
  # Option 2 - Save to file:
285
448
  # forge completion bash > ~/.local/share/bash-completion/completions/forge
286
449
  # # Or system-wide: /etc/bash_completion.d/forge
450
+ #
451
+ # Tip: Run 'forge alias bash' to generate shell aliases for quick access.
287
452
  `;
288
453
  }
289
- /**
290
- * Generate zsh completion script.
291
- *
292
- * @returns Zsh completion script content
293
- */
454
+ // ============================================================================
455
+ // ZSH COMPLETION
456
+ // ============================================================================
294
457
  generateZshCompletion() {
295
- const mainCommands = this.getMainCommands();
296
- const featureCmd = this.findCommand('feature');
297
- const modeCmd = this.findCommand('mode');
298
- const agentCmd = this.findCommand('agent');
299
- // Build command arrays with descriptions
300
- const commandsArray = mainCommands.map((cmd) => ` '${cmd.name}:${cmd.description.replace(/'/g, "''")}'`).join('\n');
301
- const featureArray = featureCmd?.subcommands.map((cmd) => ` '${cmd.name}:${cmd.description.replace(/'/g, "''")}'`).join('\n') || '';
302
- const modeArray = modeCmd?.subcommands.map((cmd) => ` '${cmd.name}:${cmd.description.replace(/'/g, "''")}'`).join('\n') || '';
303
- const agentArray = agentCmd?.subcommands.map((cmd) => ` '${cmd.name}:${cmd.description.replace(/'/g, "''")}'`).join('\n') || '';
304
- // Limit slug completions to active feature commands only
305
- const activeFeatureSlugCommands = ['stop', 'archive', 'resync', 'merge', 'rebase', 'open'];
306
- const featureWithActiveSlug = featureCmd?.subcommands.filter((cmd) => activeFeatureSlugCommands.includes(cmd.name)).map((cmd) => cmd.name) || [];
307
- const mainWithActiveSlug = mainCommands.filter((cmd) => ['merge', 'rebase', 'open'].includes(cmd.name)).map((cmd) => cmd.name);
308
- const featureSlugCase = featureWithActiveSlug.length > 0
309
- ? ` ${featureWithActiveSlug.join('|')})
310
- # Suggest available features
311
- local features
312
- local worktrees_root="\$(_forge_worktrees_root)"
313
- features=(\${(f)"\$(find "\${worktrees_root}" -mindepth 1 -maxdepth 1 -type d -exec basename {} \\; 2>/dev/null)"})
314
- _describe 'feature slug' features
458
+ const data = this.getCompletionData();
459
+ const commandsArray = data.mainCommands
460
+ .map((cmd) => ` '${cmd.name}:${cmd.description.replace(/'/g, "''")}'`)
461
+ .join('\n');
462
+ const featureArray = data.featureCmd?.subcommands.map((cmd) => ` '${cmd.name}:${cmd.description.replace(/'/g, "''")}'`).join('\n') || '';
463
+ const fixArray = data.fixCmd?.subcommands.map((cmd) => ` '${cmd.name}:${cmd.description.replace(/'/g, "''")}'`).join('\n') || '';
464
+ const releaseArray = data.releaseCmd?.subcommands.map((cmd) => ` '${cmd.name}:${cmd.description.replace(/'/g, "''")}'`).join('\n') || '';
465
+ const servicesArray = data.servicesCmd?.subcommands.map((cmd) => ` '${cmd.name}:${cmd.description.replace(/'/g, "''")}'`).join('\n') ||
466
+ '';
467
+ const envArray = data.envCmd?.subcommands.map((cmd) => ` '${cmd.name}:${cmd.description.replace(/'/g, "''")}'`).join('\n') || '';
468
+ const maintenanceArray = data.maintenanceCmd?.subcommands.map((cmd) => ` '${cmd.name}:${cmd.description.replace(/'/g, "''")}'`).join('\n') ||
469
+ '';
470
+ const agentArray = data.agentCmd?.subcommands.map((cmd) => ` '${cmd.name}:${cmd.description.replace(/'/g, "''")}'`).join('\n') || '';
471
+ const featureSlugCmds = data.featureCmd?.subcommands.filter((cmd) => data.slugSubcommands.includes(cmd.name)).map((cmd) => cmd.name) || [];
472
+ const fixSlugCmds = data.fixCmd?.subcommands.filter((cmd) => data.slugSubcommands.includes(cmd.name)).map((cmd) => cmd.name) || [];
473
+ const releaseSlugCmds = data.releaseCmd?.subcommands.filter((cmd) => data.slugSubcommands.includes(cmd.name)).map((cmd) => cmd.name) || [];
474
+ const mainSlugCmds = data.mainCommands.filter((cmd) => data.rootSlugCommands.includes(cmd.name)).map((cmd) => cmd.name);
475
+ const featureSlugCase = featureSlugCmds.length > 0
476
+ ? ` ${featureSlugCmds.join('|')})
477
+ local -a slugs
478
+ slugs=(\${(f)"\$(_forge_feature_slugs)"})
479
+ _describe 'feature slug' slugs
315
480
  ;;
316
481
  `
317
482
  : '';
318
- const mainSlugCase = mainWithActiveSlug.length > 0
319
- ? ` ${mainWithActiveSlug.join('|')})
320
- # Suggest available features for shortcut commands
321
- local features
322
- local worktrees_root="\$(_forge_worktrees_root)"
323
- features=(\${(f)"\$(find "\${worktrees_root}" -mindepth 1 -maxdepth 1 -type d -exec basename {} \\; 2>/dev/null)"})
324
- _describe 'feature slug' features
483
+ const fixSlugCase = fixSlugCmds.length > 0
484
+ ? ` ${fixSlugCmds.join('|')})
485
+ local -a slugs
486
+ slugs=(\${(f)"\$(_forge_fix_slugs)"})
487
+ _describe 'fix slug' slugs
488
+ ;;
489
+ `
490
+ : '';
491
+ const releaseSlugCase = releaseSlugCmds.length > 0
492
+ ? ` ${releaseSlugCmds.join('|')})
493
+ local -a slugs
494
+ slugs=(\${(f)"\$(_forge_release_slugs)"})
495
+ _describe 'release slug' slugs
496
+ ;;
497
+ `
498
+ : '';
499
+ const mainSlugCase = mainSlugCmds.length > 0
500
+ ? ` ${mainSlugCmds.join('|')})
501
+ local -a branches
502
+ branches=(\${(f)"\$(_forge_all_branches)"})
503
+ _describe 'branch name' branches
325
504
  ;;
326
505
  `
327
506
  : '';
@@ -340,41 +519,111 @@ _forge_find_config() {
340
519
  return 1
341
520
  }
342
521
 
343
- _forge_worktrees_root() {
344
- if [[ -n "\${FORGE_WORKTREES_ROOT}" ]]; then
345
- echo "\${FORGE_WORKTREES_ROOT}"
522
+ # Load forge config once and cache variables for the current completion
523
+ _forge_config_loaded=0
524
+ _forge_load_config() {
525
+ if [[ \${_forge_config_loaded} -eq 1 ]]; then
346
526
  return 0
347
527
  fi
528
+ _FORGE_WORKTREES_ROOT=""
529
+ _FORGE_FEATURE_PREFIX="feature/"
530
+ _FORGE_FIX_PREFIX="fix/"
531
+ _FORGE_RELEASE_PREFIX="release/"
532
+ _FORGE_MODES=""
533
+
534
+ if [[ -n "\${FORGE_WORKTREES_ROOT}" ]]; then
535
+ _FORGE_WORKTREES_ROOT="\${FORGE_WORKTREES_ROOT}"
536
+ fi
537
+
348
538
  local start="\${PWD}"
349
539
  local config_root
350
540
  config_root="\$(_forge_find_config "\${start}")" || true
351
541
  if [[ -n "\${config_root}" ]]; then
352
- local worktrees
353
- worktrees=\$(node -e 'const fs=require("fs");const path=require("path");
354
- try{
355
- const file=process.argv[1];
356
- const configRoot=path.dirname(file);
357
- const data=JSON.parse(fs.readFileSync(file,"utf8"));
358
- let rootDir=data.rootDir;
359
- if(rootDir){ if(!path.isAbsolute(rootDir)) rootDir=path.join(configRoot, rootDir); }
360
- else { rootDir=configRoot; }
361
- const folders=(data.options && data.options.folders) || data.folders || {};
362
- const worktrees=folders.worktrees || "features";
363
- process.stdout.write(path.join(rootDir, worktrees));
364
- }catch(e){}
365
- ' "\${config_root}/.feat-forge.json" 2>/dev/null)
366
- if [[ -n "\${worktrees}" ]]; then
367
- echo "\${worktrees}"
368
- return 0
542
+ local output
543
+ output=\$(node -e ${this.nodeConfigSnippet()} "\${config_root}/.feat-forge.json" 2>/dev/null)
544
+ if [[ -n "\${output}" ]]; then
545
+ eval "\${output}"
546
+ elif [[ -z "\${_FORGE_WORKTREES_ROOT}" ]]; then
547
+ _FORGE_WORKTREES_ROOT="\${config_root}/worktrees"
369
548
  fi
370
- echo "\${config_root}/features"
371
- return 0
549
+ elif [[ -z "\${_FORGE_WORKTREES_ROOT}" ]]; then
550
+ _FORGE_WORKTREES_ROOT="worktrees"
372
551
  fi
373
- echo "features"
552
+ _forge_config_loaded=1
553
+ }
554
+
555
+ _forge_feature_slugs() {
556
+ _forge_load_config
557
+ local prefix_dir="\${_FORGE_FEATURE_PREFIX%/}"
558
+ local dir="\${_FORGE_WORKTREES_ROOT}/\${prefix_dir}"
559
+ if [[ -d "\${dir}" ]]; then
560
+ find "\${dir}" -mindepth 1 -maxdepth 1 -type d -exec basename {} \\; 2>/dev/null
561
+ fi
562
+ }
563
+
564
+ _forge_fix_slugs() {
565
+ _forge_load_config
566
+ local prefix_dir="\${_FORGE_FIX_PREFIX%/}"
567
+ local dir="\${_FORGE_WORKTREES_ROOT}/\${prefix_dir}"
568
+ if [[ -d "\${dir}" ]]; then
569
+ find "\${dir}" -mindepth 1 -maxdepth 1 -type d -exec basename {} \\; 2>/dev/null
570
+ fi
571
+ }
572
+
573
+ _forge_release_slugs() {
574
+ _forge_load_config
575
+ local prefix_dir="\${_FORGE_RELEASE_PREFIX%/}"
576
+ local dir="\${_FORGE_WORKTREES_ROOT}/\${prefix_dir}"
577
+ if [[ -d "\${dir}" ]]; then
578
+ find "\${dir}" -mindepth 1 -maxdepth 1 -type d -exec basename {} \\; 2>/dev/null
579
+ fi
580
+ }
581
+
582
+ _forge_all_branches() {
583
+ _forge_load_config
584
+ local root="\${_FORGE_WORKTREES_ROOT}"
585
+ if [[ ! -d "\${root}" ]]; then
586
+ return
587
+ fi
588
+ local prefix_dirs=()
589
+ for p in "\${_FORGE_FEATURE_PREFIX%/}" "\${_FORGE_FIX_PREFIX%/}" "\${_FORGE_RELEASE_PREFIX%/}"; do
590
+ if [[ -n "\${p}" ]]; then
591
+ prefix_dirs+=("\${p}")
592
+ fi
593
+ done
594
+ for entry in "\${root}"/*/; do
595
+ [[ -d "\${entry}" ]] || continue
596
+ local name="\$(basename "\${entry}")"
597
+ local is_prefix=0
598
+ for pd in "\${prefix_dirs[@]}"; do
599
+ if [[ "\${name}" == "\${pd}" ]]; then
600
+ is_prefix=1
601
+ break
602
+ fi
603
+ done
604
+ if [[ \${is_prefix} -eq 1 ]]; then
605
+ for sub in "\${entry}"*/; do
606
+ [[ -d "\${sub}" ]] || continue
607
+ echo "\${name}/\$(basename "\${sub}")"
608
+ done
609
+ else
610
+ echo "\${name}"
611
+ fi
612
+ done
613
+ }
614
+
615
+ _forge_modes() {
616
+ _forge_load_config
617
+ echo "\${_FORGE_MODES}"
374
618
  }
375
619
 
376
620
  _forge() {
377
- local -a commands feature_commands mode_commands agent_commands
621
+ local -a commands feature_commands fix_commands release_commands
622
+ local -a services_commands env_commands maintenance_commands
623
+ local -a agent_commands
624
+
625
+ # Reset config cache for each completion
626
+ _forge_config_loaded=0
378
627
 
379
628
  commands=(
380
629
  ${commandsArray}
@@ -384,8 +633,24 @@ ${commandsArray}
384
633
  ${featureArray}
385
634
  )
386
635
 
387
- mode_commands=(
388
- ${modeArray}
636
+ fix_commands=(
637
+ ${fixArray}
638
+ )
639
+
640
+ release_commands=(
641
+ ${releaseArray}
642
+ )
643
+
644
+ services_commands=(
645
+ ${servicesArray}
646
+ )
647
+
648
+ env_commands=(
649
+ ${envArray}
650
+ )
651
+
652
+ maintenance_commands=(
653
+ ${maintenanceArray}
389
654
  )
390
655
 
391
656
  agent_commands=(
@@ -409,14 +674,52 @@ ${featureSlugCase} *)
409
674
  ;;
410
675
  esac
411
676
  ;;
677
+ fix)
678
+ case \${words[2]} in
679
+ ${fixSlugCase} *)
680
+ _describe 'fix command' fix_commands
681
+ ;;
682
+ esac
683
+ ;;
684
+ release)
685
+ case \${words[2]} in
686
+ ${releaseSlugCase} *)
687
+ _describe 'release command' release_commands
688
+ ;;
689
+ esac
690
+ ;;
691
+ services)
692
+ _describe 'services command' services_commands
693
+ ;;
694
+ env)
695
+ _describe 'env command' env_commands
696
+ ;;
697
+ maintenance)
698
+ if (( CURRENT == 2 )); then
699
+ _describe 'maintenance command' maintenance_commands
700
+ else
701
+ local -a branches
702
+ branches=(\${(f)"\$(_forge_all_branches)"})
703
+ _describe 'branch name' branches
704
+ fi
705
+ ;;
412
706
  mode)
413
- _describe 'mode command' mode_commands
707
+ local -a modes
708
+ modes=(\${(f)"\$(_forge_modes)"})
709
+ if [[ \${#modes} -gt 0 ]]; then
710
+ _describe 'mode name' modes
711
+ fi
414
712
  ;;
415
713
  agent)
416
714
  _describe 'agent command' agent_commands
417
715
  ;;
418
716
  ${mainSlugCase} completion)
419
- local shells
717
+ local -a shells
718
+ shells=('bash' 'zsh' 'fish' 'powershell' 'pwsh')
719
+ _describe 'shell type' shells
720
+ ;;
721
+ alias)
722
+ local -a shells
420
723
  shells=('bash' 'zsh' 'fish' 'powershell' 'pwsh')
421
724
  _describe 'shell type' shells
422
725
  ;;
@@ -436,50 +739,59 @@ compdef _forge forge
436
739
  # forge completion zsh > ~/.zsh/completions/_forge
437
740
  # # Add to .zshrc: fpath=(~/.zsh/completions $fpath)
438
741
  # # Then run: autoload -Uz compinit && compinit
742
+ #
743
+ # Tip: Run 'forge alias zsh' to generate shell aliases for quick access.
439
744
  `;
440
745
  }
441
- /**
442
- * Generate fish completion script.
443
- *
444
- * @returns Fish completion script content
445
- */
746
+ // ============================================================================
747
+ // FISH COMPLETION
748
+ // ============================================================================
446
749
  generateFishCompletion() {
447
- const mainCommands = this.getMainCommands();
448
- const featureCmd = this.findCommand('feature');
449
- const modeCmd = this.findCommand('mode');
450
- const agentCmd = this.findCommand('agent');
451
- // Generate main commands
452
- const mainCommandsLines = mainCommands
750
+ const data = this.getCompletionData();
751
+ // Main commands
752
+ const mainCommandsLines = data.mainCommands
453
753
  .map((cmd) => `complete -c forge -n "__fish_use_subcommand" -a ${cmd.name} -d "${cmd.description.replace(/"/g, '\\"')}"`)
454
754
  .join('\n');
455
- // Generate feature subcommands
456
- const featureSubLines = featureCmd?.subcommands.map((cmd) => cmd.name).join(' ') || '';
457
- const featureSubCmds = featureCmd?.subcommands
458
- .map((cmd) => `complete -c forge -n "__fish_seen_subcommand_from feature; and not __fish_seen_subcommand_from ${featureSubLines}" -a ${cmd.name} -d "${cmd.description.replace(/"/g, '\\"')}"`)
459
- .join('\n') || '';
460
- // Generate mode subcommands
461
- const modeSubLines = modeCmd?.subcommands.map((cmd) => cmd.name).join(' ') || '';
462
- const modeSubCmds = modeCmd?.subcommands
463
- .map((cmd) => `complete -c forge -n "__fish_seen_subcommand_from mode; and not __fish_seen_subcommand_from ${modeSubLines}" -a ${cmd.name} -d "${cmd.description.replace(/"/g, '\\"')}"`)
464
- .join('\n') || '';
465
- // Generate agent subcommands
466
- const agentSubLines = agentCmd?.subcommands.map((cmd) => cmd.name).join(' ') || '';
467
- const agentSubCmds = agentCmd?.subcommands
468
- .map((cmd) => `complete -c forge -n "__fish_seen_subcommand_from agent; and not __fish_seen_subcommand_from ${agentSubLines}" -a ${cmd.name} -d "${cmd.description.replace(/"/g, '\\"')}"`)
469
- .join('\n') || '';
470
- // Limit slug completions to active feature commands only
471
- const activeFeatureSlugCommands = ['stop', 'archive', 'resync', 'merge', 'rebase', 'open'];
472
- const featureWithActiveSlug = featureCmd?.subcommands.filter((cmd) => activeFeatureSlugCommands.includes(cmd.name)) || [];
473
- const featureSlugCompletions = featureWithActiveSlug
474
- .map((cmd) => `complete -c forge -n "__fish_seen_subcommand_from feature; and __fish_seen_subcommand_from ${cmd.name}" -a "(__forge_features)"`)
475
- .join('\n');
476
- const mainWithActiveSlug = mainCommands.filter((cmd) => ['merge', 'rebase', 'open'].includes(cmd.name));
477
- const mainSlugCompletions = mainWithActiveSlug
478
- .map((cmd) => `complete -c forge -n "__fish_seen_subcommand_from ${cmd.name}" -a "(__forge_features)"`)
755
+ // Helper to generate subcommand completions for a group
756
+ const genSubCmds = (parentName, cmd) => {
757
+ if (!cmd || !cmd.subcommands.length)
758
+ return '';
759
+ const subNames = cmd.subcommands.map((c) => c.name).join(' ');
760
+ return cmd.subcommands
761
+ .map((c) => `complete -c forge -n "__fish_seen_subcommand_from ${parentName}; and not __fish_seen_subcommand_from ${subNames}" -a ${c.name} -d "${c.description.replace(/"/g, '\\"')}"`)
762
+ .join('\n');
763
+ };
764
+ const featureSubCmds = genSubCmds('feature', data.featureCmd);
765
+ const fixSubCmds = genSubCmds('fix', data.fixCmd);
766
+ const releaseSubCmds = genSubCmds('release', data.releaseCmd);
767
+ const servicesSubCmds = genSubCmds('services', data.servicesCmd);
768
+ const envSubCmds = genSubCmds('env', data.envCmd);
769
+ const maintenanceSubCmds = genSubCmds('maintenance', data.maintenanceCmd);
770
+ const agentSubCmds = genSubCmds('agent', data.agentCmd);
771
+ // Slug completions for feature/fix/release
772
+ const genSlugCompletions = (parentName, cmd, slugFn) => {
773
+ if (!cmd)
774
+ return '';
775
+ const slugCmds = cmd.subcommands.filter((c) => data.slugSubcommands.includes(c.name));
776
+ return slugCmds
777
+ .map((c) => `complete -c forge -n "__fish_seen_subcommand_from ${parentName}; and __fish_seen_subcommand_from ${c.name}" -a "(${slugFn})"`)
778
+ .join('\n');
779
+ };
780
+ const featureSlugCompletions = genSlugCompletions('feature', data.featureCmd, '__forge_feature_slugs');
781
+ const fixSlugCompletions = genSlugCompletions('fix', data.fixCmd, '__forge_fix_slugs');
782
+ const releaseSlugCompletions = genSlugCompletions('release', data.releaseCmd, '__forge_release_slugs');
783
+ // Root-level slug commands
784
+ const mainSlugCmds = data.mainCommands.filter((cmd) => data.rootSlugCommands.includes(cmd.name));
785
+ const mainSlugCompletions = mainSlugCmds
786
+ .map((cmd) => `complete -c forge -n "__fish_seen_subcommand_from ${cmd.name}" -a "(__forge_all_branches)"`)
479
787
  .join('\n');
788
+ // Maintenance slug completion at position 3
789
+ const maintenanceSlugCompletion = data.maintenanceCmd?.subcommands
790
+ .map((c) => `complete -c forge -n "__fish_seen_subcommand_from maintenance; and __fish_seen_subcommand_from ${c.name}" -a "(__forge_all_branches)"`)
791
+ .join('\n') || '';
480
792
  return `# forge fish completion script
481
793
 
482
- # Helper functions to get available features
794
+ # Helper functions
483
795
  function __forge_find_config_root
484
796
  set -l dir $PWD
485
797
  while test -n "$dir" -a "$dir" != "/"
@@ -492,45 +804,107 @@ function __forge_find_config_root
492
804
  return 1
493
805
  end
494
806
 
495
- function __forge_worktrees_root
496
- if test -n "$FORGE_WORKTREES_ROOT"
497
- echo $FORGE_WORKTREES_ROOT
807
+ set -g _forge_config_loaded 0
808
+
809
+ function __forge_load_config
810
+ if test $_forge_config_loaded -eq 1
498
811
  return 0
499
812
  end
813
+ set -g _FORGE_WORKTREES_ROOT ""
814
+ set -g _FORGE_FEATURE_PREFIX "feature/"
815
+ set -g _FORGE_FIX_PREFIX "fix/"
816
+ set -g _FORGE_RELEASE_PREFIX "release/"
817
+ set -g _FORGE_MODES ""
818
+
819
+ if test -n "$FORGE_WORKTREES_ROOT"
820
+ set -g _FORGE_WORKTREES_ROOT $FORGE_WORKTREES_ROOT
821
+ end
822
+
500
823
  set -l config_root (__forge_find_config_root)
501
824
  if test -n "$config_root"
502
- set -l worktrees (node -e 'const fs=require("fs");const path=require("path");
503
- try{
504
- const file=process.argv[1];
505
- const configRoot=path.dirname(file);
506
- const data=JSON.parse(fs.readFileSync(file,"utf8"));
507
- let rootDir=data.rootDir;
508
- if(rootDir){ if(!path.isAbsolute(rootDir)) rootDir=path.join(configRoot, rootDir); }
509
- else { rootDir=configRoot; }
510
- const folders=(data.options && data.options.folders) || data.folders || {};
511
- const worktrees=folders.worktrees || "features";
512
- process.stdout.write(path.join(rootDir, worktrees));
513
- }catch(e){}
514
- ' "$config_root/.feat-forge.json" 2>/dev/null)
515
- if test -n "$worktrees"
516
- echo "$worktrees"
517
- return 0
825
+ set -l output (node -e ${this.nodeConfigSnippetFish()} "$config_root/.feat-forge.json" 2>/dev/null)
826
+ if test -n "$output"
827
+ eval $output
828
+ else if test -z "$_FORGE_WORKTREES_ROOT"
829
+ set -g _FORGE_WORKTREES_ROOT "$config_root/worktrees"
830
+ end
831
+ else if test -z "$_FORGE_WORKTREES_ROOT"
832
+ set -g _FORGE_WORKTREES_ROOT "worktrees"
833
+ end
834
+ set -g _forge_config_loaded 1
835
+ end
836
+
837
+ function __forge_feature_slugs
838
+ __forge_load_config
839
+ set -l prefix_dir (string trim --right --chars=/ $_FORGE_FEATURE_PREFIX)
840
+ set -l dir "$_FORGE_WORKTREES_ROOT/$prefix_dir"
841
+ if test -d "$dir"
842
+ for entry in $dir/*/
843
+ basename $entry
518
844
  end
519
- echo "$config_root/features"
520
- return 0
521
845
  end
522
- echo "features"
523
846
  end
524
847
 
525
- function __forge_features
526
- set -l worktrees_root (__forge_worktrees_root)
527
- if test -d $worktrees_root
528
- for dir in $worktrees_root/*/
529
- basename $dir
848
+ function __forge_fix_slugs
849
+ __forge_load_config
850
+ set -l prefix_dir (string trim --right --chars=/ $_FORGE_FIX_PREFIX)
851
+ set -l dir "$_FORGE_WORKTREES_ROOT/$prefix_dir"
852
+ if test -d "$dir"
853
+ for entry in $dir/*/
854
+ basename $entry
530
855
  end
531
856
  end
532
857
  end
533
858
 
859
+ function __forge_release_slugs
860
+ __forge_load_config
861
+ set -l prefix_dir (string trim --right --chars=/ $_FORGE_RELEASE_PREFIX)
862
+ set -l dir "$_FORGE_WORKTREES_ROOT/$prefix_dir"
863
+ if test -d "$dir"
864
+ for entry in $dir/*/
865
+ basename $entry
866
+ end
867
+ end
868
+ end
869
+
870
+ function __forge_all_branches
871
+ __forge_load_config
872
+ set -l root $_FORGE_WORKTREES_ROOT
873
+ if not test -d "$root"
874
+ return
875
+ end
876
+ set -l prefix_dirs (string trim --right --chars=/ $_FORGE_FEATURE_PREFIX) (string trim --right --chars=/ $_FORGE_FIX_PREFIX) (string trim --right --chars=/ $_FORGE_RELEASE_PREFIX)
877
+ for entry in $root/*/
878
+ set -l name (basename $entry)
879
+ set -l is_prefix 0
880
+ for pd in $prefix_dirs
881
+ if test "$name" = "$pd"
882
+ set is_prefix 1
883
+ break
884
+ end
885
+ end
886
+ if test $is_prefix -eq 1
887
+ for sub in $entry*/
888
+ if test -d "$sub"
889
+ echo "$name/"(basename $sub)
890
+ end
891
+ end
892
+ else
893
+ echo $name
894
+ end
895
+ end
896
+ end
897
+
898
+ function __forge_modes
899
+ __forge_load_config
900
+ string split " " $_FORGE_MODES
901
+ end
902
+
903
+ # Reset config cache on each completion invocation
904
+ function __forge_reset_config --on-event fish_prompt
905
+ set -g _forge_config_loaded 0
906
+ end
907
+
534
908
  # Disable file completion by default
535
909
  complete -c forge -f
536
910
 
@@ -540,18 +914,42 @@ ${mainCommandsLines}
540
914
  # Feature subcommands
541
915
  ${featureSubCmds}
542
916
 
543
- # Feature commands with slug completion
917
+ # Feature slug completions
544
918
  ${featureSlugCompletions}
545
919
 
546
- # Mode subcommands
547
- ${modeSubCmds}
920
+ # Fix subcommands
921
+ ${fixSubCmds}
922
+
923
+ # Fix slug completions
924
+ ${fixSlugCompletions}
925
+
926
+ # Release subcommands
927
+ ${releaseSubCmds}
928
+
929
+ # Release slug completions
930
+ ${releaseSlugCompletions}
931
+
932
+ # Services subcommands
933
+ ${servicesSubCmds}
934
+
935
+ # Env subcommands
936
+ ${envSubCmds}
937
+
938
+ # Maintenance subcommands
939
+ ${maintenanceSubCmds}
940
+
941
+ # Maintenance slug completions
942
+ ${maintenanceSlugCompletion}
548
943
 
549
944
  # Agent subcommands
550
945
  ${agentSubCmds}
551
946
 
552
- # Main commands with feature slug completion
947
+ # Root-level commands with branch completion
553
948
  ${mainSlugCompletions}
554
949
 
950
+ # Mode command with mode name completion
951
+ complete -c forge -n "__fish_seen_subcommand_from mode" -a "(__forge_modes)"
952
+
555
953
  # Completion command with shell types
556
954
  complete -c forge -n "__fish_seen_subcommand_from completion" -a bash -d "Generate bash completion"
557
955
  complete -c forge -n "__fish_seen_subcommand_from completion" -a zsh -d "Generate zsh completion"
@@ -559,33 +957,41 @@ complete -c forge -n "__fish_seen_subcommand_from completion" -a fish -d "Genera
559
957
  complete -c forge -n "__fish_seen_subcommand_from completion" -a powershell -d "Generate PowerShell completion"
560
958
  complete -c forge -n "__fish_seen_subcommand_from completion" -a pwsh -d "Generate PowerShell completion"
561
959
 
960
+ # Alias command with shell types
961
+ complete -c forge -n "__fish_seen_subcommand_from alias" -a bash -d "Generate bash aliases"
962
+ complete -c forge -n "__fish_seen_subcommand_from alias" -a zsh -d "Generate zsh aliases"
963
+ complete -c forge -n "__fish_seen_subcommand_from alias" -a fish -d "Generate fish aliases"
964
+ complete -c forge -n "__fish_seen_subcommand_from alias" -a powershell -d "Generate PowerShell aliases"
965
+ complete -c forge -n "__fish_seen_subcommand_from alias" -a pwsh -d "Generate PowerShell aliases"
966
+
562
967
  # Installation instructions:
563
968
  # Option 1 - Add to ~/.config/fish/config.fish:
564
969
  # forge completion fish | source
565
970
  #
566
971
  # Option 2 - Save to completions directory:
567
972
  # forge completion fish > ~/.config/fish/completions/forge.fish
973
+ #
974
+ # Tip: Run 'forge alias fish' to generate shell aliases for quick access.
568
975
  `;
569
976
  }
570
- /**
571
- * Generate PowerShell completion script.
572
- *
573
- * @returns PowerShell completion script content
574
- */
977
+ // ============================================================================
978
+ // POWERSHELL COMPLETION
979
+ // ============================================================================
575
980
  generatePowerShellCompletion() {
576
- const mainCommands = this.getMainCommands();
577
- const featureCmd = this.findCommand('feature');
578
- const modeCmd = this.findCommand('mode');
579
- const agentCmd = this.findCommand('agent');
981
+ const data = this.getCompletionData();
580
982
  const toPsArray = (items) => items.length > 0 ? items.map((item) => `'${item.replace(/'/g, "''")}'`).join(', ') : '';
581
- const mainCommandsList = toPsArray(mainCommands.map((cmd) => cmd.name));
582
- const featureCommandsList = toPsArray(featureCmd?.subcommands.map((cmd) => cmd.name) || []);
583
- const modeCommandsList = toPsArray(modeCmd?.subcommands.map((cmd) => cmd.name) || []);
584
- const agentCommandsList = toPsArray(agentCmd?.subcommands.map((cmd) => cmd.name) || []);
585
- const activeFeatureSlugCommands = ['stop', 'archive', 'resync', 'merge', 'rebase', 'open'];
586
- const featureWithActiveSlug = featureCmd?.subcommands.filter((cmd) => activeFeatureSlugCommands.includes(cmd.name)) || [];
587
- const featureSlugCommandsList = toPsArray(featureWithActiveSlug.map((cmd) => cmd.name));
588
- const mainSlugCommandsList = toPsArray(mainCommands.filter((cmd) => ['merge', 'rebase', 'open'].includes(cmd.name)).map((cmd) => cmd.name));
983
+ const mainCommandsList = toPsArray(data.mainCommands.map((cmd) => cmd.name));
984
+ const featureCommandsList = toPsArray(data.featureCmd?.subcommands.map((cmd) => cmd.name) || []);
985
+ const fixCommandsList = toPsArray(data.fixCmd?.subcommands.map((cmd) => cmd.name) || []);
986
+ const releaseCommandsList = toPsArray(data.releaseCmd?.subcommands.map((cmd) => cmd.name) || []);
987
+ const servicesCommandsList = toPsArray(data.servicesCmd?.subcommands.map((cmd) => cmd.name) || []);
988
+ const envCommandsList = toPsArray(data.envCmd?.subcommands.map((cmd) => cmd.name) || []);
989
+ const maintenanceCommandsList = toPsArray(data.maintenanceCmd?.subcommands.map((cmd) => cmd.name) || []);
990
+ const agentCommandsList = toPsArray(data.agentCmd?.subcommands.map((cmd) => cmd.name) || []);
991
+ const featureSlugCommandsList = toPsArray(data.featureCmd?.subcommands.filter((cmd) => data.slugSubcommands.includes(cmd.name)).map((cmd) => cmd.name) || []);
992
+ const fixSlugCommandsList = toPsArray(data.fixCmd?.subcommands.filter((cmd) => data.slugSubcommands.includes(cmd.name)).map((cmd) => cmd.name) || []);
993
+ const releaseSlugCommandsList = toPsArray(data.releaseCmd?.subcommands.filter((cmd) => data.slugSubcommands.includes(cmd.name)).map((cmd) => cmd.name) || []);
994
+ const mainSlugCommandsList = toPsArray(data.mainCommands.filter((cmd) => data.rootSlugCommands.includes(cmd.name)).map((cmd) => cmd.name));
589
995
  const completionShellsList = toPsArray(['bash', 'zsh', 'fish', 'powershell', 'pwsh']);
590
996
  return `# forge PowerShell completion script
591
997
 
@@ -600,40 +1006,100 @@ function __ForgeFindConfigRoot {
600
1006
  return $null
601
1007
  }
602
1008
 
603
- function __ForgeWorktreesRoot {
604
- if ($env:FORGE_WORKTREES_ROOT) { return $env:FORGE_WORKTREES_ROOT }
1009
+ $script:__ForgeConfigLoaded = $false
1010
+ $script:__ForgeConfig = $null
1011
+
1012
+ function __ForgeLoadConfig {
1013
+ if ($script:__ForgeConfigLoaded) { return }
1014
+
1015
+ $script:__ForgeConfig = @{
1016
+ WorktreesRoot = ""
1017
+ FeaturePrefix = "feature/"
1018
+ FixPrefix = "fix/"
1019
+ ReleasePrefix = "release/"
1020
+ Modes = @()
1021
+ }
1022
+
1023
+ if ($env:FORGE_WORKTREES_ROOT) {
1024
+ $script:__ForgeConfig.WorktreesRoot = $env:FORGE_WORKTREES_ROOT
1025
+ }
1026
+
605
1027
  $configRoot = __ForgeFindConfigRoot
606
1028
  if ($configRoot) {
607
1029
  try {
608
- $json = Get-Content -Raw -Path (Join-Path $configRoot ".feat-forge.json") | ConvertFrom-Json
609
- $rootDir = $json.rootDir
610
- if ($rootDir) {
611
- if (-not [System.IO.Path]::IsPathRooted($rootDir)) {
612
- $rootDir = Join-Path $configRoot $rootDir
613
- }
614
- } else {
615
- $rootDir = $configRoot
616
- }
617
- if ($json.options -and $json.options.folders -and $json.options.folders.worktrees) {
618
- return (Join-Path $rootDir $json.options.folders.worktrees)
1030
+ $output = node -e ${this.nodeConfigSnippetPowerShell()} (Join-Path $configRoot ".feat-forge.json") 2>$null
1031
+ if ($output) {
1032
+ $parsed = $output | ConvertFrom-Json
1033
+ if ($parsed.worktreesRoot) { $script:__ForgeConfig.WorktreesRoot = $parsed.worktreesRoot }
1034
+ if ($parsed.featurePrefix) { $script:__ForgeConfig.FeaturePrefix = $parsed.featurePrefix }
1035
+ if ($parsed.fixPrefix) { $script:__ForgeConfig.FixPrefix = $parsed.fixPrefix }
1036
+ if ($parsed.releasePrefix) { $script:__ForgeConfig.ReleasePrefix = $parsed.releasePrefix }
1037
+ if ($parsed.modes) { $script:__ForgeConfig.Modes = $parsed.modes }
619
1038
  }
620
- if ($json.folders -and $json.folders.worktrees) {
621
- return (Join-Path $rootDir $json.folders.worktrees)
622
- }
623
- } catch {
1039
+ } catch {}
1040
+ if (-not $script:__ForgeConfig.WorktreesRoot) {
1041
+ $script:__ForgeConfig.WorktreesRoot = Join-Path $configRoot "worktrees"
624
1042
  }
625
- return (Join-Path $rootDir "features")
626
1043
  }
627
- return "features"
1044
+ if (-not $script:__ForgeConfig.WorktreesRoot) {
1045
+ $script:__ForgeConfig.WorktreesRoot = "worktrees"
1046
+ }
1047
+ $script:__ForgeConfigLoaded = $true
628
1048
  }
629
1049
 
630
1050
  function __ForgeFeatureSlugs {
631
- $worktreesRoot = __ForgeWorktreesRoot
632
- if (Test-Path $worktreesRoot) {
633
- Get-ChildItem -Directory -Path $worktreesRoot | ForEach-Object { $_.Name }
1051
+ __ForgeLoadConfig
1052
+ $prefix = $script:__ForgeConfig.FeaturePrefix.TrimEnd('/')
1053
+ $dir = Join-Path $script:__ForgeConfig.WorktreesRoot $prefix
1054
+ if (Test-Path $dir) {
1055
+ Get-ChildItem -Directory -Path $dir | ForEach-Object { $_.Name }
1056
+ }
1057
+ }
1058
+
1059
+ function __ForgeFixSlugs {
1060
+ __ForgeLoadConfig
1061
+ $prefix = $script:__ForgeConfig.FixPrefix.TrimEnd('/')
1062
+ $dir = Join-Path $script:__ForgeConfig.WorktreesRoot $prefix
1063
+ if (Test-Path $dir) {
1064
+ Get-ChildItem -Directory -Path $dir | ForEach-Object { $_.Name }
634
1065
  }
635
1066
  }
636
1067
 
1068
+ function __ForgeReleaseSlugs {
1069
+ __ForgeLoadConfig
1070
+ $prefix = $script:__ForgeConfig.ReleasePrefix.TrimEnd('/')
1071
+ $dir = Join-Path $script:__ForgeConfig.WorktreesRoot $prefix
1072
+ if (Test-Path $dir) {
1073
+ Get-ChildItem -Directory -Path $dir | ForEach-Object { $_.Name }
1074
+ }
1075
+ }
1076
+
1077
+ function __ForgeAllBranches {
1078
+ __ForgeLoadConfig
1079
+ $root = $script:__ForgeConfig.WorktreesRoot
1080
+ if (-not (Test-Path $root)) { return }
1081
+ $prefixDirs = @(
1082
+ $script:__ForgeConfig.FeaturePrefix.TrimEnd('/'),
1083
+ $script:__ForgeConfig.FixPrefix.TrimEnd('/'),
1084
+ $script:__ForgeConfig.ReleasePrefix.TrimEnd('/')
1085
+ )
1086
+ Get-ChildItem -Directory -Path $root | ForEach-Object {
1087
+ $name = $_.Name
1088
+ if ($prefixDirs -contains $name) {
1089
+ Get-ChildItem -Directory -Path $_.FullName | ForEach-Object {
1090
+ "$name/$($_.Name)"
1091
+ }
1092
+ } else {
1093
+ $name
1094
+ }
1095
+ }
1096
+ }
1097
+
1098
+ function __ForgeModes {
1099
+ __ForgeLoadConfig
1100
+ $script:__ForgeConfig.Modes
1101
+ }
1102
+
637
1103
  function __ForgeCompletionResults {
638
1104
  param([string[]]$Items, [string]$Word)
639
1105
  if (-not $Items) { return }
@@ -644,15 +1110,24 @@ function __ForgeCompletionResults {
644
1110
 
645
1111
  $mainCommands = @(${mainCommandsList})
646
1112
  $featureCommands = @(${featureCommandsList})
647
- $modeCommands = @(${modeCommandsList})
1113
+ $fixCommands = @(${fixCommandsList})
1114
+ $releaseCommands = @(${releaseCommandsList})
1115
+ $servicesCommands = @(${servicesCommandsList})
1116
+ $envCommands = @(${envCommandsList})
1117
+ $maintenanceCommands = @(${maintenanceCommandsList})
648
1118
  $agentCommands = @(${agentCommandsList})
649
1119
  $featureSlugCommands = @(${featureSlugCommandsList})
1120
+ $fixSlugCommands = @(${fixSlugCommandsList})
1121
+ $releaseSlugCommands = @(${releaseSlugCommandsList})
650
1122
  $mainSlugCommands = @(${mainSlugCommandsList})
651
1123
  $completionShells = @(${completionShellsList})
652
1124
 
653
1125
  Register-ArgumentCompleter -CommandName forge -ScriptBlock {
654
1126
  param($commandName, $parameterName, $wordToComplete, $commandAst, $fakeBoundParameters)
655
1127
 
1128
+ # Reset config cache for each completion
1129
+ $script:__ForgeConfigLoaded = $false
1130
+
656
1131
  $elements = $commandAst.CommandElements | ForEach-Object { $_.Value }
657
1132
 
658
1133
  if ($elements.Count -le 1) {
@@ -673,8 +1148,46 @@ Register-ArgumentCompleter -CommandName forge -ScriptBlock {
673
1148
  return
674
1149
  }
675
1150
  }
1151
+ 'fix' {
1152
+ if ($elements.Count -le 2) {
1153
+ __ForgeCompletionResults -Items $fixCommands -Word $wordToComplete
1154
+ return
1155
+ }
1156
+ $sub = $elements[2]
1157
+ if ($fixSlugCommands -contains $sub) {
1158
+ __ForgeCompletionResults -Items (__ForgeFixSlugs) -Word $wordToComplete
1159
+ return
1160
+ }
1161
+ }
1162
+ 'release' {
1163
+ if ($elements.Count -le 2) {
1164
+ __ForgeCompletionResults -Items $releaseCommands -Word $wordToComplete
1165
+ return
1166
+ }
1167
+ $sub = $elements[2]
1168
+ if ($releaseSlugCommands -contains $sub) {
1169
+ __ForgeCompletionResults -Items (__ForgeReleaseSlugs) -Word $wordToComplete
1170
+ return
1171
+ }
1172
+ }
1173
+ 'services' {
1174
+ __ForgeCompletionResults -Items $servicesCommands -Word $wordToComplete
1175
+ return
1176
+ }
1177
+ 'env' {
1178
+ __ForgeCompletionResults -Items $envCommands -Word $wordToComplete
1179
+ return
1180
+ }
1181
+ 'maintenance' {
1182
+ if ($elements.Count -le 2) {
1183
+ __ForgeCompletionResults -Items $maintenanceCommands -Word $wordToComplete
1184
+ return
1185
+ }
1186
+ __ForgeCompletionResults -Items (__ForgeAllBranches) -Word $wordToComplete
1187
+ return
1188
+ }
676
1189
  'mode' {
677
- __ForgeCompletionResults -Items $modeCommands -Word $wordToComplete
1190
+ __ForgeCompletionResults -Items (__ForgeModes) -Word $wordToComplete
678
1191
  return
679
1192
  }
680
1193
  'agent' {
@@ -685,9 +1198,13 @@ Register-ArgumentCompleter -CommandName forge -ScriptBlock {
685
1198
  __ForgeCompletionResults -Items $completionShells -Word $wordToComplete
686
1199
  return
687
1200
  }
1201
+ 'alias' {
1202
+ __ForgeCompletionResults -Items $completionShells -Word $wordToComplete
1203
+ return
1204
+ }
688
1205
  default {
689
1206
  if ($mainSlugCommands -contains $first) {
690
- __ForgeCompletionResults -Items (__ForgeFeatureSlugs) -Word $wordToComplete
1207
+ __ForgeCompletionResults -Items (__ForgeAllBranches) -Word $wordToComplete
691
1208
  return
692
1209
  }
693
1210
  }
@@ -695,8 +1212,10 @@ Register-ArgumentCompleter -CommandName forge -ScriptBlock {
695
1212
  }
696
1213
 
697
1214
  # Installation instructions:
698
- # Option 1 - Add to $PROFILE:
1215
+ # Option 1 - Add to \$PROFILE:
699
1216
  # forge completion powershell | Out-String | Invoke-Expression
1217
+ #
1218
+ # Tip: Run 'forge alias powershell' to generate shell aliases for quick access.
700
1219
  `;
701
1220
  }
702
1221
  }