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.
- package/LICENSE +661 -0
- package/README.md +350 -0
- package/dist/cli.js +306 -0
- package/dist/commands/AbstractCommands.js +16 -0
- package/dist/commands/AgentCommands.js +14 -0
- package/dist/commands/BranchCommands.js +400 -0
- package/dist/commands/CompletionCommands.js +702 -0
- package/dist/commands/EnvCommands.js +56 -0
- package/dist/commands/FeatureCommands.js +4 -0
- package/dist/commands/FixCommands.js +4 -0
- package/dist/commands/InitCommands.js +380 -0
- package/dist/commands/MaintenanceCommands.js +39 -0
- package/dist/commands/ModeCommands.js +15 -0
- package/dist/commands/ProxyCommands.js +14 -0
- package/dist/commands/ReleaseCommands.js +4 -0
- package/dist/commands/ServicesCommands.js +95 -0
- package/dist/commands/SubBranchCommands.js +49 -0
- package/dist/commands/types/InitOptions.js +1 -0
- package/dist/foundation/BranchContext.js +427 -0
- package/dist/foundation/ForgeConfig.js +264 -0
- package/dist/foundation/ForgeConfigFile.js +391 -0
- package/dist/foundation/ForgeContext.js +169 -0
- package/dist/foundation/NpmHelper.js +131 -0
- package/dist/foundation/PathHelper.js +56 -0
- package/dist/foundation/PortAllocator.js +192 -0
- package/dist/foundation/Proxy.js +176 -0
- package/dist/foundation/Repository.js +431 -0
- package/dist/foundation/errors/ForgeError.js +9 -0
- package/dist/foundation/errors/_error.config.js +12 -0
- package/dist/foundation/errors/generated/ForgeBadStateError.js +11 -0
- package/dist/foundation/errors/generated/ForgeConfigError.js +11 -0
- package/dist/foundation/errors/generated/ForgeExpectMainRepositoryError.js +11 -0
- package/dist/foundation/errors/generated/ForgeModeNotDefinedError.js +11 -0
- package/dist/foundation/errors/generated/ForgeNotInActiveBranchError.js +11 -0
- package/dist/foundation/errors/generated/ForgePortAllocationsLoadError.js +11 -0
- package/dist/foundation/errors/generated/ForgePortNotAssignedError.js +11 -0
- package/dist/foundation/errors/generated/ForgePortRangeExhaustedError.js +11 -0
- package/dist/foundation/errors/generated/ForgeServicesScanError.js +11 -0
- package/dist/foundation/errors/generated/ForgeServicesValidationError.js +11 -0
- package/dist/foundation/errors/index.js +13 -0
- package/dist/foundation/types/AIAgent.js +1 -0
- package/dist/foundation/types/AIAgentName.js +11 -0
- package/dist/foundation/types/DeepPartial.js +1 -0
- package/dist/foundation/types/IDE.js +1 -0
- package/dist/foundation/types/IDEName.js +7 -0
- package/dist/foundation/types/ModeConfig.js +1 -0
- package/dist/foundation/types/RepositoryInfos.js +1 -0
- package/dist/foundation/types/Services.js +156 -0
- package/dist/foundation/types/ShellName.js +11 -0
- package/dist/lib/agents.js +47 -0
- package/dist/lib/bootstrap.js +54 -0
- package/dist/lib/branch.js +4 -0
- package/dist/lib/config.js +65 -0
- package/dist/lib/constants.js +13 -0
- package/dist/lib/env.js +20 -0
- package/dist/lib/fs.js +156 -0
- package/dist/lib/git.js +170 -0
- package/dist/lib/hooks.js +98 -0
- package/dist/lib/ide.js +75 -0
- package/dist/lib/merger.js +103 -0
- package/dist/lib/platform.js +13 -0
- package/dist/lib/prompt.js +134 -0
- package/dist/lib/proxy-dashboard.js +75 -0
- package/dist/lib/scanner.js +118 -0
- package/dist/lib/services.js +132 -0
- package/dist/lib/slug.js +35 -0
- package/dist/lib/templates.js +115 -0
- package/dist/lib/validator.js +15 -0
- package/dist/templates/SPEC.md +21 -0
- package/dist/templates/TODO.md +5 -0
- package/dist/templates/agent/001.general.Omnibus.agent.md +4 -0
- package/dist/templates/agent/002.discovery.Inventorius.agent.md +4 -0
- package/dist/templates/agent/003.design.Architecturius.agent.md +8 -0
- package/dist/templates/agent/004.plan.Strategos.agent.md +8 -0
- package/dist/templates/agent/005.tdd.TestDrivenCodificius.agent.md +8 -0
- package/dist/templates/agent/006.code.Codificius.agent.md +8 -0
- package/dist/templates/agent/007.simplify.Consolidarius.agent.md +8 -0
- package/dist/templates/agent/008.review.Auditorix.agent.md +8 -0
- package/dist/templates/agent/009.testwriter.TestScriptor.agent.md +8 -0
- package/dist/templates/agent/010.testexecutor.TestExecutor.agent.md +8 -0
- package/dist/templates/agent/011.commit.Scribus.agent.md +10 -0
- package/dist/templates/agent/CONTEXT.code.md +145 -0
- package/dist/templates/agent/CONTEXT.spec.md +98 -0
- package/dist/templates/agent/Copilot/Code.agent.md +28 -0
- package/dist/templates/agent/Copilot/CodeCommit.agent.md +16 -0
- package/dist/templates/agent/Copilot/Feature-Builder.agent.md +49 -0
- package/dist/templates/agent/Copilot/Reviewer.agent.md +17 -0
- package/dist/templates/agent/Copilot/Simplifier.agent.md +21 -0
- package/dist/templates/agent/Copilot/Specs.agent.md +66 -0
- package/dist/templates/agent/Copilot/SpecsCommit.agent.md +19 -0
- package/dist/templates/agent/Copilot/TODO-Reader.agent.md +18 -0
- package/dist/templates/agent/Copilot/Tester.agent.md +12 -0
- 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
|
+
}
|