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
|
@@ -0,0 +1,391 @@
|
|
|
1
|
+
var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
|
|
2
|
+
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
|
|
3
|
+
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
|
|
4
|
+
else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
|
|
5
|
+
return c > 3 && r && Object.defineProperty(target, key, r), r;
|
|
6
|
+
};
|
|
7
|
+
var __metadata = (this && this.__metadata) || function (k, v) {
|
|
8
|
+
if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
|
|
9
|
+
};
|
|
10
|
+
import { Type } from 'class-transformer';
|
|
11
|
+
import { IsOptional, IsString, IsNotEmpty, IsArray, ValidateNested, ArrayNotEmpty, IsBoolean, IsInt, Min } from 'class-validator';
|
|
12
|
+
export class ForgeFoldersOptions {
|
|
13
|
+
/**
|
|
14
|
+
* Folder for feature specs within each repo.
|
|
15
|
+
*/
|
|
16
|
+
specs = '.specs';
|
|
17
|
+
/**
|
|
18
|
+
* Folder for git worktrees. Default: 'worktrees'
|
|
19
|
+
* Can be empty to put them in the root of the repo, but that can get messy so we default to a separate folder
|
|
20
|
+
*/
|
|
21
|
+
worktrees = 'worktrees';
|
|
22
|
+
/**
|
|
23
|
+
* Folder for active spec/branch tracking. Default: '.active-spec'
|
|
24
|
+
*/
|
|
25
|
+
activeSpec = '.active-spec';
|
|
26
|
+
/**
|
|
27
|
+
* Folder for spec files templates. Default: '.template'
|
|
28
|
+
*/
|
|
29
|
+
template = '.template';
|
|
30
|
+
/**
|
|
31
|
+
* Folder for repo's agents when agentsInEachRepo is active. Default: '.forge-agents'
|
|
32
|
+
*/
|
|
33
|
+
repoAgents = '.forge-agents';
|
|
34
|
+
/**
|
|
35
|
+
* Folder for archived feature specs within each repo. Default: '.archives'
|
|
36
|
+
*/
|
|
37
|
+
archive = '.archives';
|
|
38
|
+
/**
|
|
39
|
+
* Folder for repository configuration, bootstrap and hook scripts. Default: '.forge'
|
|
40
|
+
*/
|
|
41
|
+
repoConfig = '.forge';
|
|
42
|
+
}
|
|
43
|
+
__decorate([
|
|
44
|
+
IsOptional(),
|
|
45
|
+
IsString(),
|
|
46
|
+
IsNotEmpty(),
|
|
47
|
+
__metadata("design:type", String)
|
|
48
|
+
], ForgeFoldersOptions.prototype, "specs", void 0);
|
|
49
|
+
__decorate([
|
|
50
|
+
IsOptional(),
|
|
51
|
+
IsString(),
|
|
52
|
+
__metadata("design:type", String)
|
|
53
|
+
], ForgeFoldersOptions.prototype, "worktrees", void 0);
|
|
54
|
+
__decorate([
|
|
55
|
+
IsOptional(),
|
|
56
|
+
IsString(),
|
|
57
|
+
IsNotEmpty(),
|
|
58
|
+
__metadata("design:type", String)
|
|
59
|
+
], ForgeFoldersOptions.prototype, "activeSpec", void 0);
|
|
60
|
+
__decorate([
|
|
61
|
+
IsOptional(),
|
|
62
|
+
IsString(),
|
|
63
|
+
IsNotEmpty(),
|
|
64
|
+
__metadata("design:type", String)
|
|
65
|
+
], ForgeFoldersOptions.prototype, "template", void 0);
|
|
66
|
+
__decorate([
|
|
67
|
+
IsOptional(),
|
|
68
|
+
IsString(),
|
|
69
|
+
IsNotEmpty(),
|
|
70
|
+
__metadata("design:type", String)
|
|
71
|
+
], ForgeFoldersOptions.prototype, "repoAgents", void 0);
|
|
72
|
+
__decorate([
|
|
73
|
+
IsOptional(),
|
|
74
|
+
IsString(),
|
|
75
|
+
IsNotEmpty(),
|
|
76
|
+
__metadata("design:type", String)
|
|
77
|
+
], ForgeFoldersOptions.prototype, "archive", void 0);
|
|
78
|
+
__decorate([
|
|
79
|
+
IsOptional(),
|
|
80
|
+
IsString(),
|
|
81
|
+
IsNotEmpty(),
|
|
82
|
+
__metadata("design:type", String)
|
|
83
|
+
], ForgeFoldersOptions.prototype, "repoConfig", void 0);
|
|
84
|
+
export class ForgeFilesOptions {
|
|
85
|
+
/**
|
|
86
|
+
* File name for storing the current mode of a feature. Default: '.forge-mode'
|
|
87
|
+
*/
|
|
88
|
+
forgeMode = '.forge-mode';
|
|
89
|
+
}
|
|
90
|
+
__decorate([
|
|
91
|
+
IsOptional(),
|
|
92
|
+
IsString(),
|
|
93
|
+
IsNotEmpty(),
|
|
94
|
+
__metadata("design:type", String)
|
|
95
|
+
], ForgeFilesOptions.prototype, "forgeMode", void 0);
|
|
96
|
+
export class ForgeGitOptions {
|
|
97
|
+
/**
|
|
98
|
+
* Prefix for feature branches. Default: 'feature/'
|
|
99
|
+
*/
|
|
100
|
+
featureBranchPrefix = 'feature/';
|
|
101
|
+
/**
|
|
102
|
+
* Prefix for fix branches. Default: 'fix/'
|
|
103
|
+
*/
|
|
104
|
+
fixBranchPrefix = 'fix/';
|
|
105
|
+
/**
|
|
106
|
+
* Prefix for release branches. Default: 'release/'
|
|
107
|
+
*/
|
|
108
|
+
releaseBranchPrefix = 'release/';
|
|
109
|
+
/**
|
|
110
|
+
* These branches will have a special status and will be protected from deletion.
|
|
111
|
+
* Default: ['main', 'master', 'develop', 'dev']
|
|
112
|
+
*/
|
|
113
|
+
protectedBranches = ['main', 'master', 'develop', 'dev'];
|
|
114
|
+
}
|
|
115
|
+
__decorate([
|
|
116
|
+
IsOptional(),
|
|
117
|
+
IsString(),
|
|
118
|
+
__metadata("design:type", String)
|
|
119
|
+
], ForgeGitOptions.prototype, "featureBranchPrefix", void 0);
|
|
120
|
+
__decorate([
|
|
121
|
+
IsOptional(),
|
|
122
|
+
IsString(),
|
|
123
|
+
__metadata("design:type", String)
|
|
124
|
+
], ForgeGitOptions.prototype, "fixBranchPrefix", void 0);
|
|
125
|
+
__decorate([
|
|
126
|
+
IsOptional(),
|
|
127
|
+
IsString(),
|
|
128
|
+
__metadata("design:type", String)
|
|
129
|
+
], ForgeGitOptions.prototype, "releaseBranchPrefix", void 0);
|
|
130
|
+
__decorate([
|
|
131
|
+
IsOptional(),
|
|
132
|
+
IsArray(),
|
|
133
|
+
IsString({ each: true }),
|
|
134
|
+
__metadata("design:type", Array)
|
|
135
|
+
], ForgeGitOptions.prototype, "protectedBranches", void 0);
|
|
136
|
+
export class ForgeProcessOptions {
|
|
137
|
+
/**
|
|
138
|
+
* Do we ensure that .gitignore includes the .active-spec folder in all repos to avoid accidentally committing active branch pointers? Default: true
|
|
139
|
+
* You might want to set this to false if you want to commit the active spec pointers for some reason, but be careful not to accidentally commit them.
|
|
140
|
+
*/
|
|
141
|
+
manageGitIgnore = true;
|
|
142
|
+
/**
|
|
143
|
+
* Do we put an .active-spec folder in each repo on branch start.
|
|
144
|
+
*
|
|
145
|
+
* Typical use cases :
|
|
146
|
+
* - You want to open the repo itself in an IDE and have the active spec visible here
|
|
147
|
+
* - You want to execute Agents only in the context of the repo, while maintaining access to current spec
|
|
148
|
+
*
|
|
149
|
+
* Important : You probably want to set agentsInEachRepo to true if you set this to true.
|
|
150
|
+
*/
|
|
151
|
+
activeSpecInEachRepo = true;
|
|
152
|
+
/**
|
|
153
|
+
* Do we put agent instruction files in each repo on branch start, or only in the branch context?
|
|
154
|
+
*
|
|
155
|
+
* Typical use cases for putting them in each repo:
|
|
156
|
+
* - You want to open the repo itself in an IDE and have the agent instructions visible here
|
|
157
|
+
* - You want to execute Agents only in the context of the repo, while maintaining access to current instructions
|
|
158
|
+
*
|
|
159
|
+
* Important : You probably want to set activeSpecInEachRepo to true if you set this to true.
|
|
160
|
+
*/
|
|
161
|
+
agentsInEachRepo = true;
|
|
162
|
+
/**
|
|
163
|
+
* Prefix for npm scripts in package.json. Default: 'feat-forge'
|
|
164
|
+
* Example: with prefix 'feat-forge', scripts would be 'feat-forge:bootstrap', 'feat-forge:hooks:postBranchStart'
|
|
165
|
+
*/
|
|
166
|
+
npmScriptPrefix = 'feat-forge';
|
|
167
|
+
}
|
|
168
|
+
__decorate([
|
|
169
|
+
IsOptional(),
|
|
170
|
+
IsBoolean(),
|
|
171
|
+
__metadata("design:type", Boolean)
|
|
172
|
+
], ForgeProcessOptions.prototype, "manageGitIgnore", void 0);
|
|
173
|
+
__decorate([
|
|
174
|
+
IsOptional(),
|
|
175
|
+
IsBoolean(),
|
|
176
|
+
__metadata("design:type", Boolean)
|
|
177
|
+
], ForgeProcessOptions.prototype, "activeSpecInEachRepo", void 0);
|
|
178
|
+
__decorate([
|
|
179
|
+
IsOptional(),
|
|
180
|
+
IsBoolean(),
|
|
181
|
+
__metadata("design:type", Boolean)
|
|
182
|
+
], ForgeProcessOptions.prototype, "agentsInEachRepo", void 0);
|
|
183
|
+
__decorate([
|
|
184
|
+
IsOptional(),
|
|
185
|
+
IsString(),
|
|
186
|
+
IsNotEmpty(),
|
|
187
|
+
__metadata("design:type", String)
|
|
188
|
+
], ForgeProcessOptions.prototype, "npmScriptPrefix", void 0);
|
|
189
|
+
export class ForgeProxyOptions {
|
|
190
|
+
/**
|
|
191
|
+
* Whether the proxy is enabled or not. Default: true
|
|
192
|
+
*
|
|
193
|
+
* Active by default, but you can disable it if you don't need to use the proxy features (like URL rewriting in .envrc)
|
|
194
|
+
* and want to save system resources by not running the proxy server at all.
|
|
195
|
+
*/
|
|
196
|
+
enabled = true;
|
|
197
|
+
/**
|
|
198
|
+
* Base port for service port allocation. Default: 3000
|
|
199
|
+
*/
|
|
200
|
+
servicesBasePort = 3000;
|
|
201
|
+
/**
|
|
202
|
+
* Number of ports allocated per branch. Default: 100
|
|
203
|
+
*/
|
|
204
|
+
branchRangeSize = 100;
|
|
205
|
+
/**
|
|
206
|
+
* Port for the proxy server. Default: 8080
|
|
207
|
+
*/
|
|
208
|
+
port = 8080;
|
|
209
|
+
envrc = '.envrc';
|
|
210
|
+
/**
|
|
211
|
+
* Whether to put a source_up command in the .envrc file to load parent .envrc files
|
|
212
|
+
*/
|
|
213
|
+
envrc_source_up = true;
|
|
214
|
+
environmentVariablePrefix = 'FEAT_FORGE_';
|
|
215
|
+
}
|
|
216
|
+
__decorate([
|
|
217
|
+
IsOptional(),
|
|
218
|
+
IsBoolean(),
|
|
219
|
+
__metadata("design:type", Boolean)
|
|
220
|
+
], ForgeProxyOptions.prototype, "enabled", void 0);
|
|
221
|
+
__decorate([
|
|
222
|
+
IsOptional(),
|
|
223
|
+
IsInt(),
|
|
224
|
+
Min(1),
|
|
225
|
+
__metadata("design:type", Number)
|
|
226
|
+
], ForgeProxyOptions.prototype, "servicesBasePort", void 0);
|
|
227
|
+
__decorate([
|
|
228
|
+
IsOptional(),
|
|
229
|
+
IsInt(),
|
|
230
|
+
Min(1),
|
|
231
|
+
__metadata("design:type", Number)
|
|
232
|
+
], ForgeProxyOptions.prototype, "branchRangeSize", void 0);
|
|
233
|
+
__decorate([
|
|
234
|
+
IsOptional(),
|
|
235
|
+
IsInt(),
|
|
236
|
+
Min(1),
|
|
237
|
+
__metadata("design:type", Number)
|
|
238
|
+
], ForgeProxyOptions.prototype, "port", void 0);
|
|
239
|
+
__decorate([
|
|
240
|
+
IsOptional(),
|
|
241
|
+
IsString(),
|
|
242
|
+
IsNotEmpty(),
|
|
243
|
+
__metadata("design:type", String)
|
|
244
|
+
], ForgeProxyOptions.prototype, "envrc", void 0);
|
|
245
|
+
__decorate([
|
|
246
|
+
IsOptional(),
|
|
247
|
+
IsBoolean(),
|
|
248
|
+
__metadata("design:type", Boolean)
|
|
249
|
+
], ForgeProxyOptions.prototype, "envrc_source_up", void 0);
|
|
250
|
+
__decorate([
|
|
251
|
+
IsOptional(),
|
|
252
|
+
IsString(),
|
|
253
|
+
IsNotEmpty(),
|
|
254
|
+
__metadata("design:type", String)
|
|
255
|
+
], ForgeProxyOptions.prototype, "environmentVariablePrefix", void 0);
|
|
256
|
+
export class ForgeOptions {
|
|
257
|
+
process = new ForgeProcessOptions();
|
|
258
|
+
folders = new ForgeFoldersOptions();
|
|
259
|
+
files = new ForgeFilesOptions();
|
|
260
|
+
git = new ForgeGitOptions();
|
|
261
|
+
proxy = new ForgeProxyOptions();
|
|
262
|
+
}
|
|
263
|
+
__decorate([
|
|
264
|
+
IsOptional(),
|
|
265
|
+
ValidateNested(),
|
|
266
|
+
Type(() => ForgeProcessOptions),
|
|
267
|
+
__metadata("design:type", ForgeProcessOptions)
|
|
268
|
+
], ForgeOptions.prototype, "process", void 0);
|
|
269
|
+
__decorate([
|
|
270
|
+
IsOptional(),
|
|
271
|
+
ValidateNested(),
|
|
272
|
+
Type(() => ForgeFoldersOptions),
|
|
273
|
+
__metadata("design:type", ForgeFoldersOptions)
|
|
274
|
+
], ForgeOptions.prototype, "folders", void 0);
|
|
275
|
+
__decorate([
|
|
276
|
+
IsOptional(),
|
|
277
|
+
ValidateNested(),
|
|
278
|
+
Type(() => ForgeFilesOptions),
|
|
279
|
+
__metadata("design:type", ForgeFilesOptions)
|
|
280
|
+
], ForgeOptions.prototype, "files", void 0);
|
|
281
|
+
__decorate([
|
|
282
|
+
IsOptional(),
|
|
283
|
+
ValidateNested(),
|
|
284
|
+
Type(() => ForgeGitOptions),
|
|
285
|
+
__metadata("design:type", ForgeGitOptions)
|
|
286
|
+
], ForgeOptions.prototype, "git", void 0);
|
|
287
|
+
__decorate([
|
|
288
|
+
IsOptional(),
|
|
289
|
+
ValidateNested(),
|
|
290
|
+
Type(() => ForgeProxyOptions),
|
|
291
|
+
__metadata("design:type", ForgeProxyOptions)
|
|
292
|
+
], ForgeOptions.prototype, "proxy", void 0);
|
|
293
|
+
/**
|
|
294
|
+
* Type for the .feat-forge.json configuration file
|
|
295
|
+
*/
|
|
296
|
+
export class ForgeConfigFile {
|
|
297
|
+
/**
|
|
298
|
+
* Optional root directory for all repositories. If not set, repos paths are relative to the config file location. Can be absolute or relative.
|
|
299
|
+
* Useful for monorepos or when you want to keep the config file in a separate folder.
|
|
300
|
+
*/
|
|
301
|
+
rootDir;
|
|
302
|
+
/**
|
|
303
|
+
* List of repositories to manage. Can be a simple string (repo path) or an object with more options.
|
|
304
|
+
* If multiple repos are defined, one must be marked as "main" (or it will take the first one) -> this is the repo where the feature branches will be created and where the active feature will be tracked.
|
|
305
|
+
* If only one repo is defined, it will be considered the main repo by default.
|
|
306
|
+
*/
|
|
307
|
+
repositories;
|
|
308
|
+
/**
|
|
309
|
+
* List of spec files to create in each spec folder for a branch. Default: ['SPEC.md', 'TODO.md']
|
|
310
|
+
*
|
|
311
|
+
* Important: Template file must be named accordingly (e.g. .specs/.template/FOOBAR.md for FOOBAR.md) if you want to use custom templates for these files.
|
|
312
|
+
* Note: You can specificy any type of file here (txt, json, etc.)
|
|
313
|
+
*/
|
|
314
|
+
specFiles;
|
|
315
|
+
/**
|
|
316
|
+
* List of modes for the forge.
|
|
317
|
+
*
|
|
318
|
+
* Each mode can have a specific agent template file and description.
|
|
319
|
+
* The first one (or the one mode marked as default) will be the default mode for new branches.
|
|
320
|
+
*/
|
|
321
|
+
modes;
|
|
322
|
+
agents;
|
|
323
|
+
ides;
|
|
324
|
+
options;
|
|
325
|
+
}
|
|
326
|
+
__decorate([
|
|
327
|
+
IsOptional(),
|
|
328
|
+
IsString(),
|
|
329
|
+
__metadata("design:type", String)
|
|
330
|
+
], ForgeConfigFile.prototype, "rootDir", void 0);
|
|
331
|
+
__decorate([
|
|
332
|
+
IsArray(),
|
|
333
|
+
ArrayNotEmpty(),
|
|
334
|
+
__metadata("design:type", Array)
|
|
335
|
+
], ForgeConfigFile.prototype, "repositories", void 0);
|
|
336
|
+
__decorate([
|
|
337
|
+
IsOptional(),
|
|
338
|
+
IsArray(),
|
|
339
|
+
IsNotEmpty({ each: true }),
|
|
340
|
+
IsString({ each: true }),
|
|
341
|
+
__metadata("design:type", Array)
|
|
342
|
+
], ForgeConfigFile.prototype, "specFiles", void 0);
|
|
343
|
+
__decorate([
|
|
344
|
+
IsOptional(),
|
|
345
|
+
IsArray(),
|
|
346
|
+
Type(() => ModeConfigEntry),
|
|
347
|
+
__metadata("design:type", Array)
|
|
348
|
+
], ForgeConfigFile.prototype, "modes", void 0);
|
|
349
|
+
__decorate([
|
|
350
|
+
IsOptional(),
|
|
351
|
+
IsArray(),
|
|
352
|
+
__metadata("design:type", Array)
|
|
353
|
+
], ForgeConfigFile.prototype, "agents", void 0);
|
|
354
|
+
__decorate([
|
|
355
|
+
IsOptional(),
|
|
356
|
+
IsArray(),
|
|
357
|
+
__metadata("design:type", Array)
|
|
358
|
+
], ForgeConfigFile.prototype, "ides", void 0);
|
|
359
|
+
__decorate([
|
|
360
|
+
IsOptional(),
|
|
361
|
+
ValidateNested(),
|
|
362
|
+
Type(() => ForgeOptions),
|
|
363
|
+
__metadata("design:type", Object)
|
|
364
|
+
], ForgeConfigFile.prototype, "options", void 0);
|
|
365
|
+
export class ModeConfigEntry {
|
|
366
|
+
name;
|
|
367
|
+
description;
|
|
368
|
+
agentFile;
|
|
369
|
+
default = false;
|
|
370
|
+
}
|
|
371
|
+
__decorate([
|
|
372
|
+
IsOptional(),
|
|
373
|
+
IsNotEmpty(),
|
|
374
|
+
IsString(),
|
|
375
|
+
__metadata("design:type", String)
|
|
376
|
+
], ModeConfigEntry.prototype, "name", void 0);
|
|
377
|
+
__decorate([
|
|
378
|
+
IsOptional(),
|
|
379
|
+
IsString(),
|
|
380
|
+
__metadata("design:type", String)
|
|
381
|
+
], ModeConfigEntry.prototype, "description", void 0);
|
|
382
|
+
__decorate([
|
|
383
|
+
IsNotEmpty(),
|
|
384
|
+
IsString(),
|
|
385
|
+
__metadata("design:type", String)
|
|
386
|
+
], ModeConfigEntry.prototype, "agentFile", void 0);
|
|
387
|
+
__decorate([
|
|
388
|
+
IsOptional(),
|
|
389
|
+
IsBoolean(),
|
|
390
|
+
__metadata("design:type", Boolean)
|
|
391
|
+
], ModeConfigEntry.prototype, "default", void 0);
|
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
import { rm } from 'fs/promises';
|
|
2
|
+
import { copyFilesRecursively, ensureDir, pathExists } from '../lib/fs.js';
|
|
3
|
+
import { SOURCE_TEMPLATE_AGENT_PATH } from '../lib/templates.js';
|
|
4
|
+
import { BranchContext } from './BranchContext.js';
|
|
5
|
+
import { ForgeModeNotDefinedError } from './errors/index.js';
|
|
6
|
+
import { PathHelper } from './PathHelper.js';
|
|
7
|
+
import { RootRepository } from './Repository.js';
|
|
8
|
+
export class ForgeContext {
|
|
9
|
+
config;
|
|
10
|
+
rootDir;
|
|
11
|
+
repositories;
|
|
12
|
+
agents;
|
|
13
|
+
ides;
|
|
14
|
+
paths;
|
|
15
|
+
constructor(rootDir, config) {
|
|
16
|
+
this.rootDir = config.rootDir || rootDir;
|
|
17
|
+
this.config = config;
|
|
18
|
+
this.repositories = config.repositories.map((repoInfos) => new RootRepository(this, repoInfos));
|
|
19
|
+
this.agents = config.agents;
|
|
20
|
+
this.ides = config.ides;
|
|
21
|
+
this.paths = new PathHelper(this);
|
|
22
|
+
}
|
|
23
|
+
get options() {
|
|
24
|
+
return this.config.options;
|
|
25
|
+
}
|
|
26
|
+
get mainRepo() {
|
|
27
|
+
return this.repositories.find((repo) => repo.main);
|
|
28
|
+
}
|
|
29
|
+
get mainRepoName() {
|
|
30
|
+
return this.mainRepo.name;
|
|
31
|
+
}
|
|
32
|
+
get secondaryRepos() {
|
|
33
|
+
return this.repositories.filter((repo) => !repo.main);
|
|
34
|
+
}
|
|
35
|
+
getRepo(repoName) {
|
|
36
|
+
return this.repositories.find((r) => r.name === repoName);
|
|
37
|
+
}
|
|
38
|
+
makeBranchContext(branchName) {
|
|
39
|
+
const branchRootPath = this.paths.getBranchRootPath(branchName);
|
|
40
|
+
return new BranchContext(this, branchName, branchRootPath, [], false);
|
|
41
|
+
}
|
|
42
|
+
async loadBranchContext(branchName) {
|
|
43
|
+
const branchRootPath = this.paths.getBranchRootPath(branchName);
|
|
44
|
+
return BranchContext.loadFromPath(this, branchName, branchRootPath);
|
|
45
|
+
}
|
|
46
|
+
async loadActiveBranchesContexts(startDir = this.paths.worktreesRoot, prefix = []) {
|
|
47
|
+
if (!(await pathExists(startDir))) {
|
|
48
|
+
return [];
|
|
49
|
+
}
|
|
50
|
+
const branchContexts = [];
|
|
51
|
+
const mainRepoBranches = await this.mainRepo.getBranches();
|
|
52
|
+
// Test logic
|
|
53
|
+
await Promise.all(mainRepoBranches.map(async (branchName) => {
|
|
54
|
+
if (await this.branchRootExists(branchName)) {
|
|
55
|
+
branchContexts.push(await this.loadBranchContext(branchName));
|
|
56
|
+
}
|
|
57
|
+
}));
|
|
58
|
+
return branchContexts;
|
|
59
|
+
}
|
|
60
|
+
async isBranchActive(branchName) {
|
|
61
|
+
return this.branchRootExists(branchName);
|
|
62
|
+
}
|
|
63
|
+
async getBranchContext(branchName) {
|
|
64
|
+
if (!(await this.isBranchActive(branchName))) {
|
|
65
|
+
return this.makeBranchContext(branchName);
|
|
66
|
+
}
|
|
67
|
+
return this.loadBranchContext(branchName);
|
|
68
|
+
}
|
|
69
|
+
async branchRootExists(branchName) {
|
|
70
|
+
const branchRootPath = this.paths.getBranchRootPath(branchName);
|
|
71
|
+
return pathExists(branchRootPath);
|
|
72
|
+
}
|
|
73
|
+
getFeatureBranchName(featureSlug) {
|
|
74
|
+
return `${this.options.git.featureBranchPrefix}${featureSlug}`;
|
|
75
|
+
}
|
|
76
|
+
getFixBranchName(fixSlug) {
|
|
77
|
+
return `${this.options.git.fixBranchPrefix}${fixSlug}`;
|
|
78
|
+
}
|
|
79
|
+
getReleaseBranchName(releaseSlug) {
|
|
80
|
+
return `${this.options.git.releaseBranchPrefix}${releaseSlug}`;
|
|
81
|
+
}
|
|
82
|
+
async hasBranch(branchName) {
|
|
83
|
+
// only check on mainRepo
|
|
84
|
+
return this.mainRepo.hasBranch(branchName);
|
|
85
|
+
}
|
|
86
|
+
async hasFeatureBranch(featureSlug) {
|
|
87
|
+
// only check on mainRepo
|
|
88
|
+
const branchName = this.getFeatureBranchName(featureSlug);
|
|
89
|
+
return this.mainRepo.hasBranch(branchName);
|
|
90
|
+
}
|
|
91
|
+
async hasFixBranch(fixSlug) {
|
|
92
|
+
// only check on mainRepo
|
|
93
|
+
const branchName = this.getFixBranchName(fixSlug);
|
|
94
|
+
return this.mainRepo.hasBranch(branchName);
|
|
95
|
+
}
|
|
96
|
+
async hasReleaseBranch(releaseSlug) {
|
|
97
|
+
// only check on mainRepo
|
|
98
|
+
const branchName = this.getReleaseBranchName(releaseSlug);
|
|
99
|
+
return this.mainRepo.hasBranch(branchName);
|
|
100
|
+
}
|
|
101
|
+
async hasFeatureBranchOnAllRepositories(featureSlug) {
|
|
102
|
+
const branchName = this.getFeatureBranchName(featureSlug);
|
|
103
|
+
return Promise.all(this.repositories.map((repo) => repo.hasBranch(branchName))).then((results) => results.every(Boolean));
|
|
104
|
+
}
|
|
105
|
+
async hasFixBranchOnAllRepositories(fixSlug) {
|
|
106
|
+
const branchName = this.getFixBranchName(fixSlug);
|
|
107
|
+
return Promise.all(this.repositories.map((repo) => repo.hasBranch(branchName))).then((results) => results.every(Boolean));
|
|
108
|
+
}
|
|
109
|
+
async hasReleaseBranchOnAllRepositories(releaseSlug) {
|
|
110
|
+
const branchName = this.getReleaseBranchName(releaseSlug);
|
|
111
|
+
return Promise.all(this.repositories.map((repo) => repo.hasBranch(branchName))).then((results) => results.every(Boolean));
|
|
112
|
+
}
|
|
113
|
+
getDefaultMode() {
|
|
114
|
+
return this.config.modes.find((mode) => mode.default);
|
|
115
|
+
}
|
|
116
|
+
hasMode(modeName) {
|
|
117
|
+
return this.config.modes.findIndex((mode) => mode.name === modeName) >= 0;
|
|
118
|
+
}
|
|
119
|
+
modeExistsOrThrow(modeName) {
|
|
120
|
+
if (!this.hasMode(modeName)) {
|
|
121
|
+
throw new ForgeModeNotDefinedError(`Mode '${modeName}' not found in configuration. Please add it to your configuration file before using it.`);
|
|
122
|
+
}
|
|
123
|
+
return true;
|
|
124
|
+
}
|
|
125
|
+
getMode(modeName) {
|
|
126
|
+
this.modeExistsOrThrow(modeName);
|
|
127
|
+
return this.config.modes.find((mode) => mode.name === modeName);
|
|
128
|
+
}
|
|
129
|
+
/**
|
|
130
|
+
* Create agent template files in the main repository.
|
|
131
|
+
* They will be used as source templates for the agent context files that will be created on any branch.
|
|
132
|
+
*
|
|
133
|
+
* Typical use case :
|
|
134
|
+
* - You want to customize the default agent templates provided by FeatForge for all developers working on the project
|
|
135
|
+
* - You want to stabilize the agent templates used in the project (not using FeatForge internal version)
|
|
136
|
+
*
|
|
137
|
+
* Note : They can be superceded by user-defined templates if placed in the local project .feat-forge folder, so a developer can still customize them locally if needed without affecting the rest of the team.
|
|
138
|
+
*/
|
|
139
|
+
async installAgentTemplateLocally(targetRepository, options = {}) {
|
|
140
|
+
const { deleteExisting = false, dryRun = true, overwrite = false } = options;
|
|
141
|
+
const templateAgentDir = targetRepository.getAgentTemplatePath();
|
|
142
|
+
if (deleteExisting) {
|
|
143
|
+
if (!dryRun) {
|
|
144
|
+
await rm(templateAgentDir, { recursive: true, force: true });
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
await ensureDir(templateAgentDir);
|
|
148
|
+
// basically copy every files in the TEMPLATE_AGENT_PATH folder (with subdirectories) to templateAgentDir
|
|
149
|
+
const fileChanges = await copyFilesRecursively(SOURCE_TEMPLATE_AGENT_PATH, templateAgentDir, { overwrite, dryRun });
|
|
150
|
+
return fileChanges;
|
|
151
|
+
}
|
|
152
|
+
async ensureBranch(branchName, baseBranch) {
|
|
153
|
+
let totalChanges = 0;
|
|
154
|
+
for (const repo of this.repositories) {
|
|
155
|
+
totalChanges += await repo.createBranch(branchName, baseBranch);
|
|
156
|
+
}
|
|
157
|
+
return totalChanges;
|
|
158
|
+
}
|
|
159
|
+
/**
|
|
160
|
+
* Look for any worktree that matches the branch but whose path does not exist (orphaned worktree), and remove it.
|
|
161
|
+
*/
|
|
162
|
+
async cleanOrphanedWorktrees(branchName) {
|
|
163
|
+
// We look at all context repositories to find any worktree that matches the branch but
|
|
164
|
+
// whose path does not exist (orphaned worktree), and we remove it.
|
|
165
|
+
for (const repo of this.repositories) {
|
|
166
|
+
await repo.cleanOrphanedWorktree(branchName);
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
}
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
import { execa } from 'execa';
|
|
2
|
+
import { readFile } from 'fs/promises';
|
|
3
|
+
import path from 'path';
|
|
4
|
+
import { pathExists } from '../lib/fs.js';
|
|
5
|
+
import { paramsToEnv } from '../lib/env.js';
|
|
6
|
+
/**
|
|
7
|
+
* Helper class to manage npm scripts for forge repositories
|
|
8
|
+
* Provides methods for discovering and executing npm scripts with the configured prefix
|
|
9
|
+
*/
|
|
10
|
+
export class NpmHelper {
|
|
11
|
+
forgeContext;
|
|
12
|
+
repository;
|
|
13
|
+
packageManager;
|
|
14
|
+
constructor(forgeContext, repository) {
|
|
15
|
+
this.forgeContext = forgeContext;
|
|
16
|
+
this.repository = repository;
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* Initialize the package manager detection
|
|
20
|
+
* Must be called before executing any scripts
|
|
21
|
+
*/
|
|
22
|
+
async initialize() {
|
|
23
|
+
if (this.packageManager)
|
|
24
|
+
return;
|
|
25
|
+
if (await pathExists(path.join(this.repository.path, 'pnpm-lock.yaml'))) {
|
|
26
|
+
this.packageManager = 'pnpm';
|
|
27
|
+
}
|
|
28
|
+
else if (await pathExists(path.join(this.repository.path, 'yarn.lock'))) {
|
|
29
|
+
this.packageManager = 'yarn';
|
|
30
|
+
}
|
|
31
|
+
else {
|
|
32
|
+
this.packageManager = 'npm';
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* Read and parse package.json from the repository
|
|
37
|
+
*/
|
|
38
|
+
async readPackageJson() {
|
|
39
|
+
if (process.env.VITEST)
|
|
40
|
+
return null; // Skip reading package.json during tests to avoid filesystem dependencies
|
|
41
|
+
const packageJsonPath = path.join(this.repository.path, 'package.json');
|
|
42
|
+
if (!(await pathExists(packageJsonPath))) {
|
|
43
|
+
return null;
|
|
44
|
+
}
|
|
45
|
+
try {
|
|
46
|
+
const content = await readFile(packageJsonPath, 'utf-8');
|
|
47
|
+
return JSON.parse(content);
|
|
48
|
+
}
|
|
49
|
+
catch (error) {
|
|
50
|
+
console.error(`Failed to read package.json at ${packageJsonPath}:`, error);
|
|
51
|
+
return null;
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
/**
|
|
55
|
+
* Discover npm scripts matching the configured prefix
|
|
56
|
+
* Returns script names like 'feat-forge:bootstrap', 'feat-forge:hooks:postBranchStart', etc.
|
|
57
|
+
*/
|
|
58
|
+
async discoverNpmScripts(subPart, allowMany = false) {
|
|
59
|
+
const packageJson = await this.readPackageJson();
|
|
60
|
+
if (!packageJson || !packageJson.scripts) {
|
|
61
|
+
return [];
|
|
62
|
+
}
|
|
63
|
+
const prefix = this.forgeContext.options.process.npmScriptPrefix;
|
|
64
|
+
const search = `${prefix}:${subPart ? subPart : ''}`;
|
|
65
|
+
return Object.keys(packageJson.scripts)
|
|
66
|
+
.filter((scriptName) => scriptName === search || (allowMany && scriptName.startsWith(search + '_')))
|
|
67
|
+
.sort();
|
|
68
|
+
}
|
|
69
|
+
/**
|
|
70
|
+
* Execute an npm script in the repository
|
|
71
|
+
* Uses the package manager detected during initialization
|
|
72
|
+
*/
|
|
73
|
+
async executeNpmScript(scriptName, params) {
|
|
74
|
+
const packageJson = await this.readPackageJson();
|
|
75
|
+
if (!packageJson || !packageJson.scripts || !packageJson.scripts[scriptName]) {
|
|
76
|
+
return false;
|
|
77
|
+
}
|
|
78
|
+
console.log(`🔄 Executing npm script: ${scriptName}`);
|
|
79
|
+
await this.initialize();
|
|
80
|
+
try {
|
|
81
|
+
const hookEnv = paramsToEnv(params);
|
|
82
|
+
await execa(this.packageManager, ['run', scriptName], {
|
|
83
|
+
cwd: this.repository.path,
|
|
84
|
+
stdio: 'inherit',
|
|
85
|
+
env: {
|
|
86
|
+
...process.env,
|
|
87
|
+
...hookEnv,
|
|
88
|
+
},
|
|
89
|
+
});
|
|
90
|
+
console.log(`✅ ${this.packageManager} script ${scriptName} completed successfully`);
|
|
91
|
+
return true;
|
|
92
|
+
}
|
|
93
|
+
catch (error) {
|
|
94
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
95
|
+
console.error(`❌ ${this.packageManager} script ${scriptName} failed: ${message}`);
|
|
96
|
+
throw error;
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
async executeNpmScripts(scriptNames, params) {
|
|
100
|
+
if (scriptNames.length === 0) {
|
|
101
|
+
return [];
|
|
102
|
+
}
|
|
103
|
+
await this.initialize();
|
|
104
|
+
const executedScripts = [];
|
|
105
|
+
for (const scriptName of scriptNames) {
|
|
106
|
+
try {
|
|
107
|
+
await this.executeNpmScript(scriptName, params);
|
|
108
|
+
executedScripts.push(scriptName);
|
|
109
|
+
}
|
|
110
|
+
catch (error) {
|
|
111
|
+
throw new Error(`${this.packageManager} script ${scriptName} failed in ${this.repository.name}`);
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
return executedScripts;
|
|
115
|
+
}
|
|
116
|
+
/**
|
|
117
|
+
* Execute the npm bootstrap script if it exists
|
|
118
|
+
* Looks for a script named prefix:bootstrap (e.g., 'feat-forge:bootstrap')
|
|
119
|
+
*/
|
|
120
|
+
async executeNpmBootstrapScript(params) {
|
|
121
|
+
return this.executeNpmScripts(await this.discoverNpmScripts('bootstrap', false), params);
|
|
122
|
+
}
|
|
123
|
+
/**
|
|
124
|
+
* Execute npm scripts for a specific event
|
|
125
|
+
* Discovers and executes scripts matching the pattern prefix:hooks:eventType
|
|
126
|
+
* (e.g., 'feat-forge:hooks:postBranchStart')
|
|
127
|
+
*/
|
|
128
|
+
async executeNpmScriptsForEvent(eventType, params) {
|
|
129
|
+
return this.executeNpmScripts(await this.discoverNpmScripts('hooks:' + eventType, true), params);
|
|
130
|
+
}
|
|
131
|
+
}
|