feat-forge 1.0.1

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.
Files changed (93) hide show
  1. package/LICENSE +661 -0
  2. package/README.md +350 -0
  3. package/dist/cli.js +306 -0
  4. package/dist/commands/AbstractCommands.js +16 -0
  5. package/dist/commands/AgentCommands.js +14 -0
  6. package/dist/commands/BranchCommands.js +400 -0
  7. package/dist/commands/CompletionCommands.js +702 -0
  8. package/dist/commands/EnvCommands.js +56 -0
  9. package/dist/commands/FeatureCommands.js +4 -0
  10. package/dist/commands/FixCommands.js +4 -0
  11. package/dist/commands/InitCommands.js +380 -0
  12. package/dist/commands/MaintenanceCommands.js +39 -0
  13. package/dist/commands/ModeCommands.js +15 -0
  14. package/dist/commands/ProxyCommands.js +14 -0
  15. package/dist/commands/ReleaseCommands.js +4 -0
  16. package/dist/commands/ServicesCommands.js +95 -0
  17. package/dist/commands/SubBranchCommands.js +49 -0
  18. package/dist/commands/types/InitOptions.js +1 -0
  19. package/dist/foundation/BranchContext.js +427 -0
  20. package/dist/foundation/ForgeConfig.js +264 -0
  21. package/dist/foundation/ForgeConfigFile.js +391 -0
  22. package/dist/foundation/ForgeContext.js +169 -0
  23. package/dist/foundation/NpmHelper.js +131 -0
  24. package/dist/foundation/PathHelper.js +56 -0
  25. package/dist/foundation/PortAllocator.js +192 -0
  26. package/dist/foundation/Proxy.js +176 -0
  27. package/dist/foundation/Repository.js +431 -0
  28. package/dist/foundation/errors/ForgeError.js +9 -0
  29. package/dist/foundation/errors/_error.config.js +12 -0
  30. package/dist/foundation/errors/generated/ForgeBadStateError.js +11 -0
  31. package/dist/foundation/errors/generated/ForgeConfigError.js +11 -0
  32. package/dist/foundation/errors/generated/ForgeExpectMainRepositoryError.js +11 -0
  33. package/dist/foundation/errors/generated/ForgeModeNotDefinedError.js +11 -0
  34. package/dist/foundation/errors/generated/ForgeNotInActiveBranchError.js +11 -0
  35. package/dist/foundation/errors/generated/ForgePortAllocationsLoadError.js +11 -0
  36. package/dist/foundation/errors/generated/ForgePortNotAssignedError.js +11 -0
  37. package/dist/foundation/errors/generated/ForgePortRangeExhaustedError.js +11 -0
  38. package/dist/foundation/errors/generated/ForgeServicesScanError.js +11 -0
  39. package/dist/foundation/errors/generated/ForgeServicesValidationError.js +11 -0
  40. package/dist/foundation/errors/index.js +13 -0
  41. package/dist/foundation/types/AIAgent.js +1 -0
  42. package/dist/foundation/types/AIAgentName.js +11 -0
  43. package/dist/foundation/types/DeepPartial.js +1 -0
  44. package/dist/foundation/types/IDE.js +1 -0
  45. package/dist/foundation/types/IDEName.js +7 -0
  46. package/dist/foundation/types/ModeConfig.js +1 -0
  47. package/dist/foundation/types/RepositoryInfos.js +1 -0
  48. package/dist/foundation/types/Services.js +156 -0
  49. package/dist/foundation/types/ShellName.js +11 -0
  50. package/dist/lib/agents.js +47 -0
  51. package/dist/lib/bootstrap.js +54 -0
  52. package/dist/lib/branch.js +4 -0
  53. package/dist/lib/config.js +65 -0
  54. package/dist/lib/constants.js +13 -0
  55. package/dist/lib/env.js +20 -0
  56. package/dist/lib/fs.js +156 -0
  57. package/dist/lib/git.js +170 -0
  58. package/dist/lib/hooks.js +98 -0
  59. package/dist/lib/ide.js +75 -0
  60. package/dist/lib/merger.js +103 -0
  61. package/dist/lib/platform.js +13 -0
  62. package/dist/lib/prompt.js +134 -0
  63. package/dist/lib/proxy-dashboard.js +75 -0
  64. package/dist/lib/scanner.js +118 -0
  65. package/dist/lib/services.js +132 -0
  66. package/dist/lib/slug.js +35 -0
  67. package/dist/lib/templates.js +115 -0
  68. package/dist/lib/validator.js +15 -0
  69. package/dist/templates/SPEC.md +21 -0
  70. package/dist/templates/TODO.md +5 -0
  71. package/dist/templates/agent/001.general.Omnibus.agent.md +4 -0
  72. package/dist/templates/agent/002.discovery.Inventorius.agent.md +4 -0
  73. package/dist/templates/agent/003.design.Architecturius.agent.md +8 -0
  74. package/dist/templates/agent/004.plan.Strategos.agent.md +8 -0
  75. package/dist/templates/agent/005.tdd.TestDrivenCodificius.agent.md +8 -0
  76. package/dist/templates/agent/006.code.Codificius.agent.md +8 -0
  77. package/dist/templates/agent/007.simplify.Consolidarius.agent.md +8 -0
  78. package/dist/templates/agent/008.review.Auditorix.agent.md +8 -0
  79. package/dist/templates/agent/009.testwriter.TestScriptor.agent.md +8 -0
  80. package/dist/templates/agent/010.testexecutor.TestExecutor.agent.md +8 -0
  81. package/dist/templates/agent/011.commit.Scribus.agent.md +10 -0
  82. package/dist/templates/agent/CONTEXT.code.md +145 -0
  83. package/dist/templates/agent/CONTEXT.spec.md +98 -0
  84. package/dist/templates/agent/Copilot/Code.agent.md +28 -0
  85. package/dist/templates/agent/Copilot/CodeCommit.agent.md +16 -0
  86. package/dist/templates/agent/Copilot/Feature-Builder.agent.md +49 -0
  87. package/dist/templates/agent/Copilot/Reviewer.agent.md +17 -0
  88. package/dist/templates/agent/Copilot/Simplifier.agent.md +21 -0
  89. package/dist/templates/agent/Copilot/Specs.agent.md +66 -0
  90. package/dist/templates/agent/Copilot/SpecsCommit.agent.md +19 -0
  91. package/dist/templates/agent/Copilot/TODO-Reader.agent.md +18 -0
  92. package/dist/templates/agent/Copilot/Tester.agent.md +12 -0
  93. package/package.json +76 -0
package/README.md ADDED
@@ -0,0 +1,350 @@
1
+ # FeatForge
2
+
3
+ **FeatForge** is a feature-first workflow and CLI (`forge`) to help you build software at scale, with (or without) AI agents.
4
+
5
+ Its goal is to make the specification of features explicit and separate the thinking/specifying phase from the coding/implementation phase, across multiple agents and repositories, while keeping everything organized and traceable.
6
+
7
+ With **FeatForge** you will be able to :
8
+
9
+ - Parallelize work on multiple features:
10
+ - across multiple repositories.
11
+ - accross multiple agents.
12
+ - while keeping track of everything.
13
+ - Come back to any feature after days/weeks and immediately understand its initial specifications.
14
+ - Change agent configurations and prompts on a per-feature basis.
15
+ - Switch between agents whenever you want, without losing any context or work.
16
+
17
+ **FeatForge** is :
18
+
19
+ - Developed using FeatForge itself from the start. You can look at the `.specs/` folder to see how features are specified and implemented, and how agents are configured. Test and Learn by example!
20
+ - Not opinionated about how you specify features, how you implement them, or how you use agents. It just provides a structure (yet customizable) and a workflow to keep everything organized and traceable.
21
+ - Partially tested with : Copilot, Codex, Claude code (+Ollama, LM-Studio). But it should work with any agent that can be configured to read/write files in the `.active-spec` folders.
22
+
23
+ **_This is an expirement, trying to mix between classic development and vibe-coding in large project with quality and sustainability in mind by making specification explicit and separating thinking from coding accross multiple agents and repositories._**
24
+
25
+ ## Key Concepts
26
+
27
+ - A **branch** is a self-contained unit with its own spec (`SPEC.md`) and tasks (`TODO.md`).
28
+ - You always work inside one **active spec** (`.active-spec`) per git worktree, which can span multiple repositories.
29
+ - Multiple modes are available per branch (see [Modes](#modes)).
30
+ - Agents goals are always scoped to a branch, never global.
31
+ - You can launch multiple agents per branch and work on multiple branches in parallel.
32
+ - Finished branches are archived in `.specs/.archives/`.
33
+ - Branch types: `anything` `feature/`, `fix/`, `release/` (each with their own customizable prefix).
34
+
35
+ ---
36
+
37
+ ## Installation
38
+
39
+ ```bash
40
+ npm install -g feat-forge-cli
41
+ forge --version
42
+ ```
43
+
44
+ ---
45
+
46
+ ## Quick Start
47
+
48
+ ```bash
49
+ # Initialize FeatForge in your project
50
+ forge init
51
+
52
+ # Create a new feature (branch: feature/my-feature (customizable prefix))
53
+ forge feature create my-feature
54
+
55
+ # Start working on a feature (creates worktrees)
56
+ forge feature start my-feature
57
+
58
+ # Switch mode (updates agent files with the templates for this mode)
59
+ forge mode code
60
+
61
+ # Stop working on a feature (cleanup worktrees)
62
+ forge feature stop my-feature
63
+
64
+ # Archive a completed feature
65
+ forge feature archive my-feature
66
+ ```
67
+
68
+ ---
69
+
70
+ ## Recommended Folder Structure
71
+
72
+ Here is a typical FeatForge project layout (forge creates most folders/files automatically):
73
+
74
+ ```
75
+ forge-project-root/
76
+ .feat-forge.json # Configuration file for the project (per user)
77
+ repo1/
78
+ .git/ # Git repository for repo1
79
+ repo2/ # Main repo (contains .specs/)
80
+ .git/ # Git repository for repo2
81
+ .specs/ # All branch spec directories (created on first branch creation)
82
+ .archives/ # Archived branch folders (moved here when archived)
83
+ .template/ # Template for new branches
84
+ agent/ # Agent configuration templates
85
+ CONTEXT.spec.md # Legacy spec mode context template
86
+ CONTEXT.code.md # Legacy code mode context template
87
+ <branch-slug>/ # Active branch
88
+ SPEC.md # Branch specification (required)
89
+ TODO.md # Implementation tasks (required)
90
+ .forge-mode # Current mode
91
+ repo3/
92
+ .git/ # Git repository for repo3
93
+ worktrees/
94
+ 001-bootstrap/ # Example active branch directory (created on branch start)
95
+ repo1/ # git worktree for repo1 (created on branch start)
96
+ .active-spec -> ../repo2/.active-spec # symlink to active spec in main repo
97
+ repo2/ # git worktree for repo2 (created on branch start)
98
+ .active-spec -> ../.specs/001-bootstrap # symlink to active spec in this repo
99
+ .specs/
100
+ 001-bootstrap/ # actual branch folder with spec and agent context
101
+ .forge-mode # current mode
102
+ SPEC.md # branch specification
103
+ TODO.md # implementation tasks
104
+ repo3/ # git worktree for repo3 (created on branch start)
105
+ .active-spec -> ../repo2/.active-spec # symlink to active spec in main repo
106
+ ```
107
+
108
+ ---
109
+
110
+ ## Modes
111
+
112
+ Each branch has a mode stored in `.specs/<slug>/.forge-mode`. Switching modes updates agent adapter files with the corresponding templates.
113
+ Agent name are useful when calling subagent
114
+
115
+ Built-in modes:
116
+
117
+ | Mode | Agent | Description |
118
+ | --------------- | -------------------- | ----------------------------------- |
119
+ | `general` | Omnibus | General-purpose agent |
120
+ | `discovery` | Inventorius | Explore and understand the codebase |
121
+ | `design` | Architecturius | Design architecture and specs |
122
+ | `plan` | Strategos | Plan implementation strategy |
123
+ | `tdd` | TestDrivenCodificius | Test-driven development |
124
+ | `code` | Codificius | Implement according to the spec |
125
+ | `simplify` | Consolidarius | Simplify and consolidate code |
126
+ | `review` | Auditorix | Review code |
127
+ | `test-writer` | TestScriptor | Write tests |
128
+ | `test-executor` | TestExecutor | Execute tests |
129
+ | `commit` | Scribus | Commit changes |
130
+
131
+ Switch modes at any time:
132
+
133
+ ```bash
134
+ forge mode <mode>
135
+ ```
136
+
137
+ ---
138
+
139
+ ## Command Reference
140
+
141
+ ### `forge init`
142
+
143
+ Initialize FeatForge in your project. Interactively creates `.feat-forge.json`, scans for git repos and asks for options.
144
+
145
+ **Usage:**
146
+
147
+ ```bash
148
+ forge init
149
+ ```
150
+
151
+ **Options:**
152
+
153
+ - `--yes` / `-y` : Accept all defaults
154
+ - `--force` / `-f` : Overwrite config (with backup)
155
+ - `--repositories <paths>` : Comma-separated repo paths
156
+ - `--agents <names>` : Comma-separated agent names
157
+ - `--ides <names>` : Comma-separated IDE names
158
+
159
+ ---
160
+
161
+ ### `forge feature create <slug>`
162
+
163
+ Create a new feature branch folder and initialize its spec.
164
+
165
+ **Usage:**
166
+
167
+ ```bash
168
+ forge feature create my-feature
169
+ ```
170
+
171
+ **Options:**
172
+
173
+ - `--yes` : Skip confirmation
174
+ - `--no-branch` : Do not create a branch
175
+
176
+ ---
177
+
178
+ ### `forge feature start <slug>`
179
+
180
+ Create git worktrees for all repos and set the feature as active.
181
+
182
+ **Usage:**
183
+
184
+ ```bash
185
+ forge feature start my-feature
186
+ ```
187
+
188
+ **Options:**
189
+
190
+ - `--ide <name>` : Open in specified IDE
191
+
192
+ ---
193
+
194
+ ### `forge feature list`
195
+
196
+ List all active feature branches and their worktrees.
197
+
198
+ **Usage:**
199
+
200
+ ```bash
201
+ forge feature list
202
+ ```
203
+
204
+ ---
205
+
206
+ ### `forge feature stop <slug>`
207
+
208
+ Clean up worktrees and remove the active spec symlink.
209
+
210
+ **Usage:**
211
+
212
+ ```bash
213
+ forge feature stop my-feature
214
+ ```
215
+
216
+ ---
217
+
218
+ ### `forge feature archive <slug>`
219
+
220
+ Move a completed feature to `.specs/.archives/`.
221
+
222
+ **Usage:**
223
+
224
+ ```bash
225
+ forge feature archive my-feature
226
+ ```
227
+
228
+ **Options:**
229
+
230
+ - `--force` : Skip confirmation
231
+
232
+ ---
233
+
234
+ ### `forge feature merge <slug>`
235
+
236
+ Merge a feature branch into a target branch (across all repos).
237
+
238
+ ---
239
+
240
+ ### `forge feature rebase <slug>`
241
+
242
+ Rebase a feature branch onto a base branch (across all repos).
243
+
244
+ ---
245
+
246
+ ### `forge feature open [slug]`
247
+
248
+ Open a feature branch in the configured IDE.
249
+
250
+ ---
251
+
252
+ ### `forge feature resync <slug>`
253
+
254
+ Resync all repos to the expected branch.
255
+
256
+ ---
257
+
258
+ ### `forge fix` / `forge release` / `forge <command> branch`
259
+
260
+ Same commands as `forge feature`, but with automatic `fix/` or `release/` prefix for branch names, or just plain branch names if you prefer.
261
+
262
+ ```bash
263
+ forge fix create my-bugfix
264
+ forge release create v1.2.0
265
+ forge start dev
266
+ ```
267
+
268
+ ---
269
+
270
+ ### `forge merge <slug>` / `forge rebase <slug>`
271
+
272
+ Top-level shortcuts for merge and rebase operations (across all repos).
273
+
274
+ ---
275
+
276
+ ### `forge mode <mode>`
277
+
278
+ Switch agents to the given mode (updates agent files with the templates for this mode).
279
+
280
+ **Usage:**
281
+
282
+ ```bash
283
+ forge mode code
284
+ forge mode review
285
+ ```
286
+
287
+ ---
288
+
289
+ ### `forge agent refresh`
290
+
291
+ Refresh agent adapter files for the nearest branch.
292
+
293
+ ---
294
+
295
+ ### `forge services scan [branch]`
296
+
297
+ Scan repositories for service declarations and generate configuration with allocated ports.
298
+
299
+ ---
300
+
301
+ ### `forge services list [branch]`
302
+
303
+ List discovered services with their allocated ports.
304
+
305
+ ---
306
+
307
+ ### `forge env update [branch]`
308
+
309
+ Generate `.envrc` from `generated.services.json`.
310
+
311
+ ---
312
+
313
+ ### `forge env show [branch]`
314
+
315
+ Display current `.envrc` and port allocation information.
316
+
317
+ ---
318
+
319
+ ### `forge proxy`
320
+
321
+ Start a reverse-proxy server routing branches via subdomains.
322
+
323
+ ---
324
+
325
+ ### `forge maintenance rewrite-agent-files <slug>`
326
+
327
+ Rewrite all agent template files from built-in templates (overwrite).
328
+
329
+ ---
330
+
331
+ ### `forge completion <shell>`
332
+
333
+ Generate shell completion script (bash, zsh, fish, powershell).
334
+
335
+ **Usage:**
336
+
337
+ ```bash
338
+ forge completion bash
339
+ forge completion zsh
340
+ ```
341
+
342
+ ---
343
+
344
+ ## Help
345
+
346
+ For all commands and options:
347
+
348
+ ```bash
349
+ forge --help
350
+ ```
package/dist/cli.js ADDED
@@ -0,0 +1,306 @@
1
+ #!/usr/bin/env node
2
+ import 'reflect-metadata';
3
+ // -----------
4
+ import { Command } from 'commander';
5
+ import { AgentCommands } from './commands/AgentCommands.js';
6
+ import { MaintenanceCommands } from './commands/MaintenanceCommands.js';
7
+ import { CompletionCommands } from './commands/CompletionCommands.js';
8
+ import { FeatureCommands } from './commands/FeatureCommands.js';
9
+ import { InitCommands } from './commands/InitCommands.js';
10
+ import { ModeCommands } from './commands/ModeCommands.js';
11
+ import { ForgeContext } from './foundation/ForgeContext.js';
12
+ import { loadForgeContext } from './lib/config.js';
13
+ import { ForgeConfig } from './foundation/ForgeConfig.js';
14
+ import { ShellName } from './foundation/types/ShellName.js';
15
+ import { FixCommands } from './commands/FixCommands.js';
16
+ import { ReleaseCommands } from './commands/ReleaseCommands.js';
17
+ import { BranchCommands } from './commands/BranchCommands.js';
18
+ import { ForgeConfigFile } from './foundation/ForgeConfigFile.js';
19
+ import { plainToInstance } from 'class-transformer';
20
+ import { ServicesCommands } from './commands/ServicesCommands.js';
21
+ import { EnvCommands } from './commands/EnvCommands.js';
22
+ import { ProxyCommands } from './commands/ProxyCommands.js';
23
+ // @ts-ignore
24
+ import packageJson from '../package.json' with { type: 'json' };
25
+ /**
26
+ * Register the init command on the main CLI program.
27
+ */
28
+ function registerInitCommands(program) {
29
+ const handlers = new InitCommands();
30
+ program
31
+ .command('init')
32
+ .description('Create a .feat-forge.json in the current folder')
33
+ .option('-y, --yes', 'Accept defaults and write configuration without prompting')
34
+ .option('-f, --force', 'Force overwrite existing config (creates timestamped backup)')
35
+ .option('--non-interactive', 'Require all values via flags, no interactive prompts')
36
+ .option('-q, --quiet', 'Minimize console output')
37
+ .option('--path <dir>', 'Target directory for config file (defaults to current directory)')
38
+ .option('--root-dir <dir>', 'Set explicit rootDir value in config')
39
+ .option('--repositories <paths>', 'Repository paths (comma-separated or JSON array)')
40
+ .option('--agents <names>', 'Agent names (comma-separated or JSON array)')
41
+ .option('--ides <names>', 'IDE names (comma-separated or JSON array)')
42
+ .action(async (options) => {
43
+ await handlers.init({
44
+ yes: options.yes,
45
+ force: options.force,
46
+ nonInteractive: options.nonInteractive,
47
+ quiet: options.quiet,
48
+ path: options.path,
49
+ rootDir: options.rootDir,
50
+ repositories: options.repositories,
51
+ agents: options.agents,
52
+ ides: options.ides,
53
+ });
54
+ });
55
+ }
56
+ function genericBranchTypeCommands(baseCommand, handlers, subType = null) {
57
+ const argName = subType ? `${subType}` : 'branch-name';
58
+ const argDesc = subType ? `${subType} name (without prefix)` : 'Branch name';
59
+ baseCommand
60
+ .command('create')
61
+ .argument(`<${argName}>`, argDesc)
62
+ .description(`Create a new ${subType ? subType + ' ' : ''}branch folder and initialize its spec`)
63
+ .action(handlers.create.bind(handlers));
64
+ baseCommand
65
+ .command('start')
66
+ .argument(`<${argName}>`, argDesc)
67
+ .description(`Start working on a ${subType ? subType + ' ' : ''}branch : create the worktrees and necessary spec files`)
68
+ .action(handlers.start.bind(handlers));
69
+ baseCommand
70
+ .command('stop')
71
+ .argument(`<${argName}>`, argDesc)
72
+ .description(`Stop working on a ${subType ? subType + ' ' : ''}branch and remove its worktrees`)
73
+ .action(handlers.stop.bind(handlers));
74
+ baseCommand
75
+ .command('list')
76
+ .argument(`[prefix]`, 'Optional prefix to filter branches (e.g. "feat/")')
77
+ .description(`List all active ${subType ? subType + ' ' : ''}branches worktrees`)
78
+ .action(handlers.list.bind(handlers));
79
+ baseCommand
80
+ .command('resync')
81
+ .argument(`<${argName}>`, argDesc)
82
+ .description(`Ensure all repos in a ${subType ? subType + ' ' : ''}branch are on the correct branch (useful if user manually switched branches in a worktree)`)
83
+ .action(handlers.resync.bind(handlers));
84
+ baseCommand
85
+ .command('archive')
86
+ .argument(`<${argName}>`, argDesc)
87
+ .description(`Archive a ${subType ? subType + ' ' : ''}branch by moving it to .specs/.archives/`)
88
+ .action(handlers.archive.bind(handlers));
89
+ baseCommand
90
+ .command('open')
91
+ .argument(`[${argName}]`, argDesc)
92
+ .description(`Open the given ${subType ? subType + ' ' : ''}branch in the configured IDE (if no ${subType ? subType + ' ' : ''}branch is given, opens the nearest ${subType ? subType + ' ' : ''}branch)`)
93
+ .action(handlers.open.bind(handlers));
94
+ baseCommand
95
+ .command('merge')
96
+ .argument(`<${argName}>`, argDesc)
97
+ .description(`Merge a ${subType ? subType + ' ' : ''}branch into a target branch (accross all repos). It will also ask for next actions to potentially delete the branch after merge.`)
98
+ .action(handlers.merge.bind(handlers));
99
+ baseCommand
100
+ .command('rebase')
101
+ .argument(`<${argName}>`, argDesc)
102
+ .description(`Rebase a ${subType ? subType + ' ' : ''}branch onto a base branch (accross all repos)`)
103
+ .action(handlers.rebase.bind(handlers));
104
+ }
105
+ /**
106
+ * Register feature subcommands on the main CLI program.
107
+ */
108
+ function registerBranchCommands(program, context) {
109
+ return genericBranchTypeCommands(program, new BranchCommands(context));
110
+ }
111
+ /**
112
+ * Register fix subcommands on the main CLI program.
113
+ */
114
+ function registerFixCommands(program, context) {
115
+ const fix = program
116
+ .command('fix')
117
+ .description(`Same base commands, but with automatic "${context.options.git.fixBranchPrefix}" prefix for branch names`);
118
+ const handlers = new FixCommands(context);
119
+ return genericBranchTypeCommands(fix, handlers, 'fix');
120
+ }
121
+ /**
122
+ * Register release subcommands on the main CLI program.
123
+ */
124
+ function registerReleaseCommands(program, context) {
125
+ const release = program
126
+ .command('release')
127
+ .description(`Same base commands, but with automatic "${context.options.git.releaseBranchPrefix}" prefix for branch names`);
128
+ const handlers = new ReleaseCommands(context);
129
+ return genericBranchTypeCommands(release, handlers, 'release');
130
+ }
131
+ /**
132
+ * Register feature subcommands on the main CLI program.
133
+ */
134
+ function registerFeatureCommands(program, context) {
135
+ const feature = program
136
+ .command('feature')
137
+ .description(`Same base commands, but with automatic "${context.options.git.featureBranchPrefix}" prefix for branch names`);
138
+ const handlers = new FeatureCommands(context);
139
+ return genericBranchTypeCommands(feature, handlers, 'feature');
140
+ }
141
+ /**
142
+ * Register the mode commands on the main CLI program.
143
+ */
144
+ function registerModeCommands(program, context) {
145
+ const handlers = new ModeCommands(context);
146
+ program
147
+ .command('mode')
148
+ .argument('<mode>', 'Mode to switch to (as defined in the config)')
149
+ .description('Switch agents to this mode (updates agent files with the templates for this mode)')
150
+ .action(handlers.setMode.bind(handlers));
151
+ }
152
+ /**
153
+ * Register agent commands on the main CLI program.
154
+ */
155
+ function registerAgentCommands(program, context) {
156
+ const handlers = new AgentCommands(context);
157
+ const agent = program.command('agent').description('Manage agent adapters');
158
+ agent.command('refresh').description('Refresh agent adapter files for the nearest branch').action(handlers.refresh.bind(handlers));
159
+ }
160
+ /**
161
+ * Register maintenance commands on the main CLI program.
162
+ */
163
+ function registerMaintenanceCommands(program, context) {
164
+ const handlers = new MaintenanceCommands(context);
165
+ const maintenance = program.command('maintenance').description('Maintenance utilities');
166
+ maintenance
167
+ .command('rewrite-agent-files <slug>')
168
+ .description('Rewrite all agent template files from built-in templates (overwrite)')
169
+ .option('--dry-run', 'Simulate changes without writing files')
170
+ .option('--commit', 'Commit changes if files are written')
171
+ .action(async (slug, opts) => {
172
+ await handlers.installAgentTemplateLocally({ dryRun: opts.dryRun, commit: opts.commit });
173
+ });
174
+ }
175
+ /**
176
+ * Register services commands on the main CLI program.
177
+ */
178
+ function registerServicesCommands(program, context) {
179
+ const handlers = new ServicesCommands(context);
180
+ const services = program.command('services').description('Manage service declarations and configurations');
181
+ services
182
+ .command('scan')
183
+ .argument('[branch-name]', 'Optional branch name (defaults to current branch)')
184
+ .description('Scan repositories for .forge/services.json and generate configuration with allocated ports')
185
+ .action(handlers.scan.bind(handlers));
186
+ services
187
+ .command('list')
188
+ .option('--json', 'Output as JSON')
189
+ .argument('[branch-name]', 'Optional branch name (defaults to current branch)')
190
+ .description('List discovered services with their allocated ports')
191
+ .action((branch, options) => {
192
+ const format = options.json ? 'json' : 'default';
193
+ handlers.list(format);
194
+ });
195
+ }
196
+ /**
197
+ * Register environment commands on the main CLI program.
198
+ */
199
+ function registerEnvCommands(program, context) {
200
+ const handlers = new EnvCommands(context);
201
+ const env = program.command('env').description('Manage environment configuration');
202
+ env.command('update')
203
+ .argument('[branch-name]', 'Optional branch name (defaults to current branch)')
204
+ .description('Generate .envrc from generated.services.json')
205
+ .action(handlers.update.bind(handlers));
206
+ env.command('show')
207
+ .argument('[branch-name]', 'Optional branch name (defaults to current branch)')
208
+ .description('Display current .envrc and port allocation information')
209
+ .action(handlers.show.bind(handlers));
210
+ }
211
+ /**
212
+ * Register proxy commands on the main CLI program.
213
+ */
214
+ function registerProxyCommands(program, context) {
215
+ const handlers = new ProxyCommands(context);
216
+ program
217
+ .command('proxy')
218
+ .description('Start a reverse-proxy server routing branches via subdomains')
219
+ .option('--port <port>', 'Override proxy port')
220
+ .action(async (options) => {
221
+ await handlers.start(options);
222
+ });
223
+ }
224
+ /*
225
+ * Register completion commands with the CLI.
226
+ * This command works both with and without config.
227
+ */
228
+ function registerCompletionCommands(program, context) {
229
+ const handlers = context ? new CompletionCommands(context, program) : null;
230
+ const validShells = [ShellName.Bash, ShellName.Zsh, ShellName.Fish, ShellName.PowerShell, ShellName.Pwsh];
231
+ function isValidShellName(value) {
232
+ return validShells.includes(value);
233
+ }
234
+ program
235
+ .command('completion <shell>')
236
+ .description(`Generate shell completion script (${validShells.join(', ')})`)
237
+ .action(async (shell) => {
238
+ // Validate shell type
239
+ if (!isValidShellName(shell)) {
240
+ console.error(`Error: Unsupported shell type "${shell}". Supported shells: ${validShells.join(', ')}`);
241
+ process.exitCode = 1;
242
+ return;
243
+ }
244
+ // For completion command, we need config to get worktrees path
245
+ // If no config, we'll use a fallback implementation
246
+ if (!handlers) {
247
+ const cwd = process.cwd();
248
+ const fallbackConfigFile = plainToInstance(ForgeConfigFile, { repositories: ['dummy'] });
249
+ const fallbackContext = new ForgeContext(cwd, new ForgeConfig(cwd, fallbackConfigFile));
250
+ const fallbackHandlers = new CompletionCommands(fallbackContext, program);
251
+ await fallbackHandlers.generate(shell);
252
+ }
253
+ else {
254
+ await handlers.generate(shell);
255
+ }
256
+ });
257
+ }
258
+ /**
259
+ * Entry point for the forge CLI.
260
+ */
261
+ async function main() {
262
+ const program = new Command();
263
+ program.name('forge').description('Feat-Forge workflow CLI').version(packageJson.version);
264
+ // Init command doesn't need config
265
+ registerInitCommands(program);
266
+ // Completion command should work with or without config
267
+ // Register it early so it's available even without .feat-forge.json
268
+ let context;
269
+ // Load config for other commands
270
+ const isInitCommand = process.argv[2] === 'init';
271
+ const isCompletionCommand = process.argv[2] === 'completion';
272
+ if (!isInitCommand) {
273
+ try {
274
+ context = await loadForgeContext();
275
+ }
276
+ catch (error) {
277
+ // Config not found
278
+ if (isCompletionCommand) {
279
+ // Allow completion to work without config (using fallback)
280
+ registerCompletionCommands(program);
281
+ }
282
+ else {
283
+ throw error;
284
+ }
285
+ }
286
+ if (context) {
287
+ // Register commands that require config
288
+ registerBranchCommands(program, context);
289
+ registerFeatureCommands(program, context);
290
+ registerFixCommands(program, context);
291
+ registerReleaseCommands(program, context);
292
+ registerModeCommands(program, context);
293
+ registerAgentCommands(program, context);
294
+ registerMaintenanceCommands(program, context);
295
+ registerServicesCommands(program, context);
296
+ registerEnvCommands(program, context);
297
+ registerProxyCommands(program, context);
298
+ registerCompletionCommands(program, context);
299
+ }
300
+ }
301
+ await program.parseAsync(process.argv);
302
+ }
303
+ main().catch((err) => {
304
+ console.error(err);
305
+ process.exitCode = 1;
306
+ });
@@ -0,0 +1,16 @@
1
+ /**
2
+ * Base class for command handlers with required configuration.
3
+ */
4
+ export class AbstractCommands {
5
+ context;
6
+ constructor(context) {
7
+ this.context = context;
8
+ }
9
+ async verifyCleanBranch(branchContext) {
10
+ const dirtyRepos = await branchContext.getDirtyRepositories();
11
+ if (dirtyRepos.length > 0) {
12
+ const repoNames = dirtyRepos.map((repo) => repo.name).join(', ');
13
+ throw new Error(`Worktree is not clean in: ${repoNames}.\n` + `Please commit or stash your changes before proceeding.`);
14
+ }
15
+ }
16
+ }
@@ -0,0 +1,14 @@
1
+ import { BranchContext } from '../foundation/BranchContext.js';
2
+ import { AbstractCommands } from './AbstractCommands.js';
3
+ export class AgentCommands extends AbstractCommands {
4
+ // ============================================================================
5
+ // PUBLIC COMMAND METHODS
6
+ // ============================================================================
7
+ /**
8
+ * Refresh agent adapter files for the active branch using current mode.
9
+ */
10
+ async refresh() {
11
+ const branchContext = await BranchContext.findNearestBranchContext(this.context);
12
+ await branchContext.refreshAgentContextFiles();
13
+ }
14
+ }