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,400 @@
1
+ import { getDefaultIDECommand, getWorkspaceFileName } from '../lib/ide.js';
2
+ import { execa } from 'execa';
3
+ import { BranchContext } from '../foundation/BranchContext.js';
4
+ import { pathExists } from '../lib/fs.js';
5
+ import { checkoutBranch, displayOperationSummary, gitBranchExists, GitOperation } from '../lib/git.js';
6
+ import { promptChoice, promptConfirm, promptForBranch } from '../lib/prompt.js';
7
+ import { confirmSlugOrThrow } from '../lib/slug.js';
8
+ import { AbstractCommands } from './AbstractCommands.js';
9
+ export class BranchCommands extends AbstractCommands {
10
+ // ============================================================================
11
+ // PUBLIC COMMAND METHODS
12
+ // ============================================================================
13
+ /**
14
+ * Create a new branch folder and initialize missing spec files.
15
+ * - Does not create worktrees or set active branch - use `start` for that.
16
+ * - But branch is ready for `start` after this command, since spec files are initialized
17
+ */
18
+ async create(rawSlug) {
19
+ const branchName = await confirmSlugOrThrow(rawSlug);
20
+ if (await this.context.isBranchActive(branchName)) {
21
+ console.log(`Branch "${branchName}" is already active. Use "forge start ${branchName}" to start working on it.`);
22
+ return;
23
+ }
24
+ if (await this.context.hasBranch(branchName)) {
25
+ console.log(`Branch "${branchName}" already exists in the repository. You can start it with "forge start ${branchName}".`);
26
+ return;
27
+ }
28
+ const baseBranch = await promptForBranch(this.context.mainRepo.path, 'Select base branch for the new branch (showing branches of main repo, the branch must exist in all repos):', this.context.options.git.featureBranchPrefix, true);
29
+ // Prepare branch: validate branchName, ensure branch and spec exist
30
+ await this.prepareBranch(branchName, baseBranch);
31
+ console.log(`Branch "${branchName}" created and spec files initialized. Use 'forge start ${branchName}' to start working on it using git worktrees.`);
32
+ const mergeBack = await promptConfirm(`Do you want to merge the new branch "${branchName}" to another branch now?\n
33
+ (if you want to keep track of its spec, but not start to work on it yet)`);
34
+ if (mergeBack) {
35
+ await this.merge(rawSlug);
36
+ }
37
+ }
38
+ /**
39
+ * Start the development of a branch by creating worktrees (if not already created) and setting the active branch pointer.
40
+ * - Does create worktrees and set active branch
41
+ * - But it doesn't merge spec files to main branch if they were not already created
42
+ * This is useful when you want to start working instantly on a Branch and merge it when finished.
43
+ */
44
+ async start(rawSlug) {
45
+ const branchName = await confirmSlugOrThrow(rawSlug);
46
+ let baseBranch = undefined;
47
+ if (!(await this.context.hasBranch(branchName))) {
48
+ baseBranch = await promptForBranch(this.context.mainRepo.path, 'Select base branch for the new branch (showing branches of main repo, the branch must exist in all repos):', this.context.options.git.featureBranchPrefix, true);
49
+ }
50
+ // Prepare branch: validate branchName, ensure branch and spec exist
51
+ await (await this.prepareBranch(branchName, baseBranch)).start();
52
+ // Propose to open the branch in the configured IDE
53
+ if (this.context.ides.length > 0) {
54
+ const shouldOpen = await promptConfirm(`Open branch "${branchName}" in ${this.context.ides[0].name}?`);
55
+ if (shouldOpen) {
56
+ await this.open(branchName);
57
+ }
58
+ }
59
+ }
60
+ /**
61
+ * List all branch worktrees with their git branches.
62
+ */
63
+ async list(prefix = '') {
64
+ let branchContexts = await this.context.loadActiveBranchesContexts();
65
+ if (prefix) {
66
+ branchContexts = branchContexts.filter((context) => context.branchName.startsWith(prefix));
67
+ }
68
+ // Display each branch with branch information
69
+ console.log(`Active branch${branchContexts.length > 1 ? 'es' : ''}${prefix ? ` with prefix "${prefix}"` : ''}: (${branchContexts.length} found)`);
70
+ for (const branchContext of branchContexts) {
71
+ // Collect branch information for this branch
72
+ const repositoriesStatus = await branchContext.collectRepositoriesStatus();
73
+ // Format branch information
74
+ const { info: branchInfo, onExpectedBranch, isInconsistent, } = this.formatBranchContextRepositoriesStatus(branchContext, repositoriesStatus);
75
+ // Use red color for inconsistent branches
76
+ // Use orange color for branches that are not on the expected branch, but consistent across repos (e.g. all on "main" instead of the branch branch)
77
+ // Use green color for branches that are on the expected branch
78
+ const RED = '\x1b[31m';
79
+ const ORANGE = '\x1b[33m';
80
+ const GREEN = '\x1b[32m';
81
+ const RESET = '\x1b[0m';
82
+ const COLOR = isInconsistent ? RED : !onExpectedBranch ? ORANGE : GREEN;
83
+ const output = ` - ${branchContext.branchName}${COLOR}${branchInfo}${RESET}`;
84
+ console.log(output);
85
+ }
86
+ }
87
+ /**
88
+ * Resync all repos in a branch worktree to the correct branch.
89
+ */
90
+ async resync(rawSlug) {
91
+ const branchName = await confirmSlugOrThrow(rawSlug);
92
+ const branchContext = await this.context.loadBranchContext(branchName);
93
+ console.log(`Resyncing worktrees to branch \"${branchName}\"...`);
94
+ let hasErrors = false;
95
+ // Resync each repository worktree
96
+ for (const repository of branchContext.repositories) {
97
+ const hadError = await this.resyncRepository(branchContext, repository);
98
+ if (hadError) {
99
+ hasErrors = true;
100
+ }
101
+ }
102
+ for (const rootRepository of this.context.repositories) {
103
+ if (!branchContext.repositories.some((r) => r.rootRepository.name === rootRepository.name)) {
104
+ console.log(` ⚠ ${rootRepository.name}: not part of branch context, trying to init it`);
105
+ const worktreeRepository = await branchContext.ensureWorktreeForRepo(rootRepository);
106
+ worktreeRepository.setActiveSpec(branchContext);
107
+ branchContext.repositories.push(worktreeRepository);
108
+ }
109
+ }
110
+ // Display summary
111
+ if (hasErrors) {
112
+ console.log('\\n⚠ Resync completed with errors.');
113
+ }
114
+ else {
115
+ console.log('\\n✓ All repos resynced successfully.');
116
+ }
117
+ }
118
+ /**
119
+ * Stop a branch by removing its worktrees and clearing active pointer.
120
+ */
121
+ async stop(rawSlug) {
122
+ const branchName = await confirmSlugOrThrow(rawSlug);
123
+ // Clean up any orphaned worktrees first
124
+ await this.context.cleanOrphanedWorktrees(branchName);
125
+ if (!(await this.context.isBranchActive(branchName))) {
126
+ console.log(`Branch "${branchName}" is not active. Nothing to stop.`);
127
+ return;
128
+ }
129
+ const branchContext = await this.context.loadBranchContext(branchName);
130
+ // Verify clean state before stopping
131
+ await branchContext.stop();
132
+ // Clean up all worktrees and branch directory
133
+ await branchContext.delete();
134
+ }
135
+ /**
136
+ * Archive a branch by moving it from .branchs/<branchName>/ to .branchs/.archives/<branchName>/
137
+ * and committing the change in the branch branch.
138
+ */
139
+ async archive(rawSlug) {
140
+ const branchName = await confirmSlugOrThrow(rawSlug);
141
+ let branchContext = await this.context.getBranchContext(branchName);
142
+ // First stop the branch to clean up worktrees and ensure there are no uncommitted changes, but don't delete the branch context yet
143
+ await branchContext.stop();
144
+ // Move the specs files to the archive folder and commit + merge
145
+ await branchContext.archive();
146
+ // Clean up all worktrees and branch directory
147
+ await branchContext.delete();
148
+ const confirmDeleteBranch = await promptConfirm(`Do you also want to delete the branch "${branchContext.branchName}" in all repos?\nThis cannot be undone, but you can always recreate the branch later if needed.`);
149
+ if (confirmDeleteBranch) {
150
+ await branchContext.deleteBranch();
151
+ console.log(`Branch "${branchContext.branchName}" deleted.`);
152
+ }
153
+ else {
154
+ console.log(`Branch "${branchContext.branchName}" not deleted. You can delete it manually later if you change your mind.`);
155
+ }
156
+ }
157
+ /**
158
+ * Open a branch in the configured IDE.
159
+ * If a workspace file exists, it will be opened; otherwise, the branch directory is opened.
160
+ *
161
+ * @param rawSlug - Optional branch slug. If not provided, finds the nearest branch context.
162
+ * @throws {Error} If no IDE is configured
163
+ */
164
+ async open(rawSlug) {
165
+ // 1. Resolve feature context
166
+ const branchContext = rawSlug
167
+ ? await this.context.loadBranchContext(await confirmSlugOrThrow(rawSlug))
168
+ : await BranchContext.findNearestBranchContext(this.context);
169
+ // 2. Check that at least one IDE is configured
170
+ if (!this.context.ides || this.context.ides.length === 0) {
171
+ throw new Error('No IDE configured. Add an IDE to your .feat-forge.json configuration.');
172
+ }
173
+ // 3. Use the first IDE from the configuration
174
+ const ide = this.context.ides[0];
175
+ // 4. Resolve the CLI command
176
+ const command = ide.openCommand || getDefaultIDECommand(ide.name);
177
+ // 5. Determine the target to open
178
+ const workspaceFile = branchContext.getInPath(await getWorkspaceFileName(branchContext.branchName, ide.name));
179
+ const workspaceExists = await pathExists(workspaceFile);
180
+ const target = workspaceExists ? workspaceFile : branchContext.path;
181
+ // 6. Log informative messages
182
+ if (!workspaceExists) {
183
+ console.log('Workspace file not found, opening branch directory');
184
+ }
185
+ console.log(`Opening "${branchContext.branchName}" in ${ide.name}...`);
186
+ // 7. Spawn the IDE process in detached mode
187
+ const subprocess = execa(command, [target], { detached: true, stdio: 'ignore', cleanup: false });
188
+ subprocess.unref();
189
+ // Prevent unhandled rejection if command is not found
190
+ subprocess.catch((error) => {
191
+ console.error(`Failed to open IDE: ${error.message}`);
192
+ process.exitCode = 1;
193
+ });
194
+ }
195
+ async merge(rawSlug) {
196
+ const branchName = await confirmSlugOrThrow(rawSlug);
197
+ let targetRepos;
198
+ // If the feature is active, we only merge the repos that are part of the feature context (in case some configured repos were not part of the feature when it was started)
199
+ // If the feature is not active, we merge all repos to ensure the feature branch is up to date everywhere.
200
+ const isFeatureActive = await this.context.isBranchActive(branchName);
201
+ if (isFeatureActive) {
202
+ const branchContext = await this.context.loadBranchContext(branchName);
203
+ // If the feature is active, we verify that all repos in the feature context are clean before merging
204
+ await this.verifyCleanBranch(branchContext);
205
+ targetRepos = branchContext.repositories.map((wtRepo) => wtRepo.rootRepository);
206
+ }
207
+ else {
208
+ targetRepos = this.context.repositories;
209
+ }
210
+ const targetBranch = await promptForBranch(this.context.mainRepo.path, 'Select target branch (showing branches of main repo, the branch must exist in all repos):', this.context.options.git.featureBranchPrefix, false);
211
+ console.log(`\nMerging : ${branchName}`);
212
+ console.log(`\n📍 Target branch: ${targetBranch}\n`);
213
+ let results = [];
214
+ // Sequential merge for more readable logging.
215
+ // Also merge commit editor will be opened sequentially, which is better UX than multiple merge commit editors opening at the same time if there are multiple repos to merge.
216
+ for (const repo of targetRepos) {
217
+ results.push(await repo.merge(branchName, targetBranch));
218
+ }
219
+ await this.displayMergeSummaryAndProposeAction(results, branchName);
220
+ }
221
+ async rebase(rawSlug) {
222
+ const branchName = await confirmSlugOrThrow(rawSlug);
223
+ let branchContext;
224
+ let targetRepos;
225
+ // If the feature is active, we only rebase the repos that are part of the feature context (in case some configured repos were not part of the feature when it was started)
226
+ // If the feature is not active, we rebase all repos to ensure the feature branch is up to date everywhere.
227
+ const isFeatureActive = await this.context.isBranchActive(branchName);
228
+ if (!isFeatureActive) {
229
+ throw new Error(`Branch "${branchName}" is not active. Rebase only possible on active branches. Do a "forge start ${branchName}" to start the branch first.`);
230
+ }
231
+ branchContext = await this.context.loadBranchContext(branchName);
232
+ // If the feature is active, we verify that all repos in the feature context are clean before rebasing
233
+ await this.verifyCleanBranch(branchContext);
234
+ targetRepos = branchContext.repositories;
235
+ const baseBranch = await promptForBranch(this.context.mainRepo.path, 'Select base branch (showing branches of main repo, the branch must exist in all repos):', this.context.options.git.featureBranchPrefix, true);
236
+ console.log(`\nRebasing branch: ${branchName}`);
237
+ console.log(`\n📍 Base branch: ${baseBranch}\n`);
238
+ let results = [];
239
+ // Sequential rebase for more readable logging.
240
+ // Also rebase commit editor will be opened sequentially, which is better UX than multiple rebase commit editors opening at the same time if there are multiple repos to rebase.
241
+ for (const repo of targetRepos) {
242
+ results.push(await repo.rebase(branchName, baseBranch));
243
+ }
244
+ await this.displayRebaseSummary(results, branchName);
245
+ }
246
+ // ============================================================================
247
+ // PRIVATE UTILITY METHODS
248
+ // ============================================================================
249
+ /**
250
+ * Prepare branch + spec initialization shared by create/start.
251
+ */
252
+ async prepareBranch(branchName, baseBranch) {
253
+ let invisibleRepoChanges = 0;
254
+ // Ensure branch exists in all repos (creates branch if missing, but does not check it out)
255
+ await this.context.ensureBranch(branchName, baseBranch); // doesn't realy count as changes
256
+ let branchContext;
257
+ if (await this.context.isBranchActive(branchName)) {
258
+ // The branch is already active, load the real context
259
+ branchContext = await this.context.loadBranchContext(branchName);
260
+ }
261
+ else {
262
+ // The branch is not active yet, create an empty context to prepare the branch (this will create the spec files in a temporary location and merge them to the main branch, but won't create the worktrees yet)
263
+ branchContext = this.context.makeBranchContext(branchName);
264
+ }
265
+ // Ensure branch spec files exist in the main repo branch, creating them in a temporary worktree if needed
266
+ invisibleRepoChanges += await branchContext.initBranch();
267
+ return branchContext;
268
+ }
269
+ /**
270
+ * Format branch information for display.
271
+ *
272
+ * Shows a single branch name if consistent across repos,
273
+ * or all repo-branch pairs if inconsistent (highlighted in red).
274
+ *
275
+ * @param branches - Map of repository names to branch names
276
+ * @returns Formatted branch info string and inconsistency flag
277
+ */
278
+ formatBranchContextRepositoriesStatus(branchContext, repositoriesStatus) {
279
+ const uniqueBranches = new Set(Object.values(repositoriesStatus).map((status) => status.branch));
280
+ if (uniqueBranches.size === 0) {
281
+ return { info: ' (no branch info)', isInconsistent: false, onExpectedBranch: false };
282
+ }
283
+ if (uniqueBranches.size === 1) {
284
+ // All repos on same branch
285
+ const currentBranch = Array.from(uniqueBranches)[0];
286
+ return {
287
+ info: ` (branch: ${currentBranch})`,
288
+ isInconsistent: false,
289
+ onExpectedBranch: currentBranch === branchContext.branchName,
290
+ };
291
+ }
292
+ // Different branches across repos - show all
293
+ const branchList = Object.entries(repositoriesStatus)
294
+ .map(([repo, status]) => `${repo}: ${status.branch}`)
295
+ .join(', ');
296
+ return { info: ` (${branchList})`, isInconsistent: true, onExpectedBranch: false };
297
+ }
298
+ /**
299
+ * Resync a single repository worktree to the expected branch.
300
+ *
301
+ * @param repoName - Name of the repository
302
+ * @param worktreePath - Path to the worktree
303
+ * @param expectedBranch - Expected branch name
304
+ * @returns true if an error occurred, false otherwise
305
+ */
306
+ async resyncRepository(branchContext, repository) {
307
+ // Check if worktree exists
308
+ if (!(await pathExists(repository.path))) {
309
+ console.log(` ⚠ ${repository.name}: worktree not found, skipping`);
310
+ return false;
311
+ }
312
+ const { branch, dirty, onExpectedBranch } = await repository.getStatus(branchContext.branchName);
313
+ const expectedBranch = branchContext.branchName;
314
+ // Check current branch
315
+ if (onExpectedBranch) {
316
+ console.log(` ✓ ${repository.name}: already on ${expectedBranch}`);
317
+ return false;
318
+ }
319
+ // Check for uncommitted changes
320
+ if (dirty) {
321
+ console.log(` ✗ ${repository.name}: has uncommitted changes, cannot switch branch`);
322
+ return true;
323
+ }
324
+ // Check if expected branch exists
325
+ if (!(await gitBranchExists(repository.path, expectedBranch))) {
326
+ // This should never happen ...
327
+ console.log(` ✗ ${repository.name}: branch ${expectedBranch} does not exist`);
328
+ return true;
329
+ }
330
+ // Attempt to checkout expected branch
331
+ try {
332
+ await checkoutBranch(repository.path, expectedBranch);
333
+ console.log(` ✓ ${repository.name}: switched from ${branch} to ${expectedBranch}`);
334
+ return false;
335
+ }
336
+ catch (error) {
337
+ console.log(` ✗ ${repository.name}: failed to checkout ${expectedBranch}`);
338
+ return true;
339
+ }
340
+ }
341
+ /**
342
+ * Display merge summary and propose next action to the user.
343
+ *
344
+ * Shows categorized results of all merge operations and, if all were successful,
345
+ * prompts the user to choose what to do next with the feature.
346
+ *
347
+ * @param results - Array of merge results
348
+ * @param branchName - The feature slug
349
+ */
350
+ async displayMergeSummaryAndProposeAction(results, branchName) {
351
+ const { successful } = displayOperationSummary(results, GitOperation.Merge);
352
+ // If all successful, propose next action
353
+ if (successful.length === results.length) {
354
+ await this.proposeNextAction(branchName);
355
+ }
356
+ }
357
+ /**
358
+ * Propose next action after successful merge and execute user's choice.
359
+ *
360
+ * Offers three options:
361
+ * 1. Archive the branch (recommended) - moves to .archives
362
+ * 2. Stop the branch - removes worktrees but keeps branches
363
+ * 3. Do nothing - keeps branch active
364
+ *
365
+ * @param branchName - The branch name
366
+ */
367
+ async proposeNextAction(branchName) {
368
+ const selection = await promptChoice('What would you like to do next?', [
369
+ { value: '1', name: 'Do nothing (keep branch active)' },
370
+ { value: '2', name: 'Stop branch (keep branches)' },
371
+ { value: '3', name: 'Archive branch (recommended)' },
372
+ ]);
373
+ switch (selection) {
374
+ case '3':
375
+ console.log(`\n📦 Archiving branch "${branchName}"...`);
376
+ await this.archive(branchName);
377
+ break;
378
+ case '2':
379
+ console.log(`\n⏸️ Stopping branch "${branchName}"...`);
380
+ await this.stop(branchName);
381
+ break;
382
+ case '1':
383
+ console.log('\n✅ Branch remains active. You can continue working on it.');
384
+ break;
385
+ default:
386
+ console.log('\n✅ No action taken.');
387
+ }
388
+ }
389
+ /**
390
+ * Display rebase summary to the user.
391
+ *
392
+ * Shows categorized results of all rebase operations.
393
+ *
394
+ * @param results - Array of rebase results
395
+ * @param slug - The feature slug
396
+ */
397
+ async displayRebaseSummary(results, slug) {
398
+ displayOperationSummary(results, GitOperation.Rebase);
399
+ }
400
+ }