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
@@ -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
+ }