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,431 @@
1
+ import { branchNameAsPath } from '../lib/branch.js';
2
+ import { executeBootstrapScript } from '../lib/bootstrap.js';
3
+ import { HookEvent, executeHooksForEvent } from '../lib/hooks.js';
4
+ import { NpmHelper } from '../foundation/NpmHelper.js';
5
+ import { DirtyAction, promptConfirm, promptDirtyActions } from '../lib/prompt.js';
6
+ import { copyFile, rm, symlink } from 'fs/promises';
7
+ import path from 'path';
8
+ import { ensureDir, pathExists } from '../lib/fs.js';
9
+ import { checkoutBranch, createBranch, getBranches, getCurrentBranch, getGitStatusPorcelain, getGitWorktrees, gitBranchExists, runGit, } from '../lib/git.js';
10
+ import { ForgeExpectMainRepositoryError } from './errors/index.js';
11
+ export class Repository {
12
+ name;
13
+ path;
14
+ main;
15
+ context;
16
+ constructor(context, repoInfos) {
17
+ this.context = context;
18
+ this.name = repoInfos.name;
19
+ this.path = path.resolve(this.context.rootDir, repoInfos.path);
20
+ this.main = repoInfos.main;
21
+ }
22
+ isMainRepository() {
23
+ return this.main;
24
+ }
25
+ mustBeMainRepository() {
26
+ if (!this.isMainRepository()) {
27
+ throw new ForgeExpectMainRepositoryError('This operation is only available for the main repository.');
28
+ }
29
+ }
30
+ get folders() {
31
+ return this.context.options.folders;
32
+ }
33
+ get files() {
34
+ return this.context.options.files;
35
+ }
36
+ get specsPath() {
37
+ return path.join(this.path, this.folders.specs);
38
+ }
39
+ get specsArchivePath() {
40
+ return path.join(this.specsPath, this.folders.archive);
41
+ }
42
+ get templatePath() {
43
+ return path.join(this.specsPath, this.folders.template);
44
+ }
45
+ get activeSpecPath() {
46
+ return path.join(this.path, this.folders.activeSpec);
47
+ }
48
+ get agentsPath() {
49
+ return path.join(this.path, this.folders.repoAgents);
50
+ }
51
+ get repoConfigPath() {
52
+ return path.join(this.path, this.folders.repoConfig);
53
+ }
54
+ getSpecPath(branchName, ...segments) {
55
+ return path.join(this.specsPath, branchNameAsPath(branchName), ...segments);
56
+ }
57
+ getAgentPath(...segments) {
58
+ return path.join(this.agentsPath, ...segments);
59
+ }
60
+ getTemplatePath(...segments) {
61
+ return path.join(this.templatePath, ...segments);
62
+ }
63
+ getAgentTemplatePath(...segments) {
64
+ return this.getTemplatePath(this.folders.repoAgents, ...segments);
65
+ }
66
+ getRepoConfigPath(...segments) {
67
+ return path.join(this.repoConfigPath, ...segments);
68
+ }
69
+ async hasBranch(branchName) {
70
+ return gitBranchExists(this.path, branchName);
71
+ }
72
+ async hasBranchWithPrefix(prefix) {
73
+ const branches = await getBranches(this.path, prefix);
74
+ return branches.length > 0;
75
+ }
76
+ async hasFeatureBranch(featureSlug) {
77
+ const featureBranchName = this.context.getFeatureBranchName(featureSlug);
78
+ return this.hasBranch(featureBranchName);
79
+ }
80
+ async hasFixBranch(fixSlug) {
81
+ const fixBranchName = this.context.getFixBranchName(fixSlug);
82
+ return this.hasBranch(fixBranchName);
83
+ }
84
+ async hasReleaseBranch(releaseSlug) {
85
+ const releaseBranchName = this.context.getReleaseBranchName(releaseSlug);
86
+ return this.hasBranch(releaseBranchName);
87
+ }
88
+ async createBranch(branchName, baseBranch) {
89
+ if (!(await this.hasBranch(branchName))) {
90
+ await createBranch(this.path, branchName, baseBranch);
91
+ return 1;
92
+ }
93
+ return 0;
94
+ }
95
+ async createFeatureBranch(featureSlug) {
96
+ const featureBranchName = this.context.getFeatureBranchName(featureSlug);
97
+ return this.createBranch(featureBranchName);
98
+ }
99
+ async createFixBranch(fixSlug) {
100
+ const fixBranchName = this.context.getFixBranchName(fixSlug);
101
+ return this.createBranch(fixBranchName);
102
+ }
103
+ async createReleaseBranch(releaseSlug) {
104
+ const releaseBranchName = this.context.getReleaseBranchName(releaseSlug);
105
+ return this.createBranch(releaseBranchName);
106
+ }
107
+ async getCurrentBranch() {
108
+ return getCurrentBranch(this.path);
109
+ }
110
+ async setBranch(branchName) {
111
+ await checkoutBranch(this.path, branchName);
112
+ }
113
+ async setFeatureBranch(featureSlug) {
114
+ const featureBranchName = this.context.getFeatureBranchName(featureSlug);
115
+ await this.setBranch(featureBranchName);
116
+ }
117
+ async setFixBranch(fixSlug) {
118
+ const fixBranchName = this.context.getFixBranchName(fixSlug);
119
+ await this.setBranch(fixBranchName);
120
+ }
121
+ async setReleaseBranch(releaseSlug) {
122
+ const releaseBranchName = this.context.getReleaseBranchName(releaseSlug);
123
+ await this.setBranch(releaseBranchName);
124
+ }
125
+ async deleteBranch(branchName) {
126
+ await runGit(this.path, ['branch', '-D', branchName]);
127
+ }
128
+ async getBranches() {
129
+ return getBranches(this.path);
130
+ }
131
+ async getGitStatus() {
132
+ return getGitStatusPorcelain(this.path);
133
+ }
134
+ async isDirty() {
135
+ const status = await this.getGitStatus();
136
+ return status.length > 0;
137
+ }
138
+ async getStatus(branchName) {
139
+ const branch = await this.getCurrentBranch();
140
+ const dirty = await this.isDirty();
141
+ const onExpectedBranch = branch === branchName;
142
+ return { branch, dirty, onExpectedBranch };
143
+ }
144
+ async commit(message, files = ['.']) {
145
+ await runGit(this.path, ['add', ...files]);
146
+ await runGit(this.path, ['commit', '-m', message]);
147
+ }
148
+ async promptDirtyActions() {
149
+ if (!(await this.isDirty())) {
150
+ return true;
151
+ }
152
+ console.log(`Repository ${this.name} at ${this.path} has uncommitted changes.`);
153
+ // Prompt user for action
154
+ const { action, commitMessage } = await promptDirtyActions();
155
+ switch (action) {
156
+ case DirtyAction.Commit:
157
+ await runGit(this.path, ['add', '-A']);
158
+ await runGit(this.path, ['commit', '-m', commitMessage]);
159
+ // Verify worktree is now clean
160
+ if (await this.isDirty()) {
161
+ throw new Error(`Worktree still dirty after commit: ${this.path}`);
162
+ }
163
+ return true;
164
+ case DirtyAction.Cancel:
165
+ return false;
166
+ case DirtyAction.Discard:
167
+ return await promptConfirm('This will discard local changes. Proceed?');
168
+ }
169
+ }
170
+ /**
171
+ * Execute hooks for a specific event type in this repository.
172
+ * Executes both npm scripts (feat-forge:hooks:eventType) and shell scripts (.forge/hooks/eventType.sh).
173
+ * Hooks are discovered and executed in alphabetical order for predictable and consistent execution.
174
+ *
175
+ * Supports:
176
+ * - npm scripts: feat-forge:hooks:postBranchStart, feat-forge:hooks:postBranchStart_01, etc.
177
+ * - shell scripts: .forge/hooks/postBranchStart.sh, postBranchStart_01.sh, etc.
178
+ *
179
+ * @param eventType - Type of event (HookEvent enum value)
180
+ * @param params - Optional parameters to pass to hooks as environment variables (FORGE_HOOK_PARAM_NAME)
181
+ * @returns Array of hook names that were executed (both npm and shell script names)
182
+ * @throws Error if any hook fails
183
+ */
184
+ async executeHook(eventType, params) {
185
+ const repoConfigFolderPath = this.folders.repoConfig;
186
+ const npmHelper = new NpmHelper(this.context, this);
187
+ const executedHooks = [];
188
+ // Execute npm scripts for this event
189
+ const npmScripts = await npmHelper.executeNpmScriptsForEvent(eventType, params);
190
+ executedHooks.push(...npmScripts);
191
+ // Execute shell scripts for this event
192
+ const shellScripts = await executeHooksForEvent(this.path, repoConfigFolderPath, eventType, params);
193
+ executedHooks.push(...shellScripts);
194
+ return executedHooks;
195
+ }
196
+ }
197
+ export class RootRepository extends Repository {
198
+ copyFiles;
199
+ constructor(context, repoInfos) {
200
+ super(context, repoInfos);
201
+ this.copyFiles = repoInfos.copyFiles ?? [];
202
+ }
203
+ async copyFilesToWorktree(worktree) {
204
+ for (const file of this.copyFiles) {
205
+ const src = path.join(this.path, file);
206
+ const dest = path.join(worktree.path, file);
207
+ if (!(await pathExists(src)))
208
+ continue;
209
+ if (await pathExists(dest))
210
+ continue;
211
+ await ensureDir(path.dirname(dest));
212
+ console.log(`Copying file from root repository to worktree: ${src} -> ${dest}`);
213
+ await copyFile(src, dest);
214
+ }
215
+ }
216
+ getWorktreePath(branchName, temporary) {
217
+ return temporary
218
+ ? this.getTempWorktreePath(branchName, temporary)
219
+ : this.context.paths.getPathInBranchRoot(branchName, this.name);
220
+ }
221
+ getTempWorktreePath(branchName, type) {
222
+ return this.context.paths.getTempWorktreePathForRepo(type, branchName, this.name);
223
+ }
224
+ async hasWorktree(branchName, temporary) {
225
+ const worktreePath = temporary ? this.getTempWorktreePath(branchName, temporary) : this.getWorktreePath(branchName);
226
+ return await pathExists(worktreePath);
227
+ }
228
+ async addWorktree(branchName, temporary) {
229
+ const worktreePath = temporary ? this.getTempWorktreePath(branchName, temporary) : this.getWorktreePath(branchName);
230
+ // Check if worktree unexpectedly exists
231
+ if (await pathExists(worktreePath)) {
232
+ throw new Error(`Worktree already exists at ${worktreePath}.\\n` +
233
+ `If you have manually deleted worktree folders, run 'forge feature stop ${branchName}' to clean up.`);
234
+ }
235
+ if (await this.hasBranch(branchName)) {
236
+ await runGit(this.path, ['worktree', 'add', worktreePath, branchName]);
237
+ }
238
+ else {
239
+ await runGit(this.path, ['worktree', 'add', '-b', branchName, worktreePath]);
240
+ }
241
+ return this.getWorktree(branchName, temporary);
242
+ }
243
+ async getTemporaryWorktree(branchName, type) {
244
+ if (!(await this.hasBranch(branchName))) {
245
+ throw new Error(`Feature branch ${branchName} does not exist in repository ${this.name}`);
246
+ }
247
+ return this.addWorktree(branchName, type);
248
+ }
249
+ async getWorktree(branchName, temporary) {
250
+ if (!(await this.hasWorktree(branchName, temporary))) {
251
+ throw new Error(`Worktree for feature ${branchName} does not exist in repository ${this.name}`);
252
+ }
253
+ return new WorktreeRepository(this.context, { name: this.name, path: this.getWorktreePath(branchName, temporary), main: this.main }, this, !!temporary);
254
+ }
255
+ async removeWorktree(worktreeRepository, options) {
256
+ const worktreePath = worktreeRepository.path;
257
+ const worktreePathExists = await pathExists(worktreePath);
258
+ if (worktreePathExists || options?.forceOnEmpty) {
259
+ await runGit(this.path, ['worktree', 'remove', '--force', worktreePath]);
260
+ if (worktreePathExists)
261
+ await rm(worktreePath, { recursive: true, force: true });
262
+ }
263
+ else {
264
+ console.log(`Worktree path does not exist, skipping removal: ${worktreePath}`);
265
+ }
266
+ }
267
+ async listGitWorktrees() {
268
+ const rawWorktrees = await getGitWorktrees(this.path);
269
+ const worktrees = [];
270
+ for (const wt of rawWorktrees) {
271
+ const isTemporary = wt.path.startsWith(this.context.paths.tempFolderRoot);
272
+ const wtRepo = new WorktreeRepository(this.context, { name: this.name, path: wt.path, main: this.main }, this, isTemporary);
273
+ worktrees.push(wtRepo);
274
+ }
275
+ return worktrees;
276
+ }
277
+ async cleanOrphanedWorktree(branchName) {
278
+ const repoAllWorktrees = await this.listGitWorktrees();
279
+ for (const wt of repoAllWorktrees) {
280
+ if (!(await pathExists(wt.path))) {
281
+ if ((await wt.getCurrentBranch()) === branchName) {
282
+ console.log(`Try cleaning up orphaned worktree at ${wt.path} for branch ${branchName}`);
283
+ try {
284
+ await this.removeWorktree(wt, { forceOnEmpty: true });
285
+ }
286
+ catch (error) {
287
+ console.warn(` Could not remove orphaned worktree: ${error}`);
288
+ }
289
+ }
290
+ else {
291
+ console.log(` Skipping orphaned worktree at ${wt.path} since it's not on the feature branch ${branchName}`);
292
+ }
293
+ }
294
+ }
295
+ }
296
+ async merge(sourceBranch, targetBranch) {
297
+ console.log(`\n=== Merging "${sourceBranch}" into "${targetBranch}" on repo "${this.name}" ===`);
298
+ try {
299
+ await this.executeHook(HookEvent.PRE_MERGE, { sourceBranch, targetBranch });
300
+ // check if target branch is opened as a worktree - if so we must use the worktreeRepository to merge
301
+ let mergeRepoPath = this.path;
302
+ let mergeRepo = this;
303
+ const repoAllWorktrees = await this.listGitWorktrees();
304
+ const targetWorktree = repoAllWorktrees.find((wt) => wt.path === this.getWorktreePath(targetBranch));
305
+ if (targetWorktree) {
306
+ console.log(`Target branch "${targetBranch}" is currently checked out in worktree at ${targetWorktree.path}. Merging there.`);
307
+ mergeRepoPath = targetWorktree.path;
308
+ mergeRepo = targetWorktree;
309
+ }
310
+ else {
311
+ if ((await this.getCurrentBranch()) !== targetBranch) {
312
+ // Checkout target branch
313
+ console.log(`RootRepo not on target branch. Checking out "${targetBranch}"...`);
314
+ await checkoutBranch(mergeRepoPath, targetBranch);
315
+ }
316
+ }
317
+ // Perform merge with --no-ff to preserve feature branch history
318
+ try {
319
+ console.log(`Merging "${sourceBranch}"...`);
320
+ await runGit(mergeRepoPath, ['merge', '--no-ff', sourceBranch]);
321
+ console.log(`✅ Merge successful for repo: ${this.name}`);
322
+ await mergeRepo.executeHook(HookEvent.POST_MERGE, { sourceBranch, targetBranch });
323
+ return { repo: this.name, success: true, hasConflicts: false };
324
+ }
325
+ catch (error) {
326
+ // Check if it's a merge conflict (detected by special status indicators)
327
+ const status = await getGitStatusPorcelain(mergeRepoPath);
328
+ if (status.includes('UU ') || status.includes('AA ') || status.includes('DD ')) {
329
+ console.log(`⚠️ Merge conflicts detected in ${this.name}`);
330
+ console.log(`Please resolve conflicts manually in: ${mergeRepoPath}`);
331
+ return { repo: this.name, success: false, hasConflicts: true };
332
+ }
333
+ else {
334
+ // Re-throw if it's not a merge conflict
335
+ throw error;
336
+ }
337
+ }
338
+ }
339
+ catch (error) {
340
+ console.error(`❌ Error merging ${this.name}:`, error);
341
+ return { repo: this.name, success: false, hasConflicts: false };
342
+ }
343
+ }
344
+ }
345
+ export class WorktreeRepository extends Repository {
346
+ rootRepository;
347
+ temporary;
348
+ constructor(context, repoInfos, rootRepository, temporary = false) {
349
+ super(context, repoInfos);
350
+ this.rootRepository = rootRepository;
351
+ this.temporary = temporary;
352
+ }
353
+ remove() {
354
+ return this.rootRepository.removeWorktree(this);
355
+ }
356
+ async setActiveSpec(branchContext) {
357
+ if (this.isMainRepository()) {
358
+ const specPath = this.getSpecPath(branchContext.branchName);
359
+ const mainActivePath = this.activeSpecPath;
360
+ await rm(mainActivePath, { force: true });
361
+ await symlink(path.relative(path.dirname(mainActivePath), specPath), mainActivePath);
362
+ }
363
+ else {
364
+ const mainActivePath = branchContext.mainRepo.activeSpecPath;
365
+ const secondaryActivePath = this.activeSpecPath;
366
+ await rm(secondaryActivePath, { force: true });
367
+ // Create relative path from secondary to main's .active-spec
368
+ await symlink(path.relative(path.dirname(secondaryActivePath), mainActivePath), secondaryActivePath);
369
+ }
370
+ await this.executeHook(HookEvent.POST_SET_ACTIVE_SPECS, { branch: branchContext.branchName });
371
+ }
372
+ /**
373
+ * Rebase the worktree's current branch (which should be the feature branch) onto the specified base branch.
374
+ */
375
+ async rebase(specsBranch, baseBranch) {
376
+ try {
377
+ await this.executeHook(HookEvent.PRE_REBASE, { branch: specsBranch, baseBranch });
378
+ if (await this.isDirty()) {
379
+ console.log(`⚠️ Worktree repository ${this.name} has uncommitted changes. Please commit or stash them before rebasing.`);
380
+ return { repo: this.name, success: false, hasConflicts: false };
381
+ }
382
+ // Make sure we're on the feature branch in the worktree
383
+ const currentBranch = await this.getCurrentBranch();
384
+ if (currentBranch !== specsBranch) {
385
+ console.log(`Checking out ${specsBranch}...`);
386
+ await checkoutBranch(this.path, specsBranch);
387
+ }
388
+ // Perform rebase
389
+ console.log(`Rebasing ${specsBranch} onto ${baseBranch}...`);
390
+ try {
391
+ await runGit(this.path, ['rebase', baseBranch]);
392
+ console.log(`✅ Rebase successful for ${this.name}`);
393
+ await this.executeHook(HookEvent.POST_REBASE, { branch: specsBranch, baseBranch });
394
+ return { repo: this.name, success: true, hasConflicts: false };
395
+ }
396
+ catch (error) {
397
+ // Check if it's a rebase conflict
398
+ const status = await this.getGitStatus();
399
+ if (status.includes('UU ') || status.includes('AA ') || status.includes('DD ')) {
400
+ console.log(`⚠️ Rebase conflicts detected in worktree repository: ${this.name}`);
401
+ console.log(`Please resolve conflicts manually in: ${this.path}`);
402
+ console.log(`After resolving, run: git rebase --continue`);
403
+ console.log(`To abort, run: git rebase --abort`);
404
+ return { repo: this.name, success: false, hasConflicts: true };
405
+ }
406
+ else {
407
+ // Re-throw if it's not a rebase conflict
408
+ throw error;
409
+ }
410
+ }
411
+ }
412
+ catch (error) {
413
+ console.error(`❌ Error rebasing ${this.name}:`, error);
414
+ return { repo: this.name, success: false, hasConflicts: false };
415
+ }
416
+ }
417
+ /**
418
+ * Execute the bootstrap script in this repository.
419
+ * Executes both npm script (feat-forge:bootstrap) and shell script (bootstrap.sh/.bat).
420
+ *
421
+ * @throws Error if any bootstrap script fails
422
+ */
423
+ async executeBootstrapScript() {
424
+ const repoConfigFolderPath = this.folders.repoConfig;
425
+ const npmHelper = new NpmHelper(this.context, this);
426
+ // Try npm bootstrap script first
427
+ await npmHelper.executeNpmBootstrapScript();
428
+ // Try shell bootstrap script after npm
429
+ await executeBootstrapScript(this.path, repoConfigFolderPath);
430
+ }
431
+ }
@@ -0,0 +1,9 @@
1
+ export class ForgeError extends Error {
2
+ static extend(error, message) {
3
+ const forgeError = new this(`${message ? message + '\n' : ''}${error.message}`);
4
+ if (error.stack) {
5
+ forgeError.stack = error.stack;
6
+ }
7
+ return forgeError;
8
+ }
9
+ }
@@ -0,0 +1,12 @@
1
+ export const FORGE_ERRORS = {
2
+ ForgeExpectMainRepositoryError: 'Expected to be used with the main repository, but was not.',
3
+ ForgeConfigError: 'There was an error with the Forge configuration.',
4
+ ForgeBadStateError: 'The system is in a bad state to perform the requested operation.',
5
+ ForgeModeNotDefinedError: 'The specified mode is not defined in the configuration.',
6
+ ForgePortAllocationsLoadError: 'Failed to load port allocations file.',
7
+ ForgePortRangeExhaustedError: 'Port range exhausted for this branch.',
8
+ ForgeServicesScanError: 'Failed to scan services from a repository.',
9
+ ForgeServicesValidationError: 'Service definition is invalid.',
10
+ ForgePortNotAssignedError: 'No port was assigned for a service.',
11
+ ForgeNotInActiveBranchError: 'No active branch found in this folder. Execute the command in an active branch context.',
12
+ };
@@ -0,0 +1,11 @@
1
+ // THIS FILE IS AUTO-GENERATED - DO NOT EDIT
2
+ // Run 'pnpm generate:errors' to regenerate
3
+ // Edit .errors.config.ts to add/remove errors
4
+ import { ForgeError } from '../ForgeError.js';
5
+ import { FORGE_ERRORS } from '../_error.config.js';
6
+ export class ForgeBadStateError extends ForgeError {
7
+ constructor(message) {
8
+ super(message || FORGE_ERRORS.ForgeBadStateError);
9
+ this.name = 'ForgeBadStateError';
10
+ }
11
+ }
@@ -0,0 +1,11 @@
1
+ // THIS FILE IS AUTO-GENERATED - DO NOT EDIT
2
+ // Run 'pnpm generate:errors' to regenerate
3
+ // Edit .errors.config.ts to add/remove errors
4
+ import { ForgeError } from '../ForgeError.js';
5
+ import { FORGE_ERRORS } from '../_error.config.js';
6
+ export class ForgeConfigError extends ForgeError {
7
+ constructor(message) {
8
+ super(message || FORGE_ERRORS.ForgeConfigError);
9
+ this.name = 'ForgeConfigError';
10
+ }
11
+ }
@@ -0,0 +1,11 @@
1
+ // THIS FILE IS AUTO-GENERATED - DO NOT EDIT
2
+ // Run 'pnpm generate:errors' to regenerate
3
+ // Edit .errors.config.ts to add/remove errors
4
+ import { ForgeError } from '../ForgeError.js';
5
+ import { FORGE_ERRORS } from '../_error.config.js';
6
+ export class ForgeExpectMainRepositoryError extends ForgeError {
7
+ constructor(message) {
8
+ super(message || FORGE_ERRORS.ForgeExpectMainRepositoryError);
9
+ this.name = 'ForgeExpectMainRepositoryError';
10
+ }
11
+ }
@@ -0,0 +1,11 @@
1
+ // THIS FILE IS AUTO-GENERATED - DO NOT EDIT
2
+ // Run 'pnpm generate:errors' to regenerate
3
+ // Edit .errors.config.ts to add/remove errors
4
+ import { ForgeError } from '../ForgeError.js';
5
+ import { FORGE_ERRORS } from '../_error.config.js';
6
+ export class ForgeModeNotDefinedError extends ForgeError {
7
+ constructor(message) {
8
+ super(message || FORGE_ERRORS.ForgeModeNotDefinedError);
9
+ this.name = 'ForgeModeNotDefinedError';
10
+ }
11
+ }
@@ -0,0 +1,11 @@
1
+ // THIS FILE IS AUTO-GENERATED - DO NOT EDIT
2
+ // Run 'pnpm generate:errors' to regenerate
3
+ // Edit .errors.config.ts to add/remove errors
4
+ import { ForgeError } from '../ForgeError.js';
5
+ import { FORGE_ERRORS } from '../_error.config.js';
6
+ export class ForgeNotInActiveBranchError extends ForgeError {
7
+ constructor(message) {
8
+ super(message || FORGE_ERRORS.ForgeNotInActiveBranchError);
9
+ this.name = 'ForgeNotInActiveBranchError';
10
+ }
11
+ }
@@ -0,0 +1,11 @@
1
+ // THIS FILE IS AUTO-GENERATED - DO NOT EDIT
2
+ // Run 'pnpm generate:errors' to regenerate
3
+ // Edit .errors.config.ts to add/remove errors
4
+ import { ForgeError } from '../ForgeError.js';
5
+ import { FORGE_ERRORS } from '../_error.config.js';
6
+ export class ForgePortAllocationsLoadError extends ForgeError {
7
+ constructor(message) {
8
+ super(message || FORGE_ERRORS.ForgePortAllocationsLoadError);
9
+ this.name = 'ForgePortAllocationsLoadError';
10
+ }
11
+ }
@@ -0,0 +1,11 @@
1
+ // THIS FILE IS AUTO-GENERATED - DO NOT EDIT
2
+ // Run 'pnpm generate:errors' to regenerate
3
+ // Edit .errors.config.ts to add/remove errors
4
+ import { ForgeError } from '../ForgeError.js';
5
+ import { FORGE_ERRORS } from '../_error.config.js';
6
+ export class ForgePortNotAssignedError extends ForgeError {
7
+ constructor(message) {
8
+ super(message || FORGE_ERRORS.ForgePortNotAssignedError);
9
+ this.name = 'ForgePortNotAssignedError';
10
+ }
11
+ }
@@ -0,0 +1,11 @@
1
+ // THIS FILE IS AUTO-GENERATED - DO NOT EDIT
2
+ // Run 'pnpm generate:errors' to regenerate
3
+ // Edit .errors.config.ts to add/remove errors
4
+ import { ForgeError } from '../ForgeError.js';
5
+ import { FORGE_ERRORS } from '../_error.config.js';
6
+ export class ForgePortRangeExhaustedError extends ForgeError {
7
+ constructor(message) {
8
+ super(message || FORGE_ERRORS.ForgePortRangeExhaustedError);
9
+ this.name = 'ForgePortRangeExhaustedError';
10
+ }
11
+ }
@@ -0,0 +1,11 @@
1
+ // THIS FILE IS AUTO-GENERATED - DO NOT EDIT
2
+ // Run 'pnpm generate:errors' to regenerate
3
+ // Edit .errors.config.ts to add/remove errors
4
+ import { ForgeError } from '../ForgeError.js';
5
+ import { FORGE_ERRORS } from '../_error.config.js';
6
+ export class ForgeServicesScanError extends ForgeError {
7
+ constructor(message) {
8
+ super(message || FORGE_ERRORS.ForgeServicesScanError);
9
+ this.name = 'ForgeServicesScanError';
10
+ }
11
+ }
@@ -0,0 +1,11 @@
1
+ // THIS FILE IS AUTO-GENERATED - DO NOT EDIT
2
+ // Run 'pnpm generate:errors' to regenerate
3
+ // Edit .errors.config.ts to add/remove errors
4
+ import { ForgeError } from '../ForgeError.js';
5
+ import { FORGE_ERRORS } from '../_error.config.js';
6
+ export class ForgeServicesValidationError extends ForgeError {
7
+ constructor(message) {
8
+ super(message || FORGE_ERRORS.ForgeServicesValidationError);
9
+ this.name = 'ForgeServicesValidationError';
10
+ }
11
+ }
@@ -0,0 +1,13 @@
1
+ // THIS FILE IS AUTO-GENERATED - DO NOT EDIT
2
+ // Run 'pnpm generate:errors' to regenerate
3
+ // Edit .errors.config.ts to add/remove errors
4
+ export { ForgeExpectMainRepositoryError } from './generated/ForgeExpectMainRepositoryError.js';
5
+ export { ForgeConfigError } from './generated/ForgeConfigError.js';
6
+ export { ForgeBadStateError } from './generated/ForgeBadStateError.js';
7
+ export { ForgeModeNotDefinedError } from './generated/ForgeModeNotDefinedError.js';
8
+ export { ForgePortAllocationsLoadError } from './generated/ForgePortAllocationsLoadError.js';
9
+ export { ForgePortRangeExhaustedError } from './generated/ForgePortRangeExhaustedError.js';
10
+ export { ForgeServicesScanError } from './generated/ForgeServicesScanError.js';
11
+ export { ForgeServicesValidationError } from './generated/ForgeServicesValidationError.js';
12
+ export { ForgePortNotAssignedError } from './generated/ForgePortNotAssignedError.js';
13
+ export { ForgeNotInActiveBranchError } from './generated/ForgeNotInActiveBranchError.js';
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,11 @@
1
+ /**
2
+ * Known agent types that have special behaviors
3
+ */
4
+ export var AIAgentName;
5
+ (function (AIAgentName) {
6
+ AIAgentName["CODEX"] = "Codex";
7
+ AIAgentName["CLAUDE"] = "Claude";
8
+ AIAgentName["COPILOT"] = "Copilot";
9
+ AIAgentName["CURSOR"] = "Cursor";
10
+ AIAgentName["GEMINI"] = "Gemini";
11
+ })(AIAgentName || (AIAgentName = {}));
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,7 @@
1
+ /**
2
+ * Known IDE types that we can configure automatically
3
+ */
4
+ export var IDEName;
5
+ (function (IDEName) {
6
+ IDEName["VSCODE"] = "VSCode";
7
+ })(IDEName || (IDEName = {}));
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1 @@
1
+ export {};