@iloom/cli 0.2.0 → 0.3.0

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 (169) hide show
  1. package/README.md +274 -30
  2. package/dist/BranchNamingService-3OQPRSWT.js +13 -0
  3. package/dist/ClaudeContextManager-MUQSDY2E.js +13 -0
  4. package/dist/ClaudeService-HG4VQ7AW.js +12 -0
  5. package/dist/GitHubService-EBOETDIW.js +11 -0
  6. package/dist/{LoomLauncher-CTSWJL35.js → LoomLauncher-FLEMBCSQ.js} +63 -32
  7. package/dist/LoomLauncher-FLEMBCSQ.js.map +1 -0
  8. package/dist/ProjectCapabilityDetector-34LU7JJ4.js +9 -0
  9. package/dist/{PromptTemplateManager-WII75TKH.js → PromptTemplateManager-A52RUAMS.js} +2 -2
  10. package/dist/README.md +274 -30
  11. package/dist/{SettingsManager-XOYCLH3D.js → SettingsManager-WHHFGSL7.js} +12 -4
  12. package/dist/SettingsMigrationManager-AGIIIPDQ.js +10 -0
  13. package/dist/agents/iloom-issue-analyze-and-plan.md +125 -35
  14. package/dist/agents/iloom-issue-analyzer.md +284 -32
  15. package/dist/agents/iloom-issue-complexity-evaluator.md +40 -21
  16. package/dist/agents/iloom-issue-enhancer.md +69 -48
  17. package/dist/agents/iloom-issue-implementer.md +36 -25
  18. package/dist/agents/iloom-issue-planner.md +35 -24
  19. package/dist/agents/iloom-issue-reviewer.md +62 -9
  20. package/dist/chunk-3KATJIKO.js +55 -0
  21. package/dist/chunk-3KATJIKO.js.map +1 -0
  22. package/dist/{chunk-SWCRXDZC.js → chunk-3RUPPQRG.js} +1 -18
  23. package/dist/chunk-3RUPPQRG.js.map +1 -0
  24. package/dist/{chunk-RF2YI2XJ.js → chunk-47KSHUCR.js} +3 -3
  25. package/dist/chunk-47KSHUCR.js.map +1 -0
  26. package/dist/{chunk-VETG35MF.js → chunk-4HHRTA7Q.js} +3 -3
  27. package/dist/{chunk-VETG35MF.js.map → chunk-4HHRTA7Q.js.map} +1 -1
  28. package/dist/chunk-5EF7Z346.js +1987 -0
  29. package/dist/chunk-5EF7Z346.js.map +1 -0
  30. package/dist/{chunk-4IV6W4U5.js → chunk-AWOFAD5O.js} +12 -12
  31. package/dist/chunk-AWOFAD5O.js.map +1 -0
  32. package/dist/{chunk-2PLUQT6J.js → chunk-C5QCTEQK.js} +2 -2
  33. package/dist/{chunk-CWR2SANQ.js → chunk-EBISESAP.js} +1 -1
  34. package/dist/{chunk-LHP6ROUM.js → chunk-FIAT22G7.js} +4 -16
  35. package/dist/chunk-FIAT22G7.js.map +1 -0
  36. package/dist/{chunk-TS6DL67T.js → chunk-G2IEYOLQ.js} +11 -38
  37. package/dist/chunk-G2IEYOLQ.js.map +1 -0
  38. package/dist/{chunk-ZMNQBJUI.js → chunk-IP7SMKIF.js} +61 -22
  39. package/dist/chunk-IP7SMKIF.js.map +1 -0
  40. package/dist/{chunk-JNKJ7NJV.js → chunk-JKXJ7BGL.js} +6 -2
  41. package/dist/{chunk-JNKJ7NJV.js.map → chunk-JKXJ7BGL.js.map} +1 -1
  42. package/dist/{chunk-LAPY6NAE.js → chunk-JQFO7QQN.js} +68 -12
  43. package/dist/{chunk-LAPY6NAE.js.map → chunk-JQFO7QQN.js.map} +1 -1
  44. package/dist/{SettingsMigrationManager-MTQIMI54.js → chunk-KLBYVHPK.js} +3 -2
  45. package/dist/{chunk-HBVFXN7R.js → chunk-MAVL6PJF.js} +26 -3
  46. package/dist/chunk-MAVL6PJF.js.map +1 -0
  47. package/dist/{chunk-USVVV3FP.js → chunk-MKWYLDFK.js} +5 -5
  48. package/dist/chunk-ML3NRPNB.js +396 -0
  49. package/dist/chunk-ML3NRPNB.js.map +1 -0
  50. package/dist/{chunk-DJUGYNQE.js → chunk-PA6Q6AWM.js} +16 -3
  51. package/dist/chunk-PA6Q6AWM.js.map +1 -0
  52. package/dist/chunk-RO26VS3W.js +444 -0
  53. package/dist/chunk-RO26VS3W.js.map +1 -0
  54. package/dist/{chunk-6LEQW46Y.js → chunk-VAYCCUXW.js} +72 -2
  55. package/dist/{chunk-6LEQW46Y.js.map → chunk-VAYCCUXW.js.map} +1 -1
  56. package/dist/{chunk-SPYPLHMK.js → chunk-VU3QMIP2.js} +34 -2
  57. package/dist/chunk-VU3QMIP2.js.map +1 -0
  58. package/dist/{chunk-PVAVNJKS.js → chunk-WEN5C5DM.js} +10 -1
  59. package/dist/chunk-WEN5C5DM.js.map +1 -0
  60. package/dist/{chunk-MFU53H6J.js → chunk-XXV3UFZL.js} +3 -3
  61. package/dist/{chunk-MFU53H6J.js.map → chunk-XXV3UFZL.js.map} +1 -1
  62. package/dist/{chunk-GZP4UGGM.js → chunk-ZM3CFL5L.js} +2 -2
  63. package/dist/{chunk-BLCTGFZN.js → chunk-ZT3YZB4K.js} +3 -4
  64. package/dist/chunk-ZT3YZB4K.js.map +1 -0
  65. package/dist/{claude-ZIWDG4XG.js → claude-GOP6PFC7.js} +2 -2
  66. package/dist/{cleanup-FEIVZSIV.js → cleanup-7RWLBSLE.js} +86 -25
  67. package/dist/cleanup-7RWLBSLE.js.map +1 -0
  68. package/dist/cli.js +2511 -62
  69. package/dist/cli.js.map +1 -1
  70. package/dist/{contribute-EMZKCAC6.js → contribute-BS2L4FZR.js} +6 -6
  71. package/dist/{feedback-LFNMQBAZ.js → feedback-N4ECWIPF.js} +15 -14
  72. package/dist/{feedback-LFNMQBAZ.js.map → feedback-N4ECWIPF.js.map} +1 -1
  73. package/dist/{git-WC6HZLOT.js → git-TDXKRTXM.js} +4 -2
  74. package/dist/{ignite-MQWVJEAB.js → ignite-VM64QO3J.js} +32 -27
  75. package/dist/ignite-VM64QO3J.js.map +1 -0
  76. package/dist/index.d.ts +359 -45
  77. package/dist/index.js +1266 -502
  78. package/dist/index.js.map +1 -1
  79. package/dist/{init-GJDYN2IK.js → init-G3T64SC4.js} +104 -40
  80. package/dist/init-G3T64SC4.js.map +1 -0
  81. package/dist/mcp/issue-management-server.js +934 -0
  82. package/dist/mcp/issue-management-server.js.map +1 -0
  83. package/dist/{neon-helpers-ZVIRPKCI.js → neon-helpers-WPUACUVC.js} +3 -3
  84. package/dist/neon-helpers-WPUACUVC.js.map +1 -0
  85. package/dist/{open-NXSN7XOC.js → open-KXDXEKRZ.js} +39 -36
  86. package/dist/open-KXDXEKRZ.js.map +1 -0
  87. package/dist/{prompt-ANTQWHUF.js → prompt-7INJ7YRU.js} +4 -2
  88. package/dist/prompt-7INJ7YRU.js.map +1 -0
  89. package/dist/prompts/init-prompt.txt +538 -95
  90. package/dist/prompts/issue-prompt.txt +27 -27
  91. package/dist/{rebase-DUNFOJVS.js → rebase-Q7GMM7EI.js} +6 -6
  92. package/dist/{remote-ZCXJVVNW.js → remote-VUNCQZ6J.js} +3 -2
  93. package/dist/remote-VUNCQZ6J.js.map +1 -0
  94. package/dist/{run-O7ZK7CKA.js → run-PAWJJCSX.js} +39 -36
  95. package/dist/run-PAWJJCSX.js.map +1 -0
  96. package/dist/schema/settings.schema.json +56 -0
  97. package/dist/{test-git-T76HOTIA.js → test-git-3WDLNQCA.js} +3 -3
  98. package/dist/{test-prefix-6HJUVQMH.js → test-prefix-EVGAWAJW.js} +3 -3
  99. package/dist/{test-webserver-M2I3EV4J.js → test-webserver-DAHONWCS.js} +4 -4
  100. package/dist/test-webserver-DAHONWCS.js.map +1 -0
  101. package/package.json +2 -1
  102. package/dist/ClaudeContextManager-LVCYRM6Q.js +0 -13
  103. package/dist/ClaudeService-WVTWB3DK.js +0 -12
  104. package/dist/GitHubService-7E2S5NNZ.js +0 -11
  105. package/dist/LoomLauncher-CTSWJL35.js.map +0 -1
  106. package/dist/add-issue-OBI325W7.js +0 -69
  107. package/dist/add-issue-OBI325W7.js.map +0 -1
  108. package/dist/chunk-4IV6W4U5.js.map +0 -1
  109. package/dist/chunk-BLCTGFZN.js.map +0 -1
  110. package/dist/chunk-CVLAZRNB.js +0 -54
  111. package/dist/chunk-CVLAZRNB.js.map +0 -1
  112. package/dist/chunk-DJUGYNQE.js.map +0 -1
  113. package/dist/chunk-H4E4THUZ.js +0 -55
  114. package/dist/chunk-H4E4THUZ.js.map +0 -1
  115. package/dist/chunk-H5LDRGVK.js +0 -642
  116. package/dist/chunk-H5LDRGVK.js.map +0 -1
  117. package/dist/chunk-HBVFXN7R.js.map +0 -1
  118. package/dist/chunk-LHP6ROUM.js.map +0 -1
  119. package/dist/chunk-PVAVNJKS.js.map +0 -1
  120. package/dist/chunk-RF2YI2XJ.js.map +0 -1
  121. package/dist/chunk-SPYPLHMK.js.map +0 -1
  122. package/dist/chunk-SWCRXDZC.js.map +0 -1
  123. package/dist/chunk-SYOSCMIT.js +0 -545
  124. package/dist/chunk-SYOSCMIT.js.map +0 -1
  125. package/dist/chunk-T3KEIB4D.js +0 -243
  126. package/dist/chunk-T3KEIB4D.js.map +0 -1
  127. package/dist/chunk-TS6DL67T.js.map +0 -1
  128. package/dist/chunk-ZMNQBJUI.js.map +0 -1
  129. package/dist/cleanup-FEIVZSIV.js.map +0 -1
  130. package/dist/enhance-MNA4ZGXW.js +0 -176
  131. package/dist/enhance-MNA4ZGXW.js.map +0 -1
  132. package/dist/finish-TX5CJICB.js +0 -1749
  133. package/dist/finish-TX5CJICB.js.map +0 -1
  134. package/dist/ignite-MQWVJEAB.js.map +0 -1
  135. package/dist/init-GJDYN2IK.js.map +0 -1
  136. package/dist/mcp/chunk-6SDFJ42P.js +0 -62
  137. package/dist/mcp/chunk-6SDFJ42P.js.map +0 -1
  138. package/dist/mcp/claude-NDFOCQQQ.js +0 -249
  139. package/dist/mcp/claude-NDFOCQQQ.js.map +0 -1
  140. package/dist/mcp/color-QS5BFCNN.js +0 -168
  141. package/dist/mcp/color-QS5BFCNN.js.map +0 -1
  142. package/dist/mcp/github-comment-server.js +0 -168
  143. package/dist/mcp/github-comment-server.js.map +0 -1
  144. package/dist/mcp/terminal-OMNRFWB3.js +0 -227
  145. package/dist/mcp/terminal-OMNRFWB3.js.map +0 -1
  146. package/dist/open-NXSN7XOC.js.map +0 -1
  147. package/dist/run-O7ZK7CKA.js.map +0 -1
  148. package/dist/start-73I5W7WW.js +0 -983
  149. package/dist/start-73I5W7WW.js.map +0 -1
  150. package/dist/test-webserver-M2I3EV4J.js.map +0 -1
  151. /package/dist/{ClaudeContextManager-LVCYRM6Q.js.map → BranchNamingService-3OQPRSWT.js.map} +0 -0
  152. /package/dist/{ClaudeService-WVTWB3DK.js.map → ClaudeContextManager-MUQSDY2E.js.map} +0 -0
  153. /package/dist/{GitHubService-7E2S5NNZ.js.map → ClaudeService-HG4VQ7AW.js.map} +0 -0
  154. /package/dist/{PromptTemplateManager-WII75TKH.js.map → GitHubService-EBOETDIW.js.map} +0 -0
  155. /package/dist/{SettingsManager-XOYCLH3D.js.map → ProjectCapabilityDetector-34LU7JJ4.js.map} +0 -0
  156. /package/dist/{claude-ZIWDG4XG.js.map → PromptTemplateManager-A52RUAMS.js.map} +0 -0
  157. /package/dist/{git-WC6HZLOT.js.map → SettingsManager-WHHFGSL7.js.map} +0 -0
  158. /package/dist/{neon-helpers-ZVIRPKCI.js.map → SettingsMigrationManager-AGIIIPDQ.js.map} +0 -0
  159. /package/dist/{chunk-2PLUQT6J.js.map → chunk-C5QCTEQK.js.map} +0 -0
  160. /package/dist/{chunk-CWR2SANQ.js.map → chunk-EBISESAP.js.map} +0 -0
  161. /package/dist/{SettingsMigrationManager-MTQIMI54.js.map → chunk-KLBYVHPK.js.map} +0 -0
  162. /package/dist/{chunk-USVVV3FP.js.map → chunk-MKWYLDFK.js.map} +0 -0
  163. /package/dist/{chunk-GZP4UGGM.js.map → chunk-ZM3CFL5L.js.map} +0 -0
  164. /package/dist/{prompt-ANTQWHUF.js.map → claude-GOP6PFC7.js.map} +0 -0
  165. /package/dist/{contribute-EMZKCAC6.js.map → contribute-BS2L4FZR.js.map} +0 -0
  166. /package/dist/{remote-ZCXJVVNW.js.map → git-TDXKRTXM.js.map} +0 -0
  167. /package/dist/{rebase-DUNFOJVS.js.map → rebase-Q7GMM7EI.js.map} +0 -0
  168. /package/dist/{test-git-T76HOTIA.js.map → test-git-3WDLNQCA.js.map} +0 -0
  169. /package/dist/{test-prefix-6HJUVQMH.js.map → test-prefix-EVGAWAJW.js.map} +0 -0
@@ -1,1749 +0,0 @@
1
- #!/usr/bin/env node
2
- import {
3
- MergeManager
4
- } from "./chunk-4IV6W4U5.js";
5
- import {
6
- ResourceCleanup
7
- } from "./chunk-SYOSCMIT.js";
8
- import {
9
- IdentifierParser
10
- } from "./chunk-H4E4THUZ.js";
11
- import {
12
- ProcessManager
13
- } from "./chunk-SPYPLHMK.js";
14
- import {
15
- CLIIsolationManager,
16
- DatabaseManager,
17
- EnvironmentManager
18
- } from "./chunk-H5LDRGVK.js";
19
- import {
20
- loadEnvIntoProcess
21
- } from "./chunk-ZMNQBJUI.js";
22
- import {
23
- detectPackageManager,
24
- installDependencies,
25
- runScript
26
- } from "./chunk-BLCTGFZN.js";
27
- import {
28
- createNeonProviderFromSettings
29
- } from "./chunk-LAPY6NAE.js";
30
- import {
31
- ProjectCapabilityDetector
32
- } from "./chunk-CWR2SANQ.js";
33
- import {
34
- hasScript,
35
- readPackageJson
36
- } from "./chunk-2ZPFJQ3B.js";
37
- import {
38
- openBrowser
39
- } from "./chunk-YETJNRQM.js";
40
- import {
41
- getConfiguredRepoFromSettings,
42
- getEffectivePRTargetRemote,
43
- hasMultipleRemotes,
44
- parseGitRemotes
45
- } from "./chunk-DJUGYNQE.js";
46
- import {
47
- GitHubService
48
- } from "./chunk-TS6DL67T.js";
49
- import {
50
- executeGhCommand
51
- } from "./chunk-SWCRXDZC.js";
52
- import {
53
- detectClaudeCli,
54
- launchClaude
55
- } from "./chunk-MFU53H6J.js";
56
- import {
57
- GitWorktreeManager
58
- } from "./chunk-VETG35MF.js";
59
- import {
60
- SettingsManager
61
- } from "./chunk-T3KEIB4D.js";
62
- import {
63
- executeGitCommand,
64
- findMainWorktreePathWithSettings,
65
- pushBranchToRemote
66
- } from "./chunk-HBVFXN7R.js";
67
- import {
68
- promptConfirmation
69
- } from "./chunk-JNKJ7NJV.js";
70
- import {
71
- logger
72
- } from "./chunk-GEHQXLEI.js";
73
-
74
- // src/lib/ValidationRunner.ts
75
- var ValidationRunner = class {
76
- /**
77
- * Run all validations in sequence: typecheck → lint → test
78
- * Fails fast on first error
79
- */
80
- async runValidations(worktreePath, options = {}) {
81
- const startTime = Date.now();
82
- const steps = [];
83
- if (!options.skipTypecheck) {
84
- const typecheckResult = await this.runTypecheck(
85
- worktreePath,
86
- options.dryRun ?? false
87
- );
88
- steps.push(typecheckResult);
89
- if (!typecheckResult.passed && !typecheckResult.skipped) {
90
- return {
91
- success: false,
92
- steps,
93
- totalDuration: Date.now() - startTime
94
- };
95
- }
96
- }
97
- if (!options.skipLint) {
98
- const lintResult = await this.runLint(worktreePath, options.dryRun ?? false);
99
- steps.push(lintResult);
100
- if (!lintResult.passed && !lintResult.skipped) {
101
- return { success: false, steps, totalDuration: Date.now() - startTime };
102
- }
103
- }
104
- if (!options.skipTests) {
105
- const testResult = await this.runTests(
106
- worktreePath,
107
- options.dryRun ?? false
108
- );
109
- steps.push(testResult);
110
- if (!testResult.passed && !testResult.skipped) {
111
- return { success: false, steps, totalDuration: Date.now() - startTime };
112
- }
113
- }
114
- return { success: true, steps, totalDuration: Date.now() - startTime };
115
- }
116
- /**
117
- * Run typecheck validation
118
- */
119
- async runTypecheck(worktreePath, dryRun) {
120
- const stepStartTime = Date.now();
121
- try {
122
- const pkgJson = await readPackageJson(worktreePath);
123
- const hasTypecheckScript = hasScript(pkgJson, "typecheck");
124
- if (!hasTypecheckScript) {
125
- logger.debug("Skipping typecheck - no typecheck script found");
126
- return {
127
- step: "typecheck",
128
- passed: true,
129
- skipped: true,
130
- duration: Date.now() - stepStartTime
131
- };
132
- }
133
- } catch (error) {
134
- if (error instanceof Error && error.message.includes("package.json not found")) {
135
- logger.debug("Skipping typecheck - no package.json found (non-Node.js project)");
136
- return {
137
- step: "typecheck",
138
- passed: true,
139
- skipped: true,
140
- duration: Date.now() - stepStartTime
141
- };
142
- }
143
- throw error;
144
- }
145
- const packageManager = await detectPackageManager(worktreePath);
146
- if (dryRun) {
147
- const command = packageManager === "npm" ? "npm run typecheck" : `${packageManager} typecheck`;
148
- logger.info(`[DRY RUN] Would run: ${command}`);
149
- return {
150
- step: "typecheck",
151
- passed: true,
152
- skipped: false,
153
- duration: Date.now() - stepStartTime
154
- };
155
- }
156
- logger.info("Running typecheck...");
157
- try {
158
- await runScript("typecheck", worktreePath, [], { quiet: true });
159
- logger.success("Typecheck passed");
160
- return {
161
- step: "typecheck",
162
- passed: true,
163
- skipped: false,
164
- duration: Date.now() - stepStartTime
165
- };
166
- } catch {
167
- const fixed = await this.attemptClaudeFix(
168
- "typecheck",
169
- worktreePath,
170
- packageManager
171
- );
172
- if (fixed) {
173
- return {
174
- step: "typecheck",
175
- passed: true,
176
- skipped: false,
177
- duration: Date.now() - stepStartTime
178
- };
179
- }
180
- const runCommand = packageManager === "npm" ? "npm run typecheck" : `${packageManager} typecheck`;
181
- throw new Error(
182
- `Error: Typecheck failed.
183
- Fix type errors before merging.
184
-
185
- Run '${runCommand}' to see detailed errors.`
186
- );
187
- }
188
- }
189
- /**
190
- * Run lint validation
191
- */
192
- async runLint(worktreePath, dryRun) {
193
- const stepStartTime = Date.now();
194
- try {
195
- const pkgJson = await readPackageJson(worktreePath);
196
- const hasLintScript = hasScript(pkgJson, "lint");
197
- if (!hasLintScript) {
198
- logger.debug("Skipping lint - no lint script found");
199
- return {
200
- step: "lint",
201
- passed: true,
202
- skipped: true,
203
- duration: Date.now() - stepStartTime
204
- };
205
- }
206
- } catch (error) {
207
- if (error instanceof Error && error.message.includes("package.json not found")) {
208
- logger.debug("Skipping lint - no package.json found (non-Node.js project)");
209
- return {
210
- step: "lint",
211
- passed: true,
212
- skipped: true,
213
- duration: Date.now() - stepStartTime
214
- };
215
- }
216
- throw error;
217
- }
218
- const packageManager = await detectPackageManager(worktreePath);
219
- if (dryRun) {
220
- const command = packageManager === "npm" ? "npm run lint" : `${packageManager} lint`;
221
- logger.info(`[DRY RUN] Would run: ${command}`);
222
- return {
223
- step: "lint",
224
- passed: true,
225
- skipped: false,
226
- duration: Date.now() - stepStartTime
227
- };
228
- }
229
- logger.info("Running lint...");
230
- try {
231
- await runScript("lint", worktreePath, [], { quiet: true });
232
- logger.success("Linting passed");
233
- return {
234
- step: "lint",
235
- passed: true,
236
- skipped: false,
237
- duration: Date.now() - stepStartTime
238
- };
239
- } catch {
240
- const fixed = await this.attemptClaudeFix(
241
- "lint",
242
- worktreePath,
243
- packageManager
244
- );
245
- if (fixed) {
246
- return {
247
- step: "lint",
248
- passed: true,
249
- skipped: false,
250
- duration: Date.now() - stepStartTime
251
- };
252
- }
253
- const runCommand = packageManager === "npm" ? "npm run lint" : `${packageManager} lint`;
254
- throw new Error(
255
- `Error: Linting failed.
256
- Fix linting errors before merging.
257
-
258
- Run '${runCommand}' to see detailed errors.`
259
- );
260
- }
261
- }
262
- /**
263
- * Run test validation
264
- */
265
- async runTests(worktreePath, dryRun) {
266
- const stepStartTime = Date.now();
267
- try {
268
- const pkgJson = await readPackageJson(worktreePath);
269
- const hasTestScript = hasScript(pkgJson, "test");
270
- if (!hasTestScript) {
271
- logger.debug("Skipping tests - no test script found");
272
- return {
273
- step: "test",
274
- passed: true,
275
- skipped: true,
276
- duration: Date.now() - stepStartTime
277
- };
278
- }
279
- } catch (error) {
280
- if (error instanceof Error && error.message.includes("package.json not found")) {
281
- logger.debug("Skipping tests - no package.json found (non-Node.js project)");
282
- return {
283
- step: "test",
284
- passed: true,
285
- skipped: true,
286
- duration: Date.now() - stepStartTime
287
- };
288
- }
289
- throw error;
290
- }
291
- const packageManager = await detectPackageManager(worktreePath);
292
- if (dryRun) {
293
- const command = packageManager === "npm" ? "npm run test" : `${packageManager} test`;
294
- logger.info(`[DRY RUN] Would run: ${command}`);
295
- return {
296
- step: "test",
297
- passed: true,
298
- skipped: false,
299
- duration: Date.now() - stepStartTime
300
- };
301
- }
302
- logger.info("Running tests...");
303
- try {
304
- await runScript("test", worktreePath, [], { quiet: true });
305
- logger.success("Tests passed");
306
- return {
307
- step: "test",
308
- passed: true,
309
- skipped: false,
310
- duration: Date.now() - stepStartTime
311
- };
312
- } catch {
313
- const fixed = await this.attemptClaudeFix(
314
- "test",
315
- worktreePath,
316
- packageManager
317
- );
318
- if (fixed) {
319
- return {
320
- step: "test",
321
- passed: true,
322
- skipped: false,
323
- duration: Date.now() - stepStartTime
324
- };
325
- }
326
- const runCommand = packageManager === "npm" ? "npm run test" : `${packageManager} test`;
327
- throw new Error(
328
- `Error: Tests failed.
329
- Fix test failures before merging.
330
-
331
- Run '${runCommand}' to see detailed errors.`
332
- );
333
- }
334
- }
335
- /**
336
- * Attempt to fix validation errors using Claude
337
- * Pattern based on MergeManager.attemptClaudeConflictResolution
338
- *
339
- * @param validationType - Type of validation that failed ('typecheck' | 'lint' | 'test')
340
- * @param worktreePath - Path to the worktree
341
- * @param packageManager - Detected package manager
342
- * @returns true if Claude fixed the issue, false otherwise
343
- */
344
- async attemptClaudeFix(validationType, worktreePath, packageManager) {
345
- const isClaudeAvailable = await detectClaudeCli();
346
- if (!isClaudeAvailable) {
347
- logger.debug("Claude CLI not available, skipping auto-fix");
348
- return false;
349
- }
350
- const validationCommand = this.getValidationCommand(validationType, packageManager);
351
- const prompt = this.getClaudePrompt(validationType, validationCommand);
352
- const validationTypeCapitalized = validationType.charAt(0).toUpperCase() + validationType.slice(1);
353
- logger.info(`Launching Claude to help fix ${validationTypeCapitalized} errors...`);
354
- try {
355
- await launchClaude(prompt, {
356
- addDir: worktreePath,
357
- headless: false,
358
- // Interactive mode
359
- permissionMode: "acceptEdits",
360
- // Auto-accept edits
361
- model: "sonnet"
362
- // Use Sonnet model
363
- });
364
- logger.info(`Re-running ${validationTypeCapitalized} after Claude's fixes...`);
365
- try {
366
- await runScript(validationType, worktreePath, [], { quiet: true });
367
- logger.success(`${validationTypeCapitalized} passed after Claude auto-fix`);
368
- return true;
369
- } catch {
370
- logger.warn(`${validationTypeCapitalized} still failing after Claude's help`);
371
- return false;
372
- }
373
- } catch (error) {
374
- logger.warn("Claude auto-fix failed", {
375
- error: error instanceof Error ? error.message : String(error)
376
- });
377
- return false;
378
- }
379
- }
380
- /**
381
- * Get validation command string for prompts
382
- */
383
- getValidationCommand(validationType, packageManager) {
384
- if (packageManager === "npm") {
385
- return `npm run ${validationType}`;
386
- }
387
- return `${packageManager} ${validationType}`;
388
- }
389
- /**
390
- * Get Claude prompt for specific validation type
391
- * Matches bash script prompts exactly
392
- */
393
- getClaudePrompt(validationType, validationCommand) {
394
- switch (validationType) {
395
- case "typecheck":
396
- return `There are TypeScript errors in this codebase. Please analyze the typecheck output, identify all type errors, and fix them. Run '${validationCommand}' to see the errors, then make the necessary code changes to resolve all type issues. When you are done, tell the user to quit using /exit to continue the validation process.`;
397
- case "lint":
398
- return `There are ESLint errors in this codebase. Please analyze the linting output, identify all linting issues, and fix them. Run '${validationCommand}' to see the errors, then make the necessary code changes to resolve all linting issues. Focus on code quality, consistency, and following the project's linting rules. When you are done, tell the user to quit using /exit to continue the validation process.`;
399
- case "test":
400
- return `There are unit test failures in this codebase. Please analyze the test output to understand what's failing, then fix the issues. This might involve updating test code, fixing bugs in the source code, or updating tests to match new behavior. Run '${validationCommand}' to see the detailed test failures, then make the necessary changes to get all tests passing. When you are done, tell the user to quit using /exit to continue the validation process.`;
401
- }
402
- }
403
- };
404
-
405
- // src/lib/CommitManager.ts
406
- var CommitManager = class {
407
- /**
408
- * Detect uncommitted changes in a worktree
409
- * Parses git status --porcelain output into structured GitStatus
410
- */
411
- async detectUncommittedChanges(worktreePath) {
412
- const porcelainOutput = await executeGitCommand(["status", "--porcelain"], {
413
- cwd: worktreePath
414
- });
415
- const { stagedFiles, unstagedFiles } = this.parseGitStatus(porcelainOutput);
416
- const currentBranch = await executeGitCommand(["branch", "--show-current"], {
417
- cwd: worktreePath
418
- });
419
- return {
420
- hasUncommittedChanges: stagedFiles.length > 0 || unstagedFiles.length > 0,
421
- unstagedFiles,
422
- stagedFiles,
423
- currentBranch: currentBranch.trim(),
424
- // Defer these to future enhancement
425
- isAheadOfRemote: false,
426
- isBehindRemote: false
427
- };
428
- }
429
- /**
430
- * Stage all changes and commit with Claude-generated or simple message
431
- * Tries Claude first, falls back to simple message if Claude unavailable or fails
432
- */
433
- async commitChanges(worktreePath, options) {
434
- if (options.dryRun) {
435
- logger.info("[DRY RUN] Would run: git add -A");
436
- logger.info("[DRY RUN] Would generate commit message with Claude (if available)");
437
- const fallbackMessage = this.generateFallbackMessage(options);
438
- const verifyFlag = options.skipVerify ? " --no-verify" : "";
439
- logger.info(`[DRY RUN] Would commit with message${verifyFlag}: ${fallbackMessage}`);
440
- return;
441
- }
442
- await executeGitCommand(["add", "-A"], { cwd: worktreePath });
443
- let message = null;
444
- if (!options.message) {
445
- try {
446
- message = await this.generateClaudeCommitMessage(worktreePath, options.issueNumber);
447
- } catch (error) {
448
- logger.debug("Claude commit message generation failed, using fallback", { error });
449
- }
450
- }
451
- message ??= this.generateFallbackMessage(options);
452
- if (options.skipVerify) {
453
- logger.warn("\u26A0\uFE0F Skipping pre-commit hooks (--no-verify configured in settings)");
454
- }
455
- try {
456
- if (options.noReview || options.message) {
457
- const commitArgs = ["commit", "-m", message];
458
- if (options.skipVerify) {
459
- commitArgs.push("--no-verify");
460
- }
461
- await executeGitCommand(commitArgs, { cwd: worktreePath });
462
- } else {
463
- logger.info("Opening git editor for commit message review...");
464
- const commitArgs = ["commit", "-e", "-m", message];
465
- if (options.skipVerify) {
466
- commitArgs.push("--no-verify");
467
- }
468
- await executeGitCommand(commitArgs, {
469
- cwd: worktreePath,
470
- stdio: "inherit",
471
- timeout: 3e5
472
- // 5 minutes for interactive editing
473
- });
474
- }
475
- } catch (error) {
476
- if (error instanceof Error && error.message.includes("nothing to commit")) {
477
- logger.info("No changes to commit");
478
- return;
479
- }
480
- throw error;
481
- }
482
- }
483
- /**
484
- * Generate simple fallback commit message when Claude unavailable
485
- * Used as fallback for Claude-powered commit messages
486
- */
487
- generateFallbackMessage(options) {
488
- if (options.message) {
489
- return options.message;
490
- }
491
- if (options.issueNumber) {
492
- return `WIP: Auto-commit for issue #${options.issueNumber}
493
-
494
- Fixes #${options.issueNumber}`;
495
- } else {
496
- return "WIP: Auto-commit uncommitted changes";
497
- }
498
- }
499
- /**
500
- * Parse git status --porcelain output
501
- * Format: "XY filename" where X=index, Y=worktree
502
- * Examples:
503
- * "M file.ts" - staged modification
504
- * " M file.ts" - unstaged modification
505
- * "MM file.ts" - both staged and unstaged
506
- * "?? file.ts" - untracked
507
- */
508
- parseGitStatus(porcelainOutput) {
509
- const stagedFiles = [];
510
- const unstagedFiles = [];
511
- if (!porcelainOutput.trim()) {
512
- return { stagedFiles, unstagedFiles };
513
- }
514
- const lines = porcelainOutput.split("\n").filter((line) => line.trim());
515
- for (const line of lines) {
516
- if (line.length < 3) continue;
517
- const indexStatus = line[0];
518
- const worktreeStatus = line[1];
519
- const filename = line.substring(3);
520
- if (indexStatus !== " " && indexStatus !== "?") {
521
- stagedFiles.push(filename);
522
- }
523
- if (worktreeStatus !== " " || line.startsWith("??")) {
524
- unstagedFiles.push(filename);
525
- }
526
- }
527
- return { stagedFiles, unstagedFiles };
528
- }
529
- /**
530
- * Generate commit message using Claude AI
531
- * Claude examines the git repository directly via --add-dir option
532
- * Returns null if Claude unavailable or fails validation
533
- */
534
- async generateClaudeCommitMessage(worktreePath, issueNumber) {
535
- const startTime = Date.now();
536
- logger.info("Starting Claude commit message generation...", {
537
- worktreePath: worktreePath.split("/").pop(),
538
- // Just show the folder name for privacy
539
- issueNumber
540
- });
541
- logger.debug("Checking Claude CLI availability...");
542
- const isClaudeAvailable = await detectClaudeCli();
543
- if (!isClaudeAvailable) {
544
- logger.info("Claude CLI not available, skipping Claude commit message generation");
545
- return null;
546
- }
547
- logger.debug("Claude CLI is available");
548
- logger.debug("Building commit message prompt...");
549
- const prompt = this.buildCommitMessagePrompt(issueNumber);
550
- logger.debug("Prompt built", { promptLength: prompt.length });
551
- logger.debug("Claude prompt content:", {
552
- prompt,
553
- truncatedPreview: prompt.substring(0, 500) + (prompt.length > 500 ? "...[truncated]" : "")
554
- });
555
- try {
556
- logger.info("Calling Claude CLI for commit message generation...");
557
- const claudeStartTime = Date.now();
558
- const claudeOptions = {
559
- headless: true,
560
- addDir: worktreePath,
561
- model: "claude-haiku-4-5-20251001",
562
- // Fast, cost-effective model
563
- timeout: 12e4
564
- // 120 second timeout
565
- };
566
- logger.debug("Claude CLI call parameters:", {
567
- options: claudeOptions,
568
- worktreePathForAnalysis: worktreePath,
569
- addDirContents: "Will include entire worktree directory for analysis"
570
- });
571
- const result = await launchClaude(prompt, claudeOptions);
572
- const claudeDuration = Date.now() - claudeStartTime;
573
- logger.debug("Claude API call completed", { duration: `${claudeDuration}ms` });
574
- if (typeof result !== "string") {
575
- logger.warn("Claude returned non-string result", { resultType: typeof result });
576
- return null;
577
- }
578
- logger.debug("Raw Claude output received", {
579
- outputLength: result.length,
580
- preview: result.substring(0, 200) + (result.length > 200 ? "..." : "")
581
- });
582
- logger.debug("Sanitizing Claude output...");
583
- const sanitized = this.sanitizeClaudeOutput(result);
584
- logger.debug("Output sanitized", {
585
- originalLength: result.length,
586
- sanitizedLength: sanitized.length,
587
- sanitized: sanitized.substring(0, 200) + (sanitized.length > 200 ? "..." : "")
588
- });
589
- if (!sanitized) {
590
- logger.warn("Claude returned empty message after sanitization");
591
- return null;
592
- }
593
- let finalMessage = sanitized;
594
- if (issueNumber) {
595
- if (!finalMessage.includes(`Fixes #${issueNumber}`)) {
596
- finalMessage = `${finalMessage}
597
-
598
- Fixes #${issueNumber}`;
599
- logger.debug(`Added "Fixes #${issueNumber}" trailer to commit message`);
600
- } else {
601
- logger.debug(`"Fixes #${issueNumber}" already present in commit message`);
602
- }
603
- }
604
- const totalDuration = Date.now() - startTime;
605
- logger.info("Claude commit message generated successfully", {
606
- message: finalMessage,
607
- totalDuration: `${totalDuration}ms`,
608
- claudeApiDuration: `${claudeDuration}ms`
609
- });
610
- return finalMessage;
611
- } catch (error) {
612
- const totalDuration = Date.now() - startTime;
613
- const errorMessage = error instanceof Error ? error.message : "Unknown error";
614
- if (errorMessage.includes("timed out") || errorMessage.includes("timeout")) {
615
- logger.warn("Claude commit message generation timed out after 45 seconds", {
616
- totalDuration: `${totalDuration}ms`,
617
- worktreePath: worktreePath.split("/").pop()
618
- });
619
- } else {
620
- logger.warn("Failed to generate commit message with Claude", {
621
- error: errorMessage,
622
- totalDuration: `${totalDuration}ms`,
623
- worktreePath: worktreePath.split("/").pop()
624
- });
625
- }
626
- return null;
627
- }
628
- }
629
- /**
630
- * Build structured XML prompt for commit message generation
631
- * Uses XML format for clear task definition and output expectations
632
- */
633
- buildCommitMessagePrompt(issueNumber) {
634
- const issueContext = issueNumber ? `
635
- <IssueContext>
636
- This commit is associated with GitHub issue #${issueNumber}.
637
- If the changes appear to resolve the issue, include "Fixes #${issueNumber}" at the end of the first line of commit message.
638
- </IssueContext>` : "";
639
- return `<Task>
640
- You are a software engineer writing a commit message for this repository.
641
- Examine the staged changes in the git repository and generate a concise, meaningful commit message.
642
- </Task>
643
-
644
- <Requirements>
645
- <Format>The first line must be a brief summary of the changes made as a full sentence. If it references an issue, include "Fixes #N" at the end of this line.
646
-
647
- Add 2 newlines, then add a bullet-point form description of the changes made, each change on a new line.</Format>
648
- <Mood>Use imperative mood (e.g., "Add feature" not "Added feature")</Mood>
649
- <Focus>Be specific about what was changed and why</Focus>
650
- <Conciseness>Keep message under 72 characters for subject line when possible</Conciseness>
651
- <NoMeta>CRITICAL: Do NOT include ANY explanatory text, analysis, or meta-commentary. Output ONLY the raw commit message.</NoMeta>
652
- <Examples>
653
- Good: "Add user authentication with JWT tokens. Fixes #42
654
-
655
- - Implement login and registration endpoints
656
- - Secure routes with JWT middleware
657
- - Update user model to store hashed passwords"
658
- Good: "Fix navigation bug in sidebar menu."
659
- Bad: "Based on the changes, I'll create: Add user authentication"
660
- Bad: "Looking at the files, this commit should be: Fix navigation bug"
661
- </Examples>
662
- ${issueContext}
663
- </Requirements>
664
-
665
- <Output>
666
- IMPORTANT: Your entire response will be used directly as the git commit message.
667
- Do not include any explanatory text before or after the commit message.
668
- Start your response immediately with the commit message text.
669
- </Output>`;
670
- }
671
- /**
672
- * Sanitize Claude output to remove meta-commentary and clean formatting
673
- * Handles cases where Claude includes explanatory text despite instructions
674
- */
675
- sanitizeClaudeOutput(rawOutput) {
676
- let cleaned = rawOutput.trim();
677
- const metaPatterns = [
678
- /^.*?based on.*?changes.*?:/i,
679
- /^.*?looking at.*?files.*?:/i,
680
- /^.*?examining.*?:/i,
681
- /^.*?analyzing.*?:/i,
682
- /^.*?i'll.*?generate.*?:/i,
683
- /^.*?let me.*?:/i,
684
- /^.*?the commit message.*?should be.*?:/i,
685
- /^.*?here.*?is.*?commit.*?message.*?:/i
686
- ];
687
- for (const pattern of metaPatterns) {
688
- cleaned = cleaned.replace(pattern, "").trim();
689
- }
690
- if (cleaned.includes(":")) {
691
- const colonIndex = cleaned.indexOf(":");
692
- const beforeColon = cleaned.substring(0, colonIndex).trim().toLowerCase();
693
- const metaIndicators = [
694
- "here is the commit message",
695
- "commit message",
696
- "here is",
697
- "the message should be",
698
- "i suggest",
699
- "my suggestion"
700
- ];
701
- const isMetaCommentary = metaIndicators.some((indicator) => beforeColon.includes(indicator));
702
- if (isMetaCommentary) {
703
- const afterColon = cleaned.substring(colonIndex + 1).trim();
704
- if (afterColon && afterColon.length > 10) {
705
- cleaned = afterColon;
706
- }
707
- }
708
- }
709
- if (cleaned.startsWith('"') && cleaned.endsWith('"') || cleaned.startsWith("'") && cleaned.endsWith("'")) {
710
- cleaned = cleaned.slice(1, -1).trim();
711
- }
712
- return cleaned;
713
- }
714
- };
715
-
716
- // src/lib/BuildRunner.ts
717
- var BuildRunner = class {
718
- constructor(capabilityDetector) {
719
- this.capabilityDetector = capabilityDetector ?? new ProjectCapabilityDetector();
720
- }
721
- /**
722
- * Run build verification in the specified directory
723
- * @param buildPath - Path where build should run (typically main worktree path)
724
- * @param options - Build options
725
- */
726
- async runBuild(buildPath, options = {}) {
727
- const startTime = Date.now();
728
- try {
729
- const pkgJson = await readPackageJson(buildPath);
730
- const hasBuildScript = hasScript(pkgJson, "build");
731
- if (!hasBuildScript) {
732
- logger.debug("Skipping build - no build script found");
733
- return {
734
- success: true,
735
- skipped: true,
736
- reason: "No build script found in package.json",
737
- duration: Date.now() - startTime
738
- };
739
- }
740
- } catch (error) {
741
- if (error instanceof Error && error.message.includes("package.json not found")) {
742
- logger.debug("Skipping build - no package.json found (non-Node.js project)");
743
- return {
744
- success: true,
745
- skipped: true,
746
- reason: "No package.json found in project",
747
- duration: Date.now() - startTime
748
- };
749
- }
750
- throw error;
751
- }
752
- const capabilities = await this.capabilityDetector.detectCapabilities(buildPath);
753
- const isCLIProject = capabilities.capabilities.includes("cli");
754
- if (!isCLIProject) {
755
- logger.debug("Skipping build - not a CLI project (no bin field)");
756
- return {
757
- success: true,
758
- skipped: true,
759
- reason: "Project is not a CLI project (no bin field in package.json)",
760
- duration: Date.now() - startTime
761
- };
762
- }
763
- const packageManager = await detectPackageManager(buildPath);
764
- if (options.dryRun) {
765
- const command = packageManager === "npm" ? "npm run build" : `${packageManager} build`;
766
- logger.info(`[DRY RUN] Would run: ${command}`);
767
- return {
768
- success: true,
769
- skipped: false,
770
- duration: Date.now() - startTime
771
- };
772
- }
773
- logger.info("Running build...");
774
- try {
775
- await runScript("build", buildPath, [], { quiet: true });
776
- logger.success("Build completed successfully");
777
- return {
778
- success: true,
779
- skipped: false,
780
- duration: Date.now() - startTime
781
- };
782
- } catch {
783
- const runCommand = packageManager === "npm" ? "npm run build" : `${packageManager} build`;
784
- throw new Error(
785
- `Error: Build failed.
786
- Fix build errors before proceeding.
787
-
788
- Run '${runCommand}' to see detailed errors.`
789
- );
790
- }
791
- }
792
- };
793
-
794
- // src/lib/PRManager.ts
795
- var PRManager = class {
796
- constructor(settings) {
797
- this.settings = settings;
798
- }
799
- /**
800
- * Check if a PR already exists for the given branch
801
- * @param branchName - Branch to check
802
- * @param cwd - Working directory
803
- * @returns Existing PR info or null if none found
804
- */
805
- async checkForExistingPR(branchName, cwd) {
806
- try {
807
- const prList = await executeGhCommand(
808
- ["pr", "list", "--head", branchName, "--state", "open", "--json", "number,url"],
809
- cwd ? { cwd } : void 0
810
- );
811
- if (prList.length > 0) {
812
- return prList[0] ?? null;
813
- }
814
- return null;
815
- } catch (error) {
816
- logger.debug("Error checking for existing PR", { error });
817
- return null;
818
- }
819
- }
820
- /**
821
- * Generate PR body using Claude if available, otherwise use simple template
822
- * @param issueNumber - Issue number to include in body
823
- * @param worktreePath - Path to worktree for context
824
- * @returns PR body markdown
825
- */
826
- async generatePRBody(issueNumber, worktreePath) {
827
- const hasClaudeCli = await detectClaudeCli();
828
- if (hasClaudeCli) {
829
- try {
830
- const prompt = this.buildPRBodyPrompt(issueNumber);
831
- const body2 = await launchClaude(prompt, {
832
- headless: true,
833
- addDir: worktreePath,
834
- timeout: 3e4
835
- });
836
- if (body2 && typeof body2 === "string" && body2.trim()) {
837
- const sanitized = this.sanitizeClaudeOutput(body2);
838
- if (sanitized) {
839
- return sanitized;
840
- }
841
- }
842
- } catch (error) {
843
- logger.debug("Claude PR body generation failed, using template", { error });
844
- }
845
- }
846
- let body = "This PR contains changes from the iloom workflow.\n\n";
847
- if (issueNumber) {
848
- body += `Fixes #${issueNumber}`;
849
- }
850
- return body;
851
- }
852
- /**
853
- * Build structured XML prompt for PR body generation
854
- * Uses XML format for clear task definition and output expectations
855
- */
856
- buildPRBodyPrompt(issueNumber) {
857
- const issueContext = issueNumber ? `
858
- <IssueContext>
859
- This PR is associated with GitHub issue #${issueNumber}.
860
- Include "Fixes #${issueNumber}" at the end of the body on its own line.
861
- </IssueContext>` : "";
862
- return `<Task>
863
- You are a software engineer writing a pull request body for this repository.
864
- Examine the changes in the git repository and generate a concise, professional PR description.
865
- </Task>
866
-
867
- <Requirements>
868
- <Format>Write 2-3 sentences summarizing what was changed and why.${issueNumber ? `
869
-
870
- End with "Fixes #${issueNumber}" on its own line.` : ""}</Format>
871
- <Tone>Professional and concise</Tone>
872
- <Focus>Summarize the changes and their purpose</Focus>
873
- <NoMeta>CRITICAL: Do NOT include ANY explanatory text, analysis, or meta-commentary. Output ONLY the raw PR body text.</NoMeta>
874
- <Examples>
875
- Good: "Add user authentication with JWT tokens to secure the API endpoints. This includes login and registration endpoints with proper password hashing.
876
-
877
- Fixes #42"
878
- Good: "Fix navigation bug in sidebar menu that caused incorrect highlighting on nested routes."
879
- Bad: "Here's the PR body:
880
-
881
- ---
882
-
883
- Add user authentication..."
884
- Bad: "Based on the changes, I'll write: Fix navigation bug..."
885
- </Examples>
886
- ${issueContext}
887
- </Requirements>
888
-
889
- <Output>
890
- IMPORTANT: Your entire response will be used directly as the GitHub pull request body.
891
- Do not include any explanatory text, headers, or separators before or after the body.
892
- Start your response immediately with the PR body text.
893
- </Output>`;
894
- }
895
- /**
896
- * Sanitize Claude output to remove meta-commentary and clean formatting
897
- * Handles cases where Claude includes explanatory text despite instructions
898
- */
899
- sanitizeClaudeOutput(rawOutput) {
900
- let cleaned = rawOutput.trim();
901
- const metaPatterns = [
902
- /^.*?based on.*?changes.*?:/i,
903
- /^.*?looking at.*?files.*?:/i,
904
- /^.*?examining.*?:/i,
905
- /^.*?analyzing.*?:/i,
906
- /^.*?i'll.*?generate.*?:/i,
907
- /^.*?let me.*?:/i,
908
- /^.*?here.*?is.*?(?:the\s+)?(?:pr|pull request).*?body.*?:/i,
909
- /^.*?here's.*?(?:the\s+)?(?:pr|pull request).*?body.*?:/i
910
- ];
911
- for (const pattern of metaPatterns) {
912
- cleaned = cleaned.replace(pattern, "").trim();
913
- }
914
- cleaned = cleaned.replace(/^[-=]{3,}\s*/m, "").trim();
915
- if (cleaned.includes(":")) {
916
- const colonIndex = cleaned.indexOf(":");
917
- const beforeColon = cleaned.substring(0, colonIndex).trim().toLowerCase();
918
- const metaIndicators = [
919
- "here is the pr body",
920
- "here is the pull request body",
921
- "pr body",
922
- "pull request body",
923
- "here is",
924
- "here's",
925
- "the body should be",
926
- "i suggest",
927
- "my suggestion"
928
- ];
929
- const isMetaCommentary = metaIndicators.some((indicator) => beforeColon.includes(indicator));
930
- if (isMetaCommentary) {
931
- const afterColon = cleaned.substring(colonIndex + 1).trim();
932
- const afterSeparator = afterColon.replace(/^[-=]{3,}\s*/m, "").trim();
933
- if (afterSeparator && afterSeparator.length > 10) {
934
- cleaned = afterSeparator;
935
- }
936
- }
937
- }
938
- if (cleaned.startsWith('"') && cleaned.endsWith('"') || cleaned.startsWith("'") && cleaned.endsWith("'")) {
939
- cleaned = cleaned.slice(1, -1).trim();
940
- }
941
- return cleaned;
942
- }
943
- /**
944
- * Create a GitHub PR for the branch
945
- * @param branchName - Branch to create PR from (used as --head)
946
- * @param title - PR title
947
- * @param body - PR body
948
- * @param baseBranch - Base branch to target (usually main/master)
949
- * @param cwd - Working directory
950
- * @returns PR URL
951
- */
952
- async createPR(branchName, title, body, baseBranch, cwd) {
953
- try {
954
- const targetRemote = await getEffectivePRTargetRemote(this.settings, cwd);
955
- let headValue = branchName;
956
- if (targetRemote !== "origin") {
957
- const remotes = await parseGitRemotes(cwd);
958
- const originRemote = remotes.find((r) => r.name === "origin");
959
- if (originRemote) {
960
- headValue = `${originRemote.owner}:${branchName}`;
961
- logger.debug(`Fork workflow detected, using head: ${headValue}`);
962
- }
963
- }
964
- const args = ["pr", "create", "--head", headValue, "--title", title, "--body", body, "--base", baseBranch];
965
- if (targetRemote !== "origin") {
966
- const repo = await getConfiguredRepoFromSettings(this.settings, cwd);
967
- args.push("--repo", repo);
968
- }
969
- const result = await executeGhCommand(args, cwd ? { cwd } : void 0);
970
- const url = typeof result === "string" ? result.trim() : String(result).trim();
971
- if (!url.includes("github.com") || !url.includes("/pull/")) {
972
- throw new Error(`Unexpected response from gh pr create: ${url}`);
973
- }
974
- return url;
975
- } catch (error) {
976
- const errorMessage = error instanceof Error ? error.message : String(error);
977
- if (errorMessage.includes("Head sha can't be blank") || errorMessage.includes("No commits between")) {
978
- throw new Error(
979
- `Failed to create pull request: ${errorMessage}
980
-
981
- This error typically occurs when:
982
- - The branch was not fully pushed to the remote
983
- - There's a race condition between push and PR creation
984
- - The branch has no commits ahead of the base branch
985
-
986
- Try running: git push -u origin ${branchName}
987
- Then retry: il finish`
988
- );
989
- }
990
- throw new Error(`Failed to create pull request: ${errorMessage}`);
991
- }
992
- }
993
- /**
994
- * Open PR URL in browser
995
- * @param url - PR URL to open
996
- */
997
- async openPRInBrowser(url) {
998
- try {
999
- await openBrowser(url);
1000
- logger.debug("Opened PR in browser", { url });
1001
- } catch (error) {
1002
- logger.warn("Failed to open PR in browser", { error });
1003
- }
1004
- }
1005
- /**
1006
- * Complete PR workflow: check for existing, create if needed, optionally open in browser
1007
- * @param branchName - Branch to create PR from
1008
- * @param title - PR title
1009
- * @param issueNumber - Optional issue number for body generation
1010
- * @param baseBranch - Base branch to target
1011
- * @param worktreePath - Path to worktree
1012
- * @param openInBrowser - Whether to open PR in browser
1013
- * @returns PR creation result
1014
- */
1015
- async createOrOpenPR(branchName, title, issueNumber, baseBranch, worktreePath, openInBrowser) {
1016
- const existingPR = await this.checkForExistingPR(branchName, worktreePath);
1017
- if (existingPR) {
1018
- logger.info(`Pull request already exists: ${existingPR.url}`);
1019
- if (openInBrowser) {
1020
- await this.openPRInBrowser(existingPR.url);
1021
- }
1022
- return {
1023
- url: existingPR.url,
1024
- number: existingPR.number,
1025
- wasExisting: true
1026
- };
1027
- }
1028
- const body = await this.generatePRBody(issueNumber, worktreePath);
1029
- logger.info("Creating pull request...");
1030
- const url = await this.createPR(branchName, title, body, baseBranch, worktreePath);
1031
- const prNumber = this.extractPRNumberFromUrl(url);
1032
- if (openInBrowser) {
1033
- await this.openPRInBrowser(url);
1034
- }
1035
- return {
1036
- url,
1037
- number: prNumber,
1038
- wasExisting: false
1039
- };
1040
- }
1041
- /**
1042
- * Extract PR number from GitHub PR URL
1043
- * @param url - PR URL (e.g., https://github.com/owner/repo/pull/123)
1044
- * @returns PR number
1045
- */
1046
- extractPRNumberFromUrl(url) {
1047
- const match = url.match(/\/pull\/(\d+)/);
1048
- if (match == null ? void 0 : match[1]) {
1049
- return parseInt(match[1], 10);
1050
- }
1051
- throw new Error(`Could not extract PR number from URL: ${url}`);
1052
- }
1053
- };
1054
-
1055
- // src/commands/finish.ts
1056
- import path from "path";
1057
- var FinishCommand = class {
1058
- constructor(gitHubService, gitWorktreeManager, validationRunner, commitManager, mergeManager, identifierParser, resourceCleanup, buildRunner, settingsManager) {
1059
- const envResult = loadEnvIntoProcess();
1060
- if (envResult.error) {
1061
- logger.debug(`Environment loading warning: ${envResult.error.message}`);
1062
- }
1063
- if (envResult.parsed) {
1064
- logger.debug(`Loaded ${Object.keys(envResult.parsed).length} environment variables`);
1065
- }
1066
- this.gitHubService = gitHubService ?? new GitHubService();
1067
- this.gitWorktreeManager = gitWorktreeManager ?? new GitWorktreeManager();
1068
- this.validationRunner = validationRunner ?? new ValidationRunner();
1069
- this.commitManager = commitManager ?? new CommitManager();
1070
- this.mergeManager = mergeManager ?? new MergeManager();
1071
- this.identifierParser = identifierParser ?? new IdentifierParser(this.gitWorktreeManager);
1072
- this.settingsManager = settingsManager ?? new SettingsManager();
1073
- if (resourceCleanup) {
1074
- this.resourceCleanup = resourceCleanup;
1075
- }
1076
- this.buildRunner = buildRunner ?? new BuildRunner();
1077
- }
1078
- /**
1079
- * Lazy initialization of ResourceCleanup with properly configured DatabaseManager
1080
- */
1081
- async ensureResourceCleanup() {
1082
- var _a, _b;
1083
- if (this.resourceCleanup) {
1084
- return;
1085
- }
1086
- const settings = await this.settingsManager.loadSettings();
1087
- const databaseUrlEnvVarName = ((_b = (_a = settings.capabilities) == null ? void 0 : _a.database) == null ? void 0 : _b.databaseUrlEnvVarName) ?? "DATABASE_URL";
1088
- const environmentManager = new EnvironmentManager();
1089
- const neonProvider = createNeonProviderFromSettings(settings);
1090
- const databaseManager = new DatabaseManager(neonProvider, environmentManager, databaseUrlEnvVarName);
1091
- const cliIsolationManager = new CLIIsolationManager();
1092
- this.resourceCleanup = new ResourceCleanup(
1093
- this.gitWorktreeManager,
1094
- new ProcessManager(),
1095
- databaseManager,
1096
- cliIsolationManager
1097
- );
1098
- }
1099
- /**
1100
- * Main entry point for finish command
1101
- */
1102
- async execute(input) {
1103
- try {
1104
- const settings = await this.settingsManager.loadSettings();
1105
- let repo;
1106
- const multipleRemotes = await hasMultipleRemotes();
1107
- if (multipleRemotes) {
1108
- repo = await getConfiguredRepoFromSettings(settings);
1109
- logger.info(`Using GitHub repository: ${repo}`);
1110
- }
1111
- const parsed = await this.parseInput(input.identifier, input.options);
1112
- const worktrees = await this.validateInput(parsed, input.options, repo);
1113
- logger.info(`Validated input: ${this.formatParsedInput(parsed)}`);
1114
- const worktree = worktrees[0];
1115
- if (!worktree) {
1116
- throw new Error("No worktree found");
1117
- }
1118
- if (parsed.type === "pr") {
1119
- if (!parsed.number) {
1120
- throw new Error("Invalid PR number");
1121
- }
1122
- const pr = await this.gitHubService.fetchPR(parsed.number, repo);
1123
- await this.executePRWorkflow(parsed, input.options, worktree, pr);
1124
- } else {
1125
- await this.executeIssueWorkflow(parsed, input.options, worktree);
1126
- }
1127
- } catch (error) {
1128
- if (error instanceof Error) {
1129
- logger.error(`${error.message}`);
1130
- } else {
1131
- logger.error("An unknown error occurred");
1132
- }
1133
- throw error;
1134
- }
1135
- }
1136
- /**
1137
- * Parse input to determine type and extract relevant data
1138
- * Supports auto-detection from current directory when identifier is undefined
1139
- */
1140
- async parseInput(identifier, options) {
1141
- if (options.pr !== void 0) {
1142
- return {
1143
- type: "pr",
1144
- number: options.pr,
1145
- originalInput: `--pr ${options.pr}`,
1146
- autoDetected: false
1147
- };
1148
- }
1149
- if (identifier == null ? void 0 : identifier.trim()) {
1150
- return await this.parseExplicitInput(identifier.trim());
1151
- }
1152
- return await this.autoDetectFromCurrentDirectory();
1153
- }
1154
- /**
1155
- * Parse explicit identifier input using pattern-based detection
1156
- * (No GitHub API calls - uses IdentifierParser)
1157
- */
1158
- async parseExplicitInput(identifier) {
1159
- const prPattern = /^(?:pr|PR)[/-](\d+)$/;
1160
- const prMatch = identifier.match(prPattern);
1161
- if (prMatch == null ? void 0 : prMatch[1]) {
1162
- return {
1163
- type: "pr",
1164
- number: parseInt(prMatch[1], 10),
1165
- originalInput: identifier,
1166
- autoDetected: false
1167
- };
1168
- }
1169
- const parsed = await this.identifierParser.parseForPatternDetection(identifier);
1170
- if (parsed.type === "description") {
1171
- throw new Error("Description input type is not supported in finish command");
1172
- }
1173
- const result = {
1174
- type: parsed.type,
1175
- originalInput: parsed.originalInput,
1176
- autoDetected: false
1177
- };
1178
- if (parsed.number !== void 0) {
1179
- result.number = parsed.number;
1180
- }
1181
- if (parsed.branchName !== void 0) {
1182
- result.branchName = parsed.branchName;
1183
- }
1184
- return result;
1185
- }
1186
- /**
1187
- * Auto-detect PR or issue from current directory
1188
- * Ports logic from merge-current-issue.sh lines 30-52
1189
- */
1190
- async autoDetectFromCurrentDirectory() {
1191
- const currentDir = path.basename(process.cwd());
1192
- const prPattern = /_pr_(\d+)$/;
1193
- const prMatch = currentDir.match(prPattern);
1194
- if (prMatch == null ? void 0 : prMatch[1]) {
1195
- const prNumber = parseInt(prMatch[1], 10);
1196
- logger.debug(`Auto-detected PR #${prNumber} from directory: ${currentDir}`);
1197
- return {
1198
- type: "pr",
1199
- number: prNumber,
1200
- originalInput: currentDir,
1201
- autoDetected: true
1202
- };
1203
- }
1204
- const issuePattern = /issue-(\d+)/;
1205
- const issueMatch = currentDir.match(issuePattern);
1206
- if (issueMatch == null ? void 0 : issueMatch[1]) {
1207
- const issueNumber = parseInt(issueMatch[1], 10);
1208
- logger.debug(
1209
- `Auto-detected issue #${issueNumber} from directory: ${currentDir}`
1210
- );
1211
- return {
1212
- type: "issue",
1213
- number: issueNumber,
1214
- originalInput: currentDir,
1215
- autoDetected: true
1216
- };
1217
- }
1218
- const repoInfo = await this.gitWorktreeManager.getRepoInfo();
1219
- const currentBranch = repoInfo.currentBranch;
1220
- if (!currentBranch) {
1221
- throw new Error(
1222
- "Could not auto-detect identifier. Please provide an issue number, PR number, or branch name.\nExpected directory pattern: feat/issue-XX-description OR worktree with _pr_N suffix"
1223
- );
1224
- }
1225
- const branchIssueMatch = currentBranch.match(issuePattern);
1226
- if (branchIssueMatch == null ? void 0 : branchIssueMatch[1]) {
1227
- const issueNumber = parseInt(branchIssueMatch[1], 10);
1228
- logger.debug(
1229
- `Auto-detected issue #${issueNumber} from branch: ${currentBranch}`
1230
- );
1231
- return {
1232
- type: "issue",
1233
- number: issueNumber,
1234
- originalInput: currentBranch,
1235
- autoDetected: true
1236
- };
1237
- }
1238
- return {
1239
- type: "branch",
1240
- branchName: currentBranch,
1241
- originalInput: currentBranch,
1242
- autoDetected: true
1243
- };
1244
- }
1245
- /**
1246
- * Validate the parsed input based on its type
1247
- */
1248
- async validateInput(parsed, options, repo) {
1249
- switch (parsed.type) {
1250
- case "pr": {
1251
- if (!parsed.number) {
1252
- throw new Error("Invalid PR number");
1253
- }
1254
- const pr = await this.gitHubService.fetchPR(parsed.number);
1255
- logger.debug(`Validated PR #${parsed.number} (state: ${pr.state})`);
1256
- return await this.findWorktreeForIdentifier(parsed);
1257
- }
1258
- case "issue": {
1259
- if (!parsed.number) {
1260
- throw new Error("Invalid issue number");
1261
- }
1262
- const issue = await this.gitHubService.fetchIssue(parsed.number, repo);
1263
- if (issue.state === "closed" && !options.force) {
1264
- throw new Error(
1265
- `Issue #${parsed.number} is closed. Use --force to finish anyway.`
1266
- );
1267
- }
1268
- logger.debug(`Validated issue #${parsed.number} (state: ${issue.state})`);
1269
- return await this.findWorktreeForIdentifier(parsed);
1270
- }
1271
- case "branch": {
1272
- if (!parsed.branchName) {
1273
- throw new Error("Invalid branch name");
1274
- }
1275
- if (!this.isValidBranchName(parsed.branchName)) {
1276
- throw new Error(
1277
- "Invalid branch name. Use only letters, numbers, hyphens, underscores, and slashes"
1278
- );
1279
- }
1280
- logger.debug(`Validated branch name: ${parsed.branchName}`);
1281
- return await this.findWorktreeForIdentifier(parsed);
1282
- }
1283
- default: {
1284
- const unknownType = parsed;
1285
- throw new Error(`Unknown input type: ${unknownType.type}`);
1286
- }
1287
- }
1288
- }
1289
- /**
1290
- * Find worktree for the given identifier using specific methods based on type
1291
- * (uses precise pattern matching instead of broad substring matching)
1292
- * Throws error if not found
1293
- */
1294
- async findWorktreeForIdentifier(parsed) {
1295
- let worktree = null;
1296
- switch (parsed.type) {
1297
- case "pr": {
1298
- if (!parsed.number) {
1299
- throw new Error("Invalid PR number");
1300
- }
1301
- worktree = await this.gitWorktreeManager.findWorktreeForPR(
1302
- parsed.number,
1303
- ""
1304
- );
1305
- break;
1306
- }
1307
- case "issue": {
1308
- if (!parsed.number) {
1309
- throw new Error("Invalid issue number");
1310
- }
1311
- worktree = await this.gitWorktreeManager.findWorktreeForIssue(
1312
- parsed.number
1313
- );
1314
- break;
1315
- }
1316
- case "branch": {
1317
- if (!parsed.branchName) {
1318
- throw new Error("Invalid branch name");
1319
- }
1320
- worktree = await this.gitWorktreeManager.findWorktreeForBranch(
1321
- parsed.branchName
1322
- );
1323
- break;
1324
- }
1325
- default: {
1326
- const unknownType = parsed;
1327
- throw new Error(`Unknown input type: ${unknownType.type}`);
1328
- }
1329
- }
1330
- if (!worktree) {
1331
- throw new Error(
1332
- `No worktree found for ${this.formatParsedInput(parsed)}. Use 'il list' to see available worktrees.`
1333
- );
1334
- }
1335
- logger.debug(`Found worktree: ${worktree.path}`);
1336
- return [worktree];
1337
- }
1338
- /**
1339
- * Validate branch name format
1340
- */
1341
- isValidBranchName(branch) {
1342
- return /^[a-zA-Z0-9/_-]+$/.test(branch);
1343
- }
1344
- /**
1345
- * Format parsed input for display
1346
- */
1347
- formatParsedInput(parsed) {
1348
- const autoLabel = parsed.autoDetected ? " (auto-detected)" : "";
1349
- switch (parsed.type) {
1350
- case "pr":
1351
- return `PR #${parsed.number}${autoLabel}`;
1352
- case "issue":
1353
- return `Issue #${parsed.number}${autoLabel}`;
1354
- case "branch":
1355
- return `Branch '${parsed.branchName}'${autoLabel}`;
1356
- default:
1357
- return "Unknown input";
1358
- }
1359
- }
1360
- /**
1361
- * Execute workflow for issues and branches (merge into main)
1362
- * This is the traditional workflow: validate → commit → rebase → merge → cleanup
1363
- */
1364
- async executeIssueWorkflow(parsed, options, worktree) {
1365
- var _a, _b;
1366
- if (!options.dryRun) {
1367
- logger.info("Running pre-merge validations...");
1368
- await this.validationRunner.runValidations(worktree.path, {
1369
- dryRun: options.dryRun ?? false
1370
- });
1371
- logger.success("All validations passed");
1372
- } else {
1373
- logger.info("[DRY RUN] Would run pre-merge validations");
1374
- }
1375
- const gitStatus = await this.commitManager.detectUncommittedChanges(worktree.path);
1376
- if (gitStatus.hasUncommittedChanges) {
1377
- if (options.dryRun) {
1378
- logger.info("[DRY RUN] Would auto-commit uncommitted changes (validation passed)");
1379
- } else {
1380
- logger.info("Validation passed, auto-committing uncommitted changes...");
1381
- const settings2 = await this.settingsManager.loadSettings(worktree.path);
1382
- const skipVerify = ((_b = (_a = settings2.workflows) == null ? void 0 : _a.issue) == null ? void 0 : _b.noVerify) ?? false;
1383
- const commitOptions = {
1384
- dryRun: options.dryRun ?? false,
1385
- skipVerify
1386
- };
1387
- if (parsed.type === "issue" && parsed.number) {
1388
- commitOptions.issueNumber = parsed.number;
1389
- }
1390
- await this.commitManager.commitChanges(worktree.path, commitOptions);
1391
- logger.success("Changes committed successfully");
1392
- }
1393
- } else {
1394
- logger.debug("No uncommitted changes found");
1395
- }
1396
- const settings = await this.settingsManager.loadSettings(worktree.path);
1397
- const mergeBehavior = settings.mergeBehavior ?? { mode: "local" };
1398
- if (mergeBehavior.mode === "github-pr") {
1399
- await this.executeGitHubPRWorkflow(parsed, options, worktree, settings);
1400
- return;
1401
- }
1402
- logger.info("Rebasing branch on main...");
1403
- const mergeOptions = {
1404
- dryRun: options.dryRun ?? false,
1405
- force: options.force ?? false
1406
- };
1407
- await this.mergeManager.rebaseOnMain(worktree.path, mergeOptions);
1408
- logger.success("Branch rebased successfully");
1409
- logger.info("Performing fast-forward merge...");
1410
- await this.mergeManager.performFastForwardMerge(worktree.branch, worktree.path, mergeOptions);
1411
- logger.success("Fast-forward merge completed successfully");
1412
- if (options.dryRun) {
1413
- logger.info("[DRY RUN] Would install dependencies in main worktree");
1414
- } else {
1415
- logger.info("Installing dependencies in main worktree...");
1416
- const mainWorktreePath = await findMainWorktreePathWithSettings(worktree.path, this.settingsManager);
1417
- await installDependencies(mainWorktreePath, true);
1418
- }
1419
- if (!options.skipBuild) {
1420
- await this.runPostMergeBuild(worktree.path, options);
1421
- } else {
1422
- logger.debug("Skipping build verification (--skip-build flag provided)");
1423
- }
1424
- await this.performPostMergeCleanup(parsed, options, worktree);
1425
- }
1426
- /**
1427
- * Execute workflow for Pull Requests
1428
- * Behavior depends on PR state:
1429
- * - OPEN: Commit changes, push to remote, keep worktree active
1430
- * - CLOSED/MERGED: Skip to cleanup
1431
- */
1432
- async executePRWorkflow(parsed, options, worktree, pr) {
1433
- var _a, _b;
1434
- if (pr.state === "closed" || pr.state === "merged") {
1435
- logger.info(`PR #${parsed.number} is ${pr.state.toUpperCase()} - skipping to cleanup`);
1436
- const gitStatus = await this.commitManager.detectUncommittedChanges(worktree.path);
1437
- if (gitStatus.hasUncommittedChanges && !options.force) {
1438
- logger.warn("PR has uncommitted changes");
1439
- throw new Error(
1440
- "Cannot cleanup PR with uncommitted changes. Commit or stash changes, then run again with --force to cleanup anyway."
1441
- );
1442
- }
1443
- await this.performPRCleanup(parsed, options, worktree);
1444
- logger.success(`PR #${parsed.number} cleanup completed`);
1445
- } else {
1446
- logger.info(`PR #${parsed.number} is OPEN - will push changes and keep worktree active`);
1447
- const gitStatus = await this.commitManager.detectUncommittedChanges(worktree.path);
1448
- if (gitStatus.hasUncommittedChanges) {
1449
- if (options.dryRun) {
1450
- logger.info("[DRY RUN] Would commit uncommitted changes");
1451
- } else {
1452
- logger.info("Committing uncommitted changes...");
1453
- const settings = await this.settingsManager.loadSettings(worktree.path);
1454
- const skipVerify = ((_b = (_a = settings.workflows) == null ? void 0 : _a.pr) == null ? void 0 : _b.noVerify) ?? false;
1455
- await this.commitManager.commitChanges(worktree.path, {
1456
- dryRun: false,
1457
- skipVerify
1458
- // Do NOT pass issueNumber for PRs - no "Fixes #" trailer needed
1459
- });
1460
- logger.success("Changes committed");
1461
- }
1462
- } else {
1463
- logger.debug("No uncommitted changes found");
1464
- }
1465
- if (options.dryRun) {
1466
- logger.info(`[DRY RUN] Would push changes to origin/${pr.branch}`);
1467
- } else {
1468
- logger.info("Pushing changes to remote...");
1469
- const { pushBranchToRemote: pushBranchToRemote2 } = await import("./git-WC6HZLOT.js");
1470
- await pushBranchToRemote2(pr.branch, worktree.path, {
1471
- dryRun: false
1472
- });
1473
- logger.success(`Changes pushed to PR #${parsed.number}`);
1474
- }
1475
- logger.success(`PR #${parsed.number} updated successfully`);
1476
- logger.info("Worktree remains active for continued work");
1477
- logger.info(`To cleanup when done: il cleanup ${parsed.number}`);
1478
- }
1479
- }
1480
- /**
1481
- * Execute workflow for GitHub PR creation (github-pr merge mode)
1482
- * Validates → Commits → Pushes → Creates PR → Prompts for cleanup
1483
- */
1484
- async executeGitHubPRWorkflow(parsed, options, worktree, settings) {
1485
- if (options.dryRun) {
1486
- logger.info("[DRY RUN] Would push branch to origin");
1487
- } else {
1488
- logger.info("Pushing branch to origin...");
1489
- await pushBranchToRemote(worktree.branch, worktree.path, { dryRun: false });
1490
- logger.success("Branch pushed successfully");
1491
- }
1492
- const prManager = new PRManager(settings);
1493
- let prTitle = `Work from ${worktree.branch}`;
1494
- if (parsed.type === "issue" && parsed.number) {
1495
- try {
1496
- const issue = await this.gitHubService.fetchIssue(parsed.number);
1497
- prTitle = issue.title;
1498
- } catch (error) {
1499
- logger.debug("Could not fetch issue title, using branch name", { error });
1500
- }
1501
- }
1502
- if (options.dryRun) {
1503
- logger.info("[DRY RUN] Would create GitHub PR");
1504
- logger.info(` Title: ${prTitle}`);
1505
- logger.info(` Base: ${settings.mainBranch ?? "main"}`);
1506
- } else {
1507
- const baseBranch = settings.mainBranch ?? "main";
1508
- const openInBrowser = options.noBrowser !== true;
1509
- const result = await prManager.createOrOpenPR(
1510
- worktree.branch,
1511
- prTitle,
1512
- parsed.type === "issue" ? parsed.number : void 0,
1513
- baseBranch,
1514
- worktree.path,
1515
- openInBrowser
1516
- );
1517
- if (result.wasExisting) {
1518
- logger.success(`Existing pull request: ${result.url}`);
1519
- } else {
1520
- logger.success(`Pull request created: ${result.url}`);
1521
- }
1522
- await this.handlePRCleanupPrompt(parsed, options, worktree);
1523
- }
1524
- }
1525
- /**
1526
- * Handle cleanup prompt after PR creation
1527
- * Respects --cleanup and --no-cleanup flags, otherwise prompts user
1528
- */
1529
- async handlePRCleanupPrompt(parsed, options, worktree) {
1530
- if (options.cleanup === true) {
1531
- logger.info("Cleaning up worktree (--cleanup flag)...");
1532
- await this.performWorktreeCleanup(parsed, options, worktree);
1533
- } else if (options.cleanup === false) {
1534
- logger.info("Worktree kept active for continued work (--no-cleanup flag)");
1535
- logger.info(`To cleanup later: il cleanup ${parsed.originalInput}`);
1536
- } else {
1537
- logger.info("");
1538
- logger.info("PR created successfully. Would you like to clean up the worktree?");
1539
- logger.info(` Worktree: ${worktree.path}`);
1540
- logger.info(` Branch: ${worktree.branch}`);
1541
- logger.info("");
1542
- const shouldCleanup = await promptConfirmation(
1543
- "Clean up worktree now?",
1544
- false
1545
- // Default to keeping worktree (safer option)
1546
- );
1547
- if (shouldCleanup) {
1548
- await this.performWorktreeCleanup(parsed, options, worktree);
1549
- } else {
1550
- logger.info("Worktree kept active. Run `il cleanup` when ready.");
1551
- }
1552
- }
1553
- }
1554
- /**
1555
- * Perform worktree cleanup (used by GitHub PR workflow)
1556
- * Similar to performPostMergeCleanup but for PR workflow
1557
- */
1558
- async performWorktreeCleanup(parsed, options, worktree) {
1559
- const cleanupInput = {
1560
- type: parsed.type,
1561
- originalInput: parsed.originalInput,
1562
- ...parsed.number !== void 0 && { number: parsed.number },
1563
- ...parsed.branchName !== void 0 && { branchName: parsed.branchName }
1564
- };
1565
- const cleanupOptions = {
1566
- dryRun: options.dryRun ?? false,
1567
- deleteBranch: false,
1568
- // Don't delete branch - PR still needs it
1569
- keepDatabase: false,
1570
- // Clean up database
1571
- force: options.force ?? false
1572
- };
1573
- try {
1574
- logger.info("Starting worktree cleanup...");
1575
- await this.ensureResourceCleanup();
1576
- if (!this.resourceCleanup) {
1577
- throw new Error("Failed to initialize ResourceCleanup");
1578
- }
1579
- const result = await this.resourceCleanup.cleanupWorktree(cleanupInput, cleanupOptions);
1580
- this.reportCleanupResults(result);
1581
- if (!result.success) {
1582
- logger.warn("Some cleanup operations failed - manual cleanup may be required");
1583
- this.showManualCleanupInstructions(worktree);
1584
- } else {
1585
- logger.success("Worktree cleanup completed successfully");
1586
- }
1587
- if (this.isRunningFromWithinWorktree(worktree.path)) {
1588
- this.showTerminalCloseWarning(worktree);
1589
- }
1590
- } catch (error) {
1591
- const errorMessage = error instanceof Error ? error.message : "Unknown error";
1592
- logger.warn(`Cleanup failed: ${errorMessage}`);
1593
- logger.warn("Manual cleanup may be required");
1594
- this.showManualCleanupInstructions(worktree);
1595
- }
1596
- }
1597
- /**
1598
- * Perform cleanup for closed/merged PRs
1599
- * Similar to performPostMergeCleanup but with different messaging
1600
- */
1601
- async performPRCleanup(parsed, options, worktree) {
1602
- const cleanupInput = {
1603
- type: parsed.type,
1604
- originalInput: parsed.originalInput,
1605
- ...parsed.number !== void 0 && { number: parsed.number },
1606
- ...parsed.branchName !== void 0 && { branchName: parsed.branchName }
1607
- };
1608
- const cleanupOptions = {
1609
- dryRun: options.dryRun ?? false,
1610
- deleteBranch: true,
1611
- // Delete branch for closed/merged PRs
1612
- keepDatabase: false,
1613
- force: options.force ?? false
1614
- };
1615
- try {
1616
- await this.ensureResourceCleanup();
1617
- if (!this.resourceCleanup) {
1618
- throw new Error("Failed to initialize ResourceCleanup");
1619
- }
1620
- const result = await this.resourceCleanup.cleanupWorktree(cleanupInput, cleanupOptions);
1621
- this.reportCleanupResults(result);
1622
- if (!result.success) {
1623
- logger.warn("Some cleanup operations failed - manual cleanup may be required");
1624
- this.showManualCleanupInstructions(worktree);
1625
- } else {
1626
- if (this.isRunningFromWithinWorktree(worktree.path)) {
1627
- this.showTerminalCloseWarning(worktree);
1628
- }
1629
- }
1630
- } catch (error) {
1631
- const errorMessage = error instanceof Error ? error.message : "Unknown error";
1632
- logger.warn(`Cleanup failed: ${errorMessage}`);
1633
- this.showManualCleanupInstructions(worktree);
1634
- throw error;
1635
- }
1636
- }
1637
- /**
1638
- * Run post-merge build verification for CLI projects
1639
- * Runs in main worktree to verify merged code builds successfully
1640
- */
1641
- async runPostMergeBuild(worktreePath, options) {
1642
- const mainWorktreePath = await findMainWorktreePathWithSettings(worktreePath, this.settingsManager);
1643
- if (options.dryRun) {
1644
- logger.info("[DRY RUN] Would run post-merge build");
1645
- return;
1646
- }
1647
- logger.info("Running post-merge build...");
1648
- const result = await this.buildRunner.runBuild(mainWorktreePath, {
1649
- dryRun: options.dryRun ?? false
1650
- });
1651
- if (result.skipped) {
1652
- logger.debug(`Build skipped: ${result.reason}`);
1653
- } else {
1654
- logger.success("Post-merge build completed successfully");
1655
- }
1656
- }
1657
- /**
1658
- * Perform post-merge cleanup operations
1659
- * Converts ParsedFinishInput to ParsedInput and calls ResourceCleanup
1660
- * Handles failures gracefully without throwing
1661
- */
1662
- async performPostMergeCleanup(parsed, options, worktree) {
1663
- const cleanupInput = {
1664
- type: parsed.type,
1665
- originalInput: parsed.originalInput,
1666
- ...parsed.number !== void 0 && { number: parsed.number },
1667
- ...parsed.branchName !== void 0 && { branchName: parsed.branchName }
1668
- };
1669
- const cleanupOptions = {
1670
- dryRun: options.dryRun ?? false,
1671
- deleteBranch: true,
1672
- // Delete branch after successful merge
1673
- keepDatabase: false,
1674
- // Clean up database after merge
1675
- force: options.force ?? false
1676
- };
1677
- try {
1678
- logger.info("Starting post-merge cleanup...");
1679
- await this.ensureResourceCleanup();
1680
- if (!this.resourceCleanup) {
1681
- throw new Error("Failed to initialize ResourceCleanup");
1682
- }
1683
- const result = await this.resourceCleanup.cleanupWorktree(cleanupInput, cleanupOptions);
1684
- this.reportCleanupResults(result);
1685
- if (!result.success) {
1686
- logger.warn("Some cleanup operations failed - manual cleanup may be required");
1687
- this.showManualCleanupInstructions(worktree);
1688
- } else {
1689
- logger.success("Post-merge cleanup completed successfully");
1690
- }
1691
- if (this.isRunningFromWithinWorktree(worktree.path)) {
1692
- this.showTerminalCloseWarning(worktree);
1693
- }
1694
- } catch (error) {
1695
- const errorMessage = error instanceof Error ? error.message : "Unknown error";
1696
- logger.warn(`Cleanup failed: ${errorMessage}`);
1697
- logger.warn("Merge completed successfully, but manual cleanup is required");
1698
- this.showManualCleanupInstructions(worktree);
1699
- }
1700
- }
1701
- /**
1702
- * Report cleanup operation results to user
1703
- */
1704
- reportCleanupResults(result) {
1705
- if (result.operations.length === 0) {
1706
- return;
1707
- }
1708
- logger.info("Cleanup operations:");
1709
- for (const op of result.operations) {
1710
- const status = op.success ? "\u2713" : "\u2717";
1711
- const message = op.error ? `${op.message}: ${op.error}` : op.message;
1712
- if (op.success) {
1713
- logger.info(` ${status} ${message}`);
1714
- } else {
1715
- logger.warn(` ${status} ${message}`);
1716
- }
1717
- }
1718
- }
1719
- /**
1720
- * Show manual cleanup instructions when cleanup fails
1721
- */
1722
- showManualCleanupInstructions(worktree) {
1723
- logger.info("\nManual cleanup commands:");
1724
- logger.info(` 1. Remove worktree: git worktree remove ${worktree.path}`);
1725
- logger.info(` 2. Delete branch: git branch -d ${worktree.branch}`);
1726
- logger.info(` 3. Check dev servers: lsof -i :PORT (and kill if needed)`);
1727
- }
1728
- /**
1729
- * Check if current working directory is within the target worktree
1730
- */
1731
- isRunningFromWithinWorktree(worktreePath) {
1732
- const normalizedCwd = path.normalize(process.cwd());
1733
- const normalizedWorktree = path.normalize(worktreePath);
1734
- return normalizedCwd.startsWith(normalizedWorktree);
1735
- }
1736
- /**
1737
- * Display warning to close terminal/IDE when running from within finished loom
1738
- */
1739
- showTerminalCloseWarning(worktree) {
1740
- logger.info("");
1741
- logger.info("You are currently in the directory of the loom that was just finished.");
1742
- logger.info("Please close this terminal and any IDE/terminal windows using this directory.");
1743
- logger.info(`Directory: ${worktree.path}`);
1744
- }
1745
- };
1746
- export {
1747
- FinishCommand
1748
- };
1749
- //# sourceMappingURL=finish-TX5CJICB.js.map