feat-forge 1.0.2 → 1.1.0

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,175 @@ 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
180
+ .filter((cmd) => data.slugSubcommands.includes(cmd.name))
181
+ .map((cmd) => cmd.name) || [];
182
+ const fixSlugCmds = data.fixCmd?.subcommands
183
+ .filter((cmd) => data.slugSubcommands.includes(cmd.name))
184
+ .map((cmd) => cmd.name) || [];
185
+ const releaseSlugCmds = data.releaseCmd?.subcommands
186
+ .filter((cmd) => data.slugSubcommands.includes(cmd.name))
187
+ .map((cmd) => cmd.name) || [];
188
+ const mainSlugCmds = data.mainCommands
189
+ .filter((cmd) => data.rootSlugCommands.includes(cmd.name))
190
+ .map((cmd) => cmd.name);
191
+ const featureSlugCase = featureSlugCmds.length > 0
192
+ ? ` ${featureSlugCmds.join('|')})
193
+ if [[ \${cword} -eq 3 ]]; then
194
+ local slugs="\$(_forge_feature_slugs)"
195
+ COMPREPLY=( \$(compgen -W "\${slugs}" -- "\${cur}") )
196
+ return 0
197
+ fi
198
+ ;;
199
+ `
200
+ : '';
201
+ const fixSlugCase = fixSlugCmds.length > 0
202
+ ? ` ${fixSlugCmds.join('|')})
203
+ if [[ \${cword} -eq 3 ]]; then
204
+ local slugs="\$(_forge_fix_slugs)"
205
+ COMPREPLY=( \$(compgen -W "\${slugs}" -- "\${cur}") )
206
+ return 0
207
+ fi
208
+ ;;
209
+ `
210
+ : '';
211
+ const releaseSlugCase = releaseSlugCmds.length > 0
212
+ ? ` ${releaseSlugCmds.join('|')})
153
213
  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}") )
214
+ local slugs="\$(_forge_release_slugs)"
215
+ COMPREPLY=( \$(compgen -W "\${slugs}" -- "\${cur}") )
157
216
  return 0
158
217
  fi
159
218
  ;;
160
219
  `
161
220
  : '';
162
- const mainSlugCase = mainWithActiveSlug.length > 0
163
- ? ` ${mainWithActiveSlug.join('|')})
164
- # Suggest available features for commands with slug argument
221
+ const mainSlugCase = mainSlugCmds.length > 0
222
+ ? ` ${mainSlugCmds.join('|')})
165
223
  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}") )
224
+ local branches="\$(_forge_all_branches)"
225
+ COMPREPLY=( \$(compgen -W "\${branches}" -- "\${cur}") )
169
226
  return 0
170
227
  fi
171
228
  ;;
@@ -185,57 +242,125 @@ _forge_find_config() {
185
242
  return 1
186
243
  }
187
244
 
188
- _forge_worktrees_root() {
189
- if [[ -n "\${FORGE_WORKTREES_ROOT}" ]]; then
190
- echo "\${FORGE_WORKTREES_ROOT}"
245
+ # Load forge config once and cache variables for the current completion
246
+ _forge_config_loaded=0
247
+ _forge_load_config() {
248
+ if [[ \${_forge_config_loaded} -eq 1 ]]; then
191
249
  return 0
192
250
  fi
251
+ _FORGE_WORKTREES_ROOT=""
252
+ _FORGE_FEATURE_PREFIX="feature/"
253
+ _FORGE_FIX_PREFIX="fix/"
254
+ _FORGE_RELEASE_PREFIX="release/"
255
+ _FORGE_MODES=""
256
+
257
+ if [[ -n "\${FORGE_WORKTREES_ROOT}" ]]; then
258
+ _FORGE_WORKTREES_ROOT="\${FORGE_WORKTREES_ROOT}"
259
+ fi
260
+
193
261
  local start="\${PWD}"
194
262
  local config_root
195
263
  config_root="\$(_forge_find_config "\${start}")" || true
196
264
  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
265
+ local output
266
+ output=\$(node -e ${this.nodeConfigSnippet()} "\${config_root}/.feat-forge.json" 2>/dev/null)
267
+ if [[ -n "\${output}" ]]; then
268
+ eval "\${output}"
269
+ elif [[ -z "\${_FORGE_WORKTREES_ROOT}" ]]; then
270
+ _FORGE_WORKTREES_ROOT="\${config_root}/worktrees"
214
271
  fi
215
- echo "\${config_root}/features"
216
- return 0
272
+ elif [[ -z "\${_FORGE_WORKTREES_ROOT}" ]]; then
273
+ _FORGE_WORKTREES_ROOT="worktrees"
217
274
  fi
218
- echo "features"
275
+ _forge_config_loaded=1
276
+ }
277
+
278
+ _forge_feature_slugs() {
279
+ _forge_load_config
280
+ local prefix_dir="\${_FORGE_FEATURE_PREFIX%/}"
281
+ local dir="\${_FORGE_WORKTREES_ROOT}/\${prefix_dir}"
282
+ if [[ -d "\${dir}" ]]; then
283
+ find "\${dir}" -mindepth 1 -maxdepth 1 -type d -exec basename {} \\; 2>/dev/null
284
+ fi
285
+ }
286
+
287
+ _forge_fix_slugs() {
288
+ _forge_load_config
289
+ local prefix_dir="\${_FORGE_FIX_PREFIX%/}"
290
+ local dir="\${_FORGE_WORKTREES_ROOT}/\${prefix_dir}"
291
+ if [[ -d "\${dir}" ]]; then
292
+ find "\${dir}" -mindepth 1 -maxdepth 1 -type d -exec basename {} \\; 2>/dev/null
293
+ fi
294
+ }
295
+
296
+ _forge_release_slugs() {
297
+ _forge_load_config
298
+ local prefix_dir="\${_FORGE_RELEASE_PREFIX%/}"
299
+ local dir="\${_FORGE_WORKTREES_ROOT}/\${prefix_dir}"
300
+ if [[ -d "\${dir}" ]]; then
301
+ find "\${dir}" -mindepth 1 -maxdepth 1 -type d -exec basename {} \\; 2>/dev/null
302
+ fi
303
+ }
304
+
305
+ _forge_all_branches() {
306
+ _forge_load_config
307
+ local root="\${_FORGE_WORKTREES_ROOT}"
308
+ if [[ ! -d "\${root}" ]]; then
309
+ return
310
+ fi
311
+ local prefix_dirs=()
312
+ for p in "\${_FORGE_FEATURE_PREFIX%/}" "\${_FORGE_FIX_PREFIX%/}" "\${_FORGE_RELEASE_PREFIX%/}"; do
313
+ if [[ -n "\${p}" ]]; then
314
+ prefix_dirs+=("\${p}")
315
+ fi
316
+ done
317
+ for entry in "\${root}"/*/; do
318
+ [[ -d "\${entry}" ]] || continue
319
+ local name="\$(basename "\${entry}")"
320
+ local is_prefix=0
321
+ for pd in "\${prefix_dirs[@]}"; do
322
+ if [[ "\${name}" == "\${pd}" ]]; then
323
+ is_prefix=1
324
+ break
325
+ fi
326
+ done
327
+ if [[ \${is_prefix} -eq 1 ]]; then
328
+ for sub in "\${entry}"*/; do
329
+ [[ -d "\${sub}" ]] || continue
330
+ echo "\${name}/\$(basename "\${sub}")"
331
+ done
332
+ else
333
+ echo "\${name}"
334
+ fi
335
+ done
336
+ }
337
+
338
+ _forge_modes() {
339
+ _forge_load_config
340
+ echo "\${_FORGE_MODES}"
219
341
  }
220
342
 
221
343
  _forge_completion() {
222
344
  local cur prev words cword
223
345
  _init_completion || return
224
346
 
225
- # Main commands available at root level
347
+ # Reset config cache for each completion
348
+ _forge_config_loaded=0
349
+
226
350
  local commands="${commands}"
227
351
 
228
- # Subcommands for each main command
229
352
  local feature_commands="${featureCommands}"
230
- local mode_commands="${modeCommands}"
353
+ local fix_commands="${fixCommands}"
354
+ local release_commands="${releaseCommands}"
355
+ local services_commands="${servicesCommands}"
356
+ local env_commands="${envCommands}"
357
+ local maintenance_commands="${maintenanceCommands}"
231
358
  local agent_commands="${agentCommands}"
232
359
 
233
- # Get previous word for context
234
360
  case "\${words[1]}" in
235
361
  feature)
236
362
  case "\${words[2]}" in
237
363
  ${featureSlugCase} *)
238
- # Suggest feature subcommands
239
364
  if [[ \${cword} -eq 2 ]]; then
240
365
  COMPREPLY=( \$(compgen -W "\${feature_commands}" -- "\${cur}") )
241
366
  return 0
@@ -243,29 +368,75 @@ ${featureSlugCase} *)
243
368
  ;;
244
369
  esac
245
370
  ;;
371
+ fix)
372
+ case "\${words[2]}" in
373
+ ${fixSlugCase} *)
374
+ if [[ \${cword} -eq 2 ]]; then
375
+ COMPREPLY=( \$(compgen -W "\${fix_commands}" -- "\${cur}") )
376
+ return 0
377
+ fi
378
+ ;;
379
+ esac
380
+ ;;
381
+ release)
382
+ case "\${words[2]}" in
383
+ ${releaseSlugCase} *)
384
+ if [[ \${cword} -eq 2 ]]; then
385
+ COMPREPLY=( \$(compgen -W "\${release_commands}" -- "\${cur}") )
386
+ return 0
387
+ fi
388
+ ;;
389
+ esac
390
+ ;;
391
+ services)
392
+ if [[ \${cword} -eq 2 ]]; then
393
+ COMPREPLY=( \$(compgen -W "\${services_commands}" -- "\${cur}") )
394
+ return 0
395
+ fi
396
+ ;;
397
+ env)
398
+ if [[ \${cword} -eq 2 ]]; then
399
+ COMPREPLY=( \$(compgen -W "\${env_commands}" -- "\${cur}") )
400
+ return 0
401
+ fi
402
+ ;;
403
+ maintenance)
404
+ if [[ \${cword} -eq 2 ]]; then
405
+ COMPREPLY=( \$(compgen -W "\${maintenance_commands}" -- "\${cur}") )
406
+ return 0
407
+ fi
408
+ if [[ \${cword} -eq 3 ]]; then
409
+ local branches="\$(_forge_all_branches)"
410
+ COMPREPLY=( \$(compgen -W "\${branches}" -- "\${cur}") )
411
+ return 0
412
+ fi
413
+ ;;
246
414
  mode)
247
- # Suggest mode subcommands
248
415
  if [[ \${cword} -eq 2 ]]; then
249
- COMPREPLY=( \$(compgen -W "\${mode_commands}" -- "\${cur}") )
416
+ local modes="\$(_forge_modes)"
417
+ COMPREPLY=( \$(compgen -W "\${modes}" -- "\${cur}") )
250
418
  return 0
251
419
  fi
252
420
  ;;
253
421
  agent)
254
- # Suggest agent subcommands
255
422
  if [[ \${cword} -eq 2 ]]; then
256
423
  COMPREPLY=( \$(compgen -W "\${agent_commands}" -- "\${cur}") )
257
424
  return 0
258
425
  fi
259
426
  ;;
260
427
  ${mainSlugCase} completion)
261
- # Suggest shell types for completion
428
+ if [[ \${cword} -eq 2 ]]; then
429
+ COMPREPLY=( \$(compgen -W "bash zsh fish powershell pwsh" -- "\${cur}") )
430
+ return 0
431
+ fi
432
+ ;;
433
+ alias)
262
434
  if [[ \${cword} -eq 2 ]]; then
263
435
  COMPREPLY=( \$(compgen -W "bash zsh fish powershell pwsh" -- "\${cur}") )
264
436
  return 0
265
437
  fi
266
438
  ;;
267
439
  *)
268
- # Suggest main commands at root level
269
440
  if [[ \${cword} -eq 1 ]]; then
270
441
  COMPREPLY=( \$(compgen -W "\${commands}" -- "\${cur}") )
271
442
  return 0
@@ -284,44 +455,64 @@ complete -F _forge_completion forge
284
455
  # Option 2 - Save to file:
285
456
  # forge completion bash > ~/.local/share/bash-completion/completions/forge
286
457
  # # Or system-wide: /etc/bash_completion.d/forge
458
+ #
459
+ # Tip: Run 'forge alias bash' to generate shell aliases for quick access.
287
460
  `;
288
461
  }
289
- /**
290
- * Generate zsh completion script.
291
- *
292
- * @returns Zsh completion script content
293
- */
462
+ // ============================================================================
463
+ // ZSH COMPLETION
464
+ // ============================================================================
294
465
  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
466
+ const data = this.getCompletionData();
467
+ const commandsArray = data.mainCommands.map((cmd) => ` '${cmd.name}:${cmd.description.replace(/'/g, "''")}'`).join('\n');
468
+ const featureArray = data.featureCmd?.subcommands.map((cmd) => ` '${cmd.name}:${cmd.description.replace(/'/g, "''")}'`).join('\n') || '';
469
+ const fixArray = data.fixCmd?.subcommands.map((cmd) => ` '${cmd.name}:${cmd.description.replace(/'/g, "''")}'`).join('\n') || '';
470
+ const releaseArray = data.releaseCmd?.subcommands.map((cmd) => ` '${cmd.name}:${cmd.description.replace(/'/g, "''")}'`).join('\n') || '';
471
+ const servicesArray = data.servicesCmd?.subcommands.map((cmd) => ` '${cmd.name}:${cmd.description.replace(/'/g, "''")}'`).join('\n') || '';
472
+ const envArray = data.envCmd?.subcommands.map((cmd) => ` '${cmd.name}:${cmd.description.replace(/'/g, "''")}'`).join('\n') || '';
473
+ const maintenanceArray = data.maintenanceCmd?.subcommands.map((cmd) => ` '${cmd.name}:${cmd.description.replace(/'/g, "''")}'`).join('\n') || '';
474
+ const agentArray = data.agentCmd?.subcommands.map((cmd) => ` '${cmd.name}:${cmd.description.replace(/'/g, "''")}'`).join('\n') || '';
475
+ const featureSlugCmds = data.featureCmd?.subcommands
476
+ .filter((cmd) => data.slugSubcommands.includes(cmd.name))
477
+ .map((cmd) => cmd.name) || [];
478
+ const fixSlugCmds = data.fixCmd?.subcommands
479
+ .filter((cmd) => data.slugSubcommands.includes(cmd.name))
480
+ .map((cmd) => cmd.name) || [];
481
+ const releaseSlugCmds = data.releaseCmd?.subcommands
482
+ .filter((cmd) => data.slugSubcommands.includes(cmd.name))
483
+ .map((cmd) => cmd.name) || [];
484
+ const mainSlugCmds = data.mainCommands
485
+ .filter((cmd) => data.rootSlugCommands.includes(cmd.name))
486
+ .map((cmd) => cmd.name);
487
+ const featureSlugCase = featureSlugCmds.length > 0
488
+ ? ` ${featureSlugCmds.join('|')})
489
+ local -a slugs
490
+ slugs=(\${(f)"\$(_forge_feature_slugs)"})
491
+ _describe 'feature slug' slugs
492
+ ;;
493
+ `
494
+ : '';
495
+ const fixSlugCase = fixSlugCmds.length > 0
496
+ ? ` ${fixSlugCmds.join('|')})
497
+ local -a slugs
498
+ slugs=(\${(f)"\$(_forge_fix_slugs)"})
499
+ _describe 'fix slug' slugs
500
+ ;;
501
+ `
502
+ : '';
503
+ const releaseSlugCase = releaseSlugCmds.length > 0
504
+ ? ` ${releaseSlugCmds.join('|')})
505
+ local -a slugs
506
+ slugs=(\${(f)"\$(_forge_release_slugs)"})
507
+ _describe 'release slug' slugs
315
508
  ;;
316
509
  `
317
510
  : '';
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
511
+ const mainSlugCase = mainSlugCmds.length > 0
512
+ ? ` ${mainSlugCmds.join('|')})
513
+ local -a branches
514
+ branches=(\${(f)"\$(_forge_all_branches)"})
515
+ _describe 'branch name' branches
325
516
  ;;
326
517
  `
327
518
  : '';
@@ -340,41 +531,111 @@ _forge_find_config() {
340
531
  return 1
341
532
  }
342
533
 
343
- _forge_worktrees_root() {
344
- if [[ -n "\${FORGE_WORKTREES_ROOT}" ]]; then
345
- echo "\${FORGE_WORKTREES_ROOT}"
534
+ # Load forge config once and cache variables for the current completion
535
+ _forge_config_loaded=0
536
+ _forge_load_config() {
537
+ if [[ \${_forge_config_loaded} -eq 1 ]]; then
346
538
  return 0
347
539
  fi
540
+ _FORGE_WORKTREES_ROOT=""
541
+ _FORGE_FEATURE_PREFIX="feature/"
542
+ _FORGE_FIX_PREFIX="fix/"
543
+ _FORGE_RELEASE_PREFIX="release/"
544
+ _FORGE_MODES=""
545
+
546
+ if [[ -n "\${FORGE_WORKTREES_ROOT}" ]]; then
547
+ _FORGE_WORKTREES_ROOT="\${FORGE_WORKTREES_ROOT}"
548
+ fi
549
+
348
550
  local start="\${PWD}"
349
551
  local config_root
350
552
  config_root="\$(_forge_find_config "\${start}")" || true
351
553
  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
554
+ local output
555
+ output=\$(node -e ${this.nodeConfigSnippet()} "\${config_root}/.feat-forge.json" 2>/dev/null)
556
+ if [[ -n "\${output}" ]]; then
557
+ eval "\${output}"
558
+ elif [[ -z "\${_FORGE_WORKTREES_ROOT}" ]]; then
559
+ _FORGE_WORKTREES_ROOT="\${config_root}/worktrees"
369
560
  fi
370
- echo "\${config_root}/features"
371
- return 0
561
+ elif [[ -z "\${_FORGE_WORKTREES_ROOT}" ]]; then
562
+ _FORGE_WORKTREES_ROOT="worktrees"
372
563
  fi
373
- echo "features"
564
+ _forge_config_loaded=1
565
+ }
566
+
567
+ _forge_feature_slugs() {
568
+ _forge_load_config
569
+ local prefix_dir="\${_FORGE_FEATURE_PREFIX%/}"
570
+ local dir="\${_FORGE_WORKTREES_ROOT}/\${prefix_dir}"
571
+ if [[ -d "\${dir}" ]]; then
572
+ find "\${dir}" -mindepth 1 -maxdepth 1 -type d -exec basename {} \\; 2>/dev/null
573
+ fi
574
+ }
575
+
576
+ _forge_fix_slugs() {
577
+ _forge_load_config
578
+ local prefix_dir="\${_FORGE_FIX_PREFIX%/}"
579
+ local dir="\${_FORGE_WORKTREES_ROOT}/\${prefix_dir}"
580
+ if [[ -d "\${dir}" ]]; then
581
+ find "\${dir}" -mindepth 1 -maxdepth 1 -type d -exec basename {} \\; 2>/dev/null
582
+ fi
583
+ }
584
+
585
+ _forge_release_slugs() {
586
+ _forge_load_config
587
+ local prefix_dir="\${_FORGE_RELEASE_PREFIX%/}"
588
+ local dir="\${_FORGE_WORKTREES_ROOT}/\${prefix_dir}"
589
+ if [[ -d "\${dir}" ]]; then
590
+ find "\${dir}" -mindepth 1 -maxdepth 1 -type d -exec basename {} \\; 2>/dev/null
591
+ fi
592
+ }
593
+
594
+ _forge_all_branches() {
595
+ _forge_load_config
596
+ local root="\${_FORGE_WORKTREES_ROOT}"
597
+ if [[ ! -d "\${root}" ]]; then
598
+ return
599
+ fi
600
+ local prefix_dirs=()
601
+ for p in "\${_FORGE_FEATURE_PREFIX%/}" "\${_FORGE_FIX_PREFIX%/}" "\${_FORGE_RELEASE_PREFIX%/}"; do
602
+ if [[ -n "\${p}" ]]; then
603
+ prefix_dirs+=("\${p}")
604
+ fi
605
+ done
606
+ for entry in "\${root}"/*/; do
607
+ [[ -d "\${entry}" ]] || continue
608
+ local name="\$(basename "\${entry}")"
609
+ local is_prefix=0
610
+ for pd in "\${prefix_dirs[@]}"; do
611
+ if [[ "\${name}" == "\${pd}" ]]; then
612
+ is_prefix=1
613
+ break
614
+ fi
615
+ done
616
+ if [[ \${is_prefix} -eq 1 ]]; then
617
+ for sub in "\${entry}"*/; do
618
+ [[ -d "\${sub}" ]] || continue
619
+ echo "\${name}/\$(basename "\${sub}")"
620
+ done
621
+ else
622
+ echo "\${name}"
623
+ fi
624
+ done
625
+ }
626
+
627
+ _forge_modes() {
628
+ _forge_load_config
629
+ echo "\${_FORGE_MODES}"
374
630
  }
375
631
 
376
632
  _forge() {
377
- local -a commands feature_commands mode_commands agent_commands
633
+ local -a commands feature_commands fix_commands release_commands
634
+ local -a services_commands env_commands maintenance_commands
635
+ local -a agent_commands
636
+
637
+ # Reset config cache for each completion
638
+ _forge_config_loaded=0
378
639
 
379
640
  commands=(
380
641
  ${commandsArray}
@@ -384,8 +645,24 @@ ${commandsArray}
384
645
  ${featureArray}
385
646
  )
386
647
 
387
- mode_commands=(
388
- ${modeArray}
648
+ fix_commands=(
649
+ ${fixArray}
650
+ )
651
+
652
+ release_commands=(
653
+ ${releaseArray}
654
+ )
655
+
656
+ services_commands=(
657
+ ${servicesArray}
658
+ )
659
+
660
+ env_commands=(
661
+ ${envArray}
662
+ )
663
+
664
+ maintenance_commands=(
665
+ ${maintenanceArray}
389
666
  )
390
667
 
391
668
  agent_commands=(
@@ -409,14 +686,52 @@ ${featureSlugCase} *)
409
686
  ;;
410
687
  esac
411
688
  ;;
689
+ fix)
690
+ case \${words[2]} in
691
+ ${fixSlugCase} *)
692
+ _describe 'fix command' fix_commands
693
+ ;;
694
+ esac
695
+ ;;
696
+ release)
697
+ case \${words[2]} in
698
+ ${releaseSlugCase} *)
699
+ _describe 'release command' release_commands
700
+ ;;
701
+ esac
702
+ ;;
703
+ services)
704
+ _describe 'services command' services_commands
705
+ ;;
706
+ env)
707
+ _describe 'env command' env_commands
708
+ ;;
709
+ maintenance)
710
+ if (( CURRENT == 2 )); then
711
+ _describe 'maintenance command' maintenance_commands
712
+ else
713
+ local -a branches
714
+ branches=(\${(f)"\$(_forge_all_branches)"})
715
+ _describe 'branch name' branches
716
+ fi
717
+ ;;
412
718
  mode)
413
- _describe 'mode command' mode_commands
719
+ local -a modes
720
+ modes=(\${(f)"\$(_forge_modes)"})
721
+ if [[ \${#modes} -gt 0 ]]; then
722
+ _describe 'mode name' modes
723
+ fi
414
724
  ;;
415
725
  agent)
416
726
  _describe 'agent command' agent_commands
417
727
  ;;
418
728
  ${mainSlugCase} completion)
419
- local shells
729
+ local -a shells
730
+ shells=('bash' 'zsh' 'fish' 'powershell' 'pwsh')
731
+ _describe 'shell type' shells
732
+ ;;
733
+ alias)
734
+ local -a shells
420
735
  shells=('bash' 'zsh' 'fish' 'powershell' 'pwsh')
421
736
  _describe 'shell type' shells
422
737
  ;;
@@ -436,50 +751,60 @@ compdef _forge forge
436
751
  # forge completion zsh > ~/.zsh/completions/_forge
437
752
  # # Add to .zshrc: fpath=(~/.zsh/completions $fpath)
438
753
  # # Then run: autoload -Uz compinit && compinit
754
+ #
755
+ # Tip: Run 'forge alias zsh' to generate shell aliases for quick access.
439
756
  `;
440
757
  }
441
- /**
442
- * Generate fish completion script.
443
- *
444
- * @returns Fish completion script content
445
- */
758
+ // ============================================================================
759
+ // FISH COMPLETION
760
+ // ============================================================================
446
761
  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
762
+ const data = this.getCompletionData();
763
+ // Main commands
764
+ const mainCommandsLines = data.mainCommands
453
765
  .map((cmd) => `complete -c forge -n "__fish_use_subcommand" -a ${cmd.name} -d "${cmd.description.replace(/"/g, '\\"')}"`)
454
766
  .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)"`)
767
+ // Helper to generate subcommand completions for a group
768
+ const genSubCmds = (parentName, cmd) => {
769
+ if (!cmd || !cmd.subcommands.length)
770
+ return '';
771
+ const subNames = cmd.subcommands.map((c) => c.name).join(' ');
772
+ return cmd.subcommands
773
+ .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, '\\"')}"`)
774
+ .join('\n');
775
+ };
776
+ const featureSubCmds = genSubCmds('feature', data.featureCmd);
777
+ const fixSubCmds = genSubCmds('fix', data.fixCmd);
778
+ const releaseSubCmds = genSubCmds('release', data.releaseCmd);
779
+ const servicesSubCmds = genSubCmds('services', data.servicesCmd);
780
+ const envSubCmds = genSubCmds('env', data.envCmd);
781
+ const maintenanceSubCmds = genSubCmds('maintenance', data.maintenanceCmd);
782
+ const agentSubCmds = genSubCmds('agent', data.agentCmd);
783
+ // Slug completions for feature/fix/release
784
+ const genSlugCompletions = (parentName, cmd, slugFn) => {
785
+ if (!cmd)
786
+ return '';
787
+ const slugCmds = cmd.subcommands.filter((c) => data.slugSubcommands.includes(c.name));
788
+ return slugCmds
789
+ .map((c) => `complete -c forge -n "__fish_seen_subcommand_from ${parentName}; and __fish_seen_subcommand_from ${c.name}" -a "(${slugFn})"`)
790
+ .join('\n');
791
+ };
792
+ const featureSlugCompletions = genSlugCompletions('feature', data.featureCmd, '__forge_feature_slugs');
793
+ const fixSlugCompletions = genSlugCompletions('fix', data.fixCmd, '__forge_fix_slugs');
794
+ const releaseSlugCompletions = genSlugCompletions('release', data.releaseCmd, '__forge_release_slugs');
795
+ // Root-level slug commands
796
+ const mainSlugCmds = data.mainCommands
797
+ .filter((cmd) => data.rootSlugCommands.includes(cmd.name));
798
+ const mainSlugCompletions = mainSlugCmds
799
+ .map((cmd) => `complete -c forge -n "__fish_seen_subcommand_from ${cmd.name}" -a "(__forge_all_branches)"`)
479
800
  .join('\n');
801
+ // Maintenance slug completion at position 3
802
+ const maintenanceSlugCompletion = data.maintenanceCmd?.subcommands
803
+ .map((c) => `complete -c forge -n "__fish_seen_subcommand_from maintenance; and __fish_seen_subcommand_from ${c.name}" -a "(__forge_all_branches)"`)
804
+ .join('\n') || '';
480
805
  return `# forge fish completion script
481
806
 
482
- # Helper functions to get available features
807
+ # Helper functions
483
808
  function __forge_find_config_root
484
809
  set -l dir $PWD
485
810
  while test -n "$dir" -a "$dir" != "/"
@@ -492,45 +817,107 @@ function __forge_find_config_root
492
817
  return 1
493
818
  end
494
819
 
495
- function __forge_worktrees_root
496
- if test -n "$FORGE_WORKTREES_ROOT"
497
- echo $FORGE_WORKTREES_ROOT
820
+ set -g _forge_config_loaded 0
821
+
822
+ function __forge_load_config
823
+ if test $_forge_config_loaded -eq 1
498
824
  return 0
499
825
  end
826
+ set -g _FORGE_WORKTREES_ROOT ""
827
+ set -g _FORGE_FEATURE_PREFIX "feature/"
828
+ set -g _FORGE_FIX_PREFIX "fix/"
829
+ set -g _FORGE_RELEASE_PREFIX "release/"
830
+ set -g _FORGE_MODES ""
831
+
832
+ if test -n "$FORGE_WORKTREES_ROOT"
833
+ set -g _FORGE_WORKTREES_ROOT $FORGE_WORKTREES_ROOT
834
+ end
835
+
500
836
  set -l config_root (__forge_find_config_root)
501
837
  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
838
+ set -l output (node -e ${this.nodeConfigSnippetFish()} "$config_root/.feat-forge.json" 2>/dev/null)
839
+ if test -n "$output"
840
+ eval $output
841
+ else if test -z "$_FORGE_WORKTREES_ROOT"
842
+ set -g _FORGE_WORKTREES_ROOT "$config_root/worktrees"
843
+ end
844
+ else if test -z "$_FORGE_WORKTREES_ROOT"
845
+ set -g _FORGE_WORKTREES_ROOT "worktrees"
846
+ end
847
+ set -g _forge_config_loaded 1
848
+ end
849
+
850
+ function __forge_feature_slugs
851
+ __forge_load_config
852
+ set -l prefix_dir (string trim --right --chars=/ $_FORGE_FEATURE_PREFIX)
853
+ set -l dir "$_FORGE_WORKTREES_ROOT/$prefix_dir"
854
+ if test -d "$dir"
855
+ for entry in $dir/*/
856
+ basename $entry
857
+ end
858
+ end
859
+ end
860
+
861
+ function __forge_fix_slugs
862
+ __forge_load_config
863
+ set -l prefix_dir (string trim --right --chars=/ $_FORGE_FIX_PREFIX)
864
+ set -l dir "$_FORGE_WORKTREES_ROOT/$prefix_dir"
865
+ if test -d "$dir"
866
+ for entry in $dir/*/
867
+ basename $entry
868
+ end
869
+ end
870
+ end
871
+
872
+ function __forge_release_slugs
873
+ __forge_load_config
874
+ set -l prefix_dir (string trim --right --chars=/ $_FORGE_RELEASE_PREFIX)
875
+ set -l dir "$_FORGE_WORKTREES_ROOT/$prefix_dir"
876
+ if test -d "$dir"
877
+ for entry in $dir/*/
878
+ basename $entry
518
879
  end
519
- echo "$config_root/features"
520
- return 0
521
880
  end
522
- echo "features"
523
881
  end
524
882
 
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
883
+ function __forge_all_branches
884
+ __forge_load_config
885
+ set -l root $_FORGE_WORKTREES_ROOT
886
+ if not test -d "$root"
887
+ return
888
+ end
889
+ 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)
890
+ for entry in $root/*/
891
+ set -l name (basename $entry)
892
+ set -l is_prefix 0
893
+ for pd in $prefix_dirs
894
+ if test "$name" = "$pd"
895
+ set is_prefix 1
896
+ break
897
+ end
898
+ end
899
+ if test $is_prefix -eq 1
900
+ for sub in $entry*/
901
+ if test -d "$sub"
902
+ echo "$name/"(basename $sub)
903
+ end
904
+ end
905
+ else
906
+ echo $name
530
907
  end
531
908
  end
532
909
  end
533
910
 
911
+ function __forge_modes
912
+ __forge_load_config
913
+ string split " " $_FORGE_MODES
914
+ end
915
+
916
+ # Reset config cache on each completion invocation
917
+ function __forge_reset_config --on-event fish_prompt
918
+ set -g _forge_config_loaded 0
919
+ end
920
+
534
921
  # Disable file completion by default
535
922
  complete -c forge -f
536
923
 
@@ -540,18 +927,42 @@ ${mainCommandsLines}
540
927
  # Feature subcommands
541
928
  ${featureSubCmds}
542
929
 
543
- # Feature commands with slug completion
930
+ # Feature slug completions
544
931
  ${featureSlugCompletions}
545
932
 
546
- # Mode subcommands
547
- ${modeSubCmds}
933
+ # Fix subcommands
934
+ ${fixSubCmds}
935
+
936
+ # Fix slug completions
937
+ ${fixSlugCompletions}
938
+
939
+ # Release subcommands
940
+ ${releaseSubCmds}
941
+
942
+ # Release slug completions
943
+ ${releaseSlugCompletions}
944
+
945
+ # Services subcommands
946
+ ${servicesSubCmds}
947
+
948
+ # Env subcommands
949
+ ${envSubCmds}
950
+
951
+ # Maintenance subcommands
952
+ ${maintenanceSubCmds}
953
+
954
+ # Maintenance slug completions
955
+ ${maintenanceSlugCompletion}
548
956
 
549
957
  # Agent subcommands
550
958
  ${agentSubCmds}
551
959
 
552
- # Main commands with feature slug completion
960
+ # Root-level commands with branch completion
553
961
  ${mainSlugCompletions}
554
962
 
963
+ # Mode command with mode name completion
964
+ complete -c forge -n "__fish_seen_subcommand_from mode" -a "(__forge_modes)"
965
+
555
966
  # Completion command with shell types
556
967
  complete -c forge -n "__fish_seen_subcommand_from completion" -a bash -d "Generate bash completion"
557
968
  complete -c forge -n "__fish_seen_subcommand_from completion" -a zsh -d "Generate zsh completion"
@@ -559,33 +970,49 @@ complete -c forge -n "__fish_seen_subcommand_from completion" -a fish -d "Genera
559
970
  complete -c forge -n "__fish_seen_subcommand_from completion" -a powershell -d "Generate PowerShell completion"
560
971
  complete -c forge -n "__fish_seen_subcommand_from completion" -a pwsh -d "Generate PowerShell completion"
561
972
 
973
+ # Alias command with shell types
974
+ complete -c forge -n "__fish_seen_subcommand_from alias" -a bash -d "Generate bash aliases"
975
+ complete -c forge -n "__fish_seen_subcommand_from alias" -a zsh -d "Generate zsh aliases"
976
+ complete -c forge -n "__fish_seen_subcommand_from alias" -a fish -d "Generate fish aliases"
977
+ complete -c forge -n "__fish_seen_subcommand_from alias" -a powershell -d "Generate PowerShell aliases"
978
+ complete -c forge -n "__fish_seen_subcommand_from alias" -a pwsh -d "Generate PowerShell aliases"
979
+
562
980
  # Installation instructions:
563
981
  # Option 1 - Add to ~/.config/fish/config.fish:
564
982
  # forge completion fish | source
565
983
  #
566
984
  # Option 2 - Save to completions directory:
567
985
  # forge completion fish > ~/.config/fish/completions/forge.fish
986
+ #
987
+ # Tip: Run 'forge alias fish' to generate shell aliases for quick access.
568
988
  `;
569
989
  }
570
- /**
571
- * Generate PowerShell completion script.
572
- *
573
- * @returns PowerShell completion script content
574
- */
990
+ // ============================================================================
991
+ // POWERSHELL COMPLETION
992
+ // ============================================================================
575
993
  generatePowerShellCompletion() {
576
- const mainCommands = this.getMainCommands();
577
- const featureCmd = this.findCommand('feature');
578
- const modeCmd = this.findCommand('mode');
579
- const agentCmd = this.findCommand('agent');
994
+ const data = this.getCompletionData();
580
995
  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));
996
+ const mainCommandsList = toPsArray(data.mainCommands.map((cmd) => cmd.name));
997
+ const featureCommandsList = toPsArray(data.featureCmd?.subcommands.map((cmd) => cmd.name) || []);
998
+ const fixCommandsList = toPsArray(data.fixCmd?.subcommands.map((cmd) => cmd.name) || []);
999
+ const releaseCommandsList = toPsArray(data.releaseCmd?.subcommands.map((cmd) => cmd.name) || []);
1000
+ const servicesCommandsList = toPsArray(data.servicesCmd?.subcommands.map((cmd) => cmd.name) || []);
1001
+ const envCommandsList = toPsArray(data.envCmd?.subcommands.map((cmd) => cmd.name) || []);
1002
+ const maintenanceCommandsList = toPsArray(data.maintenanceCmd?.subcommands.map((cmd) => cmd.name) || []);
1003
+ const agentCommandsList = toPsArray(data.agentCmd?.subcommands.map((cmd) => cmd.name) || []);
1004
+ const featureSlugCommandsList = toPsArray(data.featureCmd?.subcommands
1005
+ .filter((cmd) => data.slugSubcommands.includes(cmd.name))
1006
+ .map((cmd) => cmd.name) || []);
1007
+ const fixSlugCommandsList = toPsArray(data.fixCmd?.subcommands
1008
+ .filter((cmd) => data.slugSubcommands.includes(cmd.name))
1009
+ .map((cmd) => cmd.name) || []);
1010
+ const releaseSlugCommandsList = toPsArray(data.releaseCmd?.subcommands
1011
+ .filter((cmd) => data.slugSubcommands.includes(cmd.name))
1012
+ .map((cmd) => cmd.name) || []);
1013
+ const mainSlugCommandsList = toPsArray(data.mainCommands
1014
+ .filter((cmd) => data.rootSlugCommands.includes(cmd.name))
1015
+ .map((cmd) => cmd.name));
589
1016
  const completionShellsList = toPsArray(['bash', 'zsh', 'fish', 'powershell', 'pwsh']);
590
1017
  return `# forge PowerShell completion script
591
1018
 
@@ -600,40 +1027,100 @@ function __ForgeFindConfigRoot {
600
1027
  return $null
601
1028
  }
602
1029
 
603
- function __ForgeWorktreesRoot {
604
- if ($env:FORGE_WORKTREES_ROOT) { return $env:FORGE_WORKTREES_ROOT }
1030
+ $script:__ForgeConfigLoaded = $false
1031
+ $script:__ForgeConfig = $null
1032
+
1033
+ function __ForgeLoadConfig {
1034
+ if ($script:__ForgeConfigLoaded) { return }
1035
+
1036
+ $script:__ForgeConfig = @{
1037
+ WorktreesRoot = ""
1038
+ FeaturePrefix = "feature/"
1039
+ FixPrefix = "fix/"
1040
+ ReleasePrefix = "release/"
1041
+ Modes = @()
1042
+ }
1043
+
1044
+ if ($env:FORGE_WORKTREES_ROOT) {
1045
+ $script:__ForgeConfig.WorktreesRoot = $env:FORGE_WORKTREES_ROOT
1046
+ }
1047
+
605
1048
  $configRoot = __ForgeFindConfigRoot
606
1049
  if ($configRoot) {
607
1050
  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)
1051
+ $output = node -e ${this.nodeConfigSnippetPowerShell()} (Join-Path $configRoot ".feat-forge.json") 2>$null
1052
+ if ($output) {
1053
+ $parsed = $output | ConvertFrom-Json
1054
+ if ($parsed.worktreesRoot) { $script:__ForgeConfig.WorktreesRoot = $parsed.worktreesRoot }
1055
+ if ($parsed.featurePrefix) { $script:__ForgeConfig.FeaturePrefix = $parsed.featurePrefix }
1056
+ if ($parsed.fixPrefix) { $script:__ForgeConfig.FixPrefix = $parsed.fixPrefix }
1057
+ if ($parsed.releasePrefix) { $script:__ForgeConfig.ReleasePrefix = $parsed.releasePrefix }
1058
+ if ($parsed.modes) { $script:__ForgeConfig.Modes = $parsed.modes }
619
1059
  }
620
- if ($json.folders -and $json.folders.worktrees) {
621
- return (Join-Path $rootDir $json.folders.worktrees)
622
- }
623
- } catch {
1060
+ } catch {}
1061
+ if (-not $script:__ForgeConfig.WorktreesRoot) {
1062
+ $script:__ForgeConfig.WorktreesRoot = Join-Path $configRoot "worktrees"
624
1063
  }
625
- return (Join-Path $rootDir "features")
626
1064
  }
627
- return "features"
1065
+ if (-not $script:__ForgeConfig.WorktreesRoot) {
1066
+ $script:__ForgeConfig.WorktreesRoot = "worktrees"
1067
+ }
1068
+ $script:__ForgeConfigLoaded = $true
628
1069
  }
629
1070
 
630
1071
  function __ForgeFeatureSlugs {
631
- $worktreesRoot = __ForgeWorktreesRoot
632
- if (Test-Path $worktreesRoot) {
633
- Get-ChildItem -Directory -Path $worktreesRoot | ForEach-Object { $_.Name }
1072
+ __ForgeLoadConfig
1073
+ $prefix = $script:__ForgeConfig.FeaturePrefix.TrimEnd('/')
1074
+ $dir = Join-Path $script:__ForgeConfig.WorktreesRoot $prefix
1075
+ if (Test-Path $dir) {
1076
+ Get-ChildItem -Directory -Path $dir | ForEach-Object { $_.Name }
1077
+ }
1078
+ }
1079
+
1080
+ function __ForgeFixSlugs {
1081
+ __ForgeLoadConfig
1082
+ $prefix = $script:__ForgeConfig.FixPrefix.TrimEnd('/')
1083
+ $dir = Join-Path $script:__ForgeConfig.WorktreesRoot $prefix
1084
+ if (Test-Path $dir) {
1085
+ Get-ChildItem -Directory -Path $dir | ForEach-Object { $_.Name }
634
1086
  }
635
1087
  }
636
1088
 
1089
+ function __ForgeReleaseSlugs {
1090
+ __ForgeLoadConfig
1091
+ $prefix = $script:__ForgeConfig.ReleasePrefix.TrimEnd('/')
1092
+ $dir = Join-Path $script:__ForgeConfig.WorktreesRoot $prefix
1093
+ if (Test-Path $dir) {
1094
+ Get-ChildItem -Directory -Path $dir | ForEach-Object { $_.Name }
1095
+ }
1096
+ }
1097
+
1098
+ function __ForgeAllBranches {
1099
+ __ForgeLoadConfig
1100
+ $root = $script:__ForgeConfig.WorktreesRoot
1101
+ if (-not (Test-Path $root)) { return }
1102
+ $prefixDirs = @(
1103
+ $script:__ForgeConfig.FeaturePrefix.TrimEnd('/'),
1104
+ $script:__ForgeConfig.FixPrefix.TrimEnd('/'),
1105
+ $script:__ForgeConfig.ReleasePrefix.TrimEnd('/')
1106
+ )
1107
+ Get-ChildItem -Directory -Path $root | ForEach-Object {
1108
+ $name = $_.Name
1109
+ if ($prefixDirs -contains $name) {
1110
+ Get-ChildItem -Directory -Path $_.FullName | ForEach-Object {
1111
+ "$name/$($_.Name)"
1112
+ }
1113
+ } else {
1114
+ $name
1115
+ }
1116
+ }
1117
+ }
1118
+
1119
+ function __ForgeModes {
1120
+ __ForgeLoadConfig
1121
+ $script:__ForgeConfig.Modes
1122
+ }
1123
+
637
1124
  function __ForgeCompletionResults {
638
1125
  param([string[]]$Items, [string]$Word)
639
1126
  if (-not $Items) { return }
@@ -644,15 +1131,24 @@ function __ForgeCompletionResults {
644
1131
 
645
1132
  $mainCommands = @(${mainCommandsList})
646
1133
  $featureCommands = @(${featureCommandsList})
647
- $modeCommands = @(${modeCommandsList})
1134
+ $fixCommands = @(${fixCommandsList})
1135
+ $releaseCommands = @(${releaseCommandsList})
1136
+ $servicesCommands = @(${servicesCommandsList})
1137
+ $envCommands = @(${envCommandsList})
1138
+ $maintenanceCommands = @(${maintenanceCommandsList})
648
1139
  $agentCommands = @(${agentCommandsList})
649
1140
  $featureSlugCommands = @(${featureSlugCommandsList})
1141
+ $fixSlugCommands = @(${fixSlugCommandsList})
1142
+ $releaseSlugCommands = @(${releaseSlugCommandsList})
650
1143
  $mainSlugCommands = @(${mainSlugCommandsList})
651
1144
  $completionShells = @(${completionShellsList})
652
1145
 
653
1146
  Register-ArgumentCompleter -CommandName forge -ScriptBlock {
654
1147
  param($commandName, $parameterName, $wordToComplete, $commandAst, $fakeBoundParameters)
655
1148
 
1149
+ # Reset config cache for each completion
1150
+ $script:__ForgeConfigLoaded = $false
1151
+
656
1152
  $elements = $commandAst.CommandElements | ForEach-Object { $_.Value }
657
1153
 
658
1154
  if ($elements.Count -le 1) {
@@ -673,8 +1169,46 @@ Register-ArgumentCompleter -CommandName forge -ScriptBlock {
673
1169
  return
674
1170
  }
675
1171
  }
1172
+ 'fix' {
1173
+ if ($elements.Count -le 2) {
1174
+ __ForgeCompletionResults -Items $fixCommands -Word $wordToComplete
1175
+ return
1176
+ }
1177
+ $sub = $elements[2]
1178
+ if ($fixSlugCommands -contains $sub) {
1179
+ __ForgeCompletionResults -Items (__ForgeFixSlugs) -Word $wordToComplete
1180
+ return
1181
+ }
1182
+ }
1183
+ 'release' {
1184
+ if ($elements.Count -le 2) {
1185
+ __ForgeCompletionResults -Items $releaseCommands -Word $wordToComplete
1186
+ return
1187
+ }
1188
+ $sub = $elements[2]
1189
+ if ($releaseSlugCommands -contains $sub) {
1190
+ __ForgeCompletionResults -Items (__ForgeReleaseSlugs) -Word $wordToComplete
1191
+ return
1192
+ }
1193
+ }
1194
+ 'services' {
1195
+ __ForgeCompletionResults -Items $servicesCommands -Word $wordToComplete
1196
+ return
1197
+ }
1198
+ 'env' {
1199
+ __ForgeCompletionResults -Items $envCommands -Word $wordToComplete
1200
+ return
1201
+ }
1202
+ 'maintenance' {
1203
+ if ($elements.Count -le 2) {
1204
+ __ForgeCompletionResults -Items $maintenanceCommands -Word $wordToComplete
1205
+ return
1206
+ }
1207
+ __ForgeCompletionResults -Items (__ForgeAllBranches) -Word $wordToComplete
1208
+ return
1209
+ }
676
1210
  'mode' {
677
- __ForgeCompletionResults -Items $modeCommands -Word $wordToComplete
1211
+ __ForgeCompletionResults -Items (__ForgeModes) -Word $wordToComplete
678
1212
  return
679
1213
  }
680
1214
  'agent' {
@@ -685,9 +1219,13 @@ Register-ArgumentCompleter -CommandName forge -ScriptBlock {
685
1219
  __ForgeCompletionResults -Items $completionShells -Word $wordToComplete
686
1220
  return
687
1221
  }
1222
+ 'alias' {
1223
+ __ForgeCompletionResults -Items $completionShells -Word $wordToComplete
1224
+ return
1225
+ }
688
1226
  default {
689
1227
  if ($mainSlugCommands -contains $first) {
690
- __ForgeCompletionResults -Items (__ForgeFeatureSlugs) -Word $wordToComplete
1228
+ __ForgeCompletionResults -Items (__ForgeAllBranches) -Word $wordToComplete
691
1229
  return
692
1230
  }
693
1231
  }
@@ -695,8 +1233,10 @@ Register-ArgumentCompleter -CommandName forge -ScriptBlock {
695
1233
  }
696
1234
 
697
1235
  # Installation instructions:
698
- # Option 1 - Add to $PROFILE:
1236
+ # Option 1 - Add to \$PROFILE:
699
1237
  # forge completion powershell | Out-String | Invoke-Expression
1238
+ #
1239
+ # Tip: Run 'forge alias powershell' to generate shell aliases for quick access.
700
1240
  `;
701
1241
  }
702
1242
  }