@iloom/cli 0.1.14
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +33 -0
- package/README.md +711 -0
- package/dist/ClaudeContextManager-XOSXQ67R.js +13 -0
- package/dist/ClaudeContextManager-XOSXQ67R.js.map +1 -0
- package/dist/ClaudeService-YSZ6EXWP.js +12 -0
- package/dist/ClaudeService-YSZ6EXWP.js.map +1 -0
- package/dist/GitHubService-F7Z3XJOS.js +11 -0
- package/dist/GitHubService-F7Z3XJOS.js.map +1 -0
- package/dist/LoomLauncher-MODG2SEM.js +263 -0
- package/dist/LoomLauncher-MODG2SEM.js.map +1 -0
- package/dist/NeonProvider-PAGPUH7F.js +12 -0
- package/dist/NeonProvider-PAGPUH7F.js.map +1 -0
- package/dist/PromptTemplateManager-7FINLRDE.js +9 -0
- package/dist/PromptTemplateManager-7FINLRDE.js.map +1 -0
- package/dist/SettingsManager-VAZF26S2.js +19 -0
- package/dist/SettingsManager-VAZF26S2.js.map +1 -0
- package/dist/SettingsMigrationManager-MTQIMI54.js +146 -0
- package/dist/SettingsMigrationManager-MTQIMI54.js.map +1 -0
- package/dist/add-issue-22JBNOML.js +54 -0
- package/dist/add-issue-22JBNOML.js.map +1 -0
- package/dist/agents/iloom-issue-analyze-and-plan.md +580 -0
- package/dist/agents/iloom-issue-analyzer.md +290 -0
- package/dist/agents/iloom-issue-complexity-evaluator.md +224 -0
- package/dist/agents/iloom-issue-enhancer.md +266 -0
- package/dist/agents/iloom-issue-implementer.md +262 -0
- package/dist/agents/iloom-issue-planner.md +358 -0
- package/dist/agents/iloom-issue-reviewer.md +63 -0
- package/dist/chunk-2ZPFJQ3B.js +63 -0
- package/dist/chunk-2ZPFJQ3B.js.map +1 -0
- package/dist/chunk-37DYYFVK.js +29 -0
- package/dist/chunk-37DYYFVK.js.map +1 -0
- package/dist/chunk-BLCTGFZN.js +121 -0
- package/dist/chunk-BLCTGFZN.js.map +1 -0
- package/dist/chunk-CP2NU2JC.js +545 -0
- package/dist/chunk-CP2NU2JC.js.map +1 -0
- package/dist/chunk-CWR2SANQ.js +39 -0
- package/dist/chunk-CWR2SANQ.js.map +1 -0
- package/dist/chunk-F3XBU2R7.js +110 -0
- package/dist/chunk-F3XBU2R7.js.map +1 -0
- package/dist/chunk-GEHQXLEI.js +130 -0
- package/dist/chunk-GEHQXLEI.js.map +1 -0
- package/dist/chunk-GYCR2LOU.js +143 -0
- package/dist/chunk-GYCR2LOU.js.map +1 -0
- package/dist/chunk-GZP4UGGM.js +48 -0
- package/dist/chunk-GZP4UGGM.js.map +1 -0
- package/dist/chunk-H4E4THUZ.js +55 -0
- package/dist/chunk-H4E4THUZ.js.map +1 -0
- package/dist/chunk-HPJJSYNS.js +644 -0
- package/dist/chunk-HPJJSYNS.js.map +1 -0
- package/dist/chunk-JBH2ZYYZ.js +220 -0
- package/dist/chunk-JBH2ZYYZ.js.map +1 -0
- package/dist/chunk-JNKJ7NJV.js +78 -0
- package/dist/chunk-JNKJ7NJV.js.map +1 -0
- package/dist/chunk-JQ7VOSTC.js +437 -0
- package/dist/chunk-JQ7VOSTC.js.map +1 -0
- package/dist/chunk-KQDEK2ZW.js +199 -0
- package/dist/chunk-KQDEK2ZW.js.map +1 -0
- package/dist/chunk-O2QWO64Z.js +179 -0
- package/dist/chunk-O2QWO64Z.js.map +1 -0
- package/dist/chunk-OC4H6HJD.js +248 -0
- package/dist/chunk-OC4H6HJD.js.map +1 -0
- package/dist/chunk-PR7FKQBG.js +120 -0
- package/dist/chunk-PR7FKQBG.js.map +1 -0
- package/dist/chunk-PXZBAC2M.js +250 -0
- package/dist/chunk-PXZBAC2M.js.map +1 -0
- package/dist/chunk-QEPVTTHD.js +383 -0
- package/dist/chunk-QEPVTTHD.js.map +1 -0
- package/dist/chunk-RSRO7564.js +203 -0
- package/dist/chunk-RSRO7564.js.map +1 -0
- package/dist/chunk-SJUQ2NDR.js +146 -0
- package/dist/chunk-SJUQ2NDR.js.map +1 -0
- package/dist/chunk-SPYPLHMK.js +177 -0
- package/dist/chunk-SPYPLHMK.js.map +1 -0
- package/dist/chunk-SSCQCCJ7.js +75 -0
- package/dist/chunk-SSCQCCJ7.js.map +1 -0
- package/dist/chunk-SSR5AVRJ.js +41 -0
- package/dist/chunk-SSR5AVRJ.js.map +1 -0
- package/dist/chunk-T7QPXANZ.js +315 -0
- package/dist/chunk-T7QPXANZ.js.map +1 -0
- package/dist/chunk-U3WU5OWO.js +203 -0
- package/dist/chunk-U3WU5OWO.js.map +1 -0
- package/dist/chunk-W3DQTW63.js +124 -0
- package/dist/chunk-W3DQTW63.js.map +1 -0
- package/dist/chunk-WKEWRSDB.js +151 -0
- package/dist/chunk-WKEWRSDB.js.map +1 -0
- package/dist/chunk-Y7SAGNUT.js +66 -0
- package/dist/chunk-Y7SAGNUT.js.map +1 -0
- package/dist/chunk-YETJNRQM.js +39 -0
- package/dist/chunk-YETJNRQM.js.map +1 -0
- package/dist/chunk-YYSKGAZT.js +384 -0
- package/dist/chunk-YYSKGAZT.js.map +1 -0
- package/dist/chunk-ZZZWQGTS.js +169 -0
- package/dist/chunk-ZZZWQGTS.js.map +1 -0
- package/dist/claude-7LUVDZZ4.js +17 -0
- package/dist/claude-7LUVDZZ4.js.map +1 -0
- package/dist/cleanup-3LUWPSM7.js +412 -0
- package/dist/cleanup-3LUWPSM7.js.map +1 -0
- package/dist/cli-overrides-XFZWY7CM.js +16 -0
- package/dist/cli-overrides-XFZWY7CM.js.map +1 -0
- package/dist/cli.js +603 -0
- package/dist/cli.js.map +1 -0
- package/dist/color-ZVALX37U.js +21 -0
- package/dist/color-ZVALX37U.js.map +1 -0
- package/dist/enhance-XJIQHVPD.js +166 -0
- package/dist/enhance-XJIQHVPD.js.map +1 -0
- package/dist/env-MDFL4ZXL.js +23 -0
- package/dist/env-MDFL4ZXL.js.map +1 -0
- package/dist/feedback-23CLXKFT.js +158 -0
- package/dist/feedback-23CLXKFT.js.map +1 -0
- package/dist/finish-CY4CIH6O.js +1608 -0
- package/dist/finish-CY4CIH6O.js.map +1 -0
- package/dist/git-LVRZ57GJ.js +43 -0
- package/dist/git-LVRZ57GJ.js.map +1 -0
- package/dist/ignite-WXEF2ID5.js +359 -0
- package/dist/ignite-WXEF2ID5.js.map +1 -0
- package/dist/index.d.ts +1341 -0
- package/dist/index.js +3058 -0
- package/dist/index.js.map +1 -0
- package/dist/init-RHACUR4E.js +123 -0
- package/dist/init-RHACUR4E.js.map +1 -0
- package/dist/installation-detector-VARGFFRZ.js +11 -0
- package/dist/installation-detector-VARGFFRZ.js.map +1 -0
- package/dist/logger-MKYH4UDV.js +12 -0
- package/dist/logger-MKYH4UDV.js.map +1 -0
- package/dist/mcp/chunk-6SDFJ42P.js +62 -0
- package/dist/mcp/chunk-6SDFJ42P.js.map +1 -0
- package/dist/mcp/claude-YHHHLSXH.js +249 -0
- package/dist/mcp/claude-YHHHLSXH.js.map +1 -0
- package/dist/mcp/color-QS5BFCNN.js +168 -0
- package/dist/mcp/color-QS5BFCNN.js.map +1 -0
- package/dist/mcp/github-comment-server.js +165 -0
- package/dist/mcp/github-comment-server.js.map +1 -0
- package/dist/mcp/terminal-SDCMDVD7.js +202 -0
- package/dist/mcp/terminal-SDCMDVD7.js.map +1 -0
- package/dist/open-X6BTENPV.js +278 -0
- package/dist/open-X6BTENPV.js.map +1 -0
- package/dist/prompt-ANTQWHUF.js +13 -0
- package/dist/prompt-ANTQWHUF.js.map +1 -0
- package/dist/prompts/issue-prompt.txt +230 -0
- package/dist/prompts/pr-prompt.txt +35 -0
- package/dist/prompts/regular-prompt.txt +14 -0
- package/dist/run-2JCPQAX3.js +278 -0
- package/dist/run-2JCPQAX3.js.map +1 -0
- package/dist/schema/settings.schema.json +221 -0
- package/dist/start-LWVRBJ6S.js +982 -0
- package/dist/start-LWVRBJ6S.js.map +1 -0
- package/dist/terminal-3D6TUAKJ.js +16 -0
- package/dist/terminal-3D6TUAKJ.js.map +1 -0
- package/dist/test-git-XPF4SZXJ.js +52 -0
- package/dist/test-git-XPF4SZXJ.js.map +1 -0
- package/dist/test-prefix-XGFXFAYN.js +68 -0
- package/dist/test-prefix-XGFXFAYN.js.map +1 -0
- package/dist/test-tabs-JRKY3QMM.js +69 -0
- package/dist/test-tabs-JRKY3QMM.js.map +1 -0
- package/dist/test-webserver-M2I3EV4J.js +62 -0
- package/dist/test-webserver-M2I3EV4J.js.map +1 -0
- package/dist/update-3ZT2XX2G.js +79 -0
- package/dist/update-3ZT2XX2G.js.map +1 -0
- package/dist/update-notifier-QSSEB5KC.js +11 -0
- package/dist/update-notifier-QSSEB5KC.js.map +1 -0
- package/package.json +113 -0
|
@@ -0,0 +1,1608 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import {
|
|
3
|
+
ResourceCleanup
|
|
4
|
+
} from "./chunk-CP2NU2JC.js";
|
|
5
|
+
import {
|
|
6
|
+
IdentifierParser
|
|
7
|
+
} from "./chunk-H4E4THUZ.js";
|
|
8
|
+
import {
|
|
9
|
+
ProcessManager
|
|
10
|
+
} from "./chunk-SPYPLHMK.js";
|
|
11
|
+
import {
|
|
12
|
+
CLIIsolationManager,
|
|
13
|
+
DatabaseManager,
|
|
14
|
+
EnvironmentManager
|
|
15
|
+
} from "./chunk-HPJJSYNS.js";
|
|
16
|
+
import "./chunk-37DYYFVK.js";
|
|
17
|
+
import {
|
|
18
|
+
detectPackageManager,
|
|
19
|
+
installDependencies,
|
|
20
|
+
runScript
|
|
21
|
+
} from "./chunk-BLCTGFZN.js";
|
|
22
|
+
import {
|
|
23
|
+
NeonProvider
|
|
24
|
+
} from "./chunk-YYSKGAZT.js";
|
|
25
|
+
import {
|
|
26
|
+
ProjectCapabilityDetector
|
|
27
|
+
} from "./chunk-CWR2SANQ.js";
|
|
28
|
+
import {
|
|
29
|
+
hasScript,
|
|
30
|
+
readPackageJson
|
|
31
|
+
} from "./chunk-2ZPFJQ3B.js";
|
|
32
|
+
import {
|
|
33
|
+
GitHubService
|
|
34
|
+
} from "./chunk-T7QPXANZ.js";
|
|
35
|
+
import "./chunk-KQDEK2ZW.js";
|
|
36
|
+
import {
|
|
37
|
+
loadEnvIntoProcess
|
|
38
|
+
} from "./chunk-SJUQ2NDR.js";
|
|
39
|
+
import {
|
|
40
|
+
detectClaudeCli,
|
|
41
|
+
launchClaude
|
|
42
|
+
} from "./chunk-PXZBAC2M.js";
|
|
43
|
+
import {
|
|
44
|
+
GitWorktreeManager
|
|
45
|
+
} from "./chunk-QEPVTTHD.js";
|
|
46
|
+
import {
|
|
47
|
+
SettingsManager
|
|
48
|
+
} from "./chunk-JBH2ZYYZ.js";
|
|
49
|
+
import {
|
|
50
|
+
executeGitCommand,
|
|
51
|
+
findMainWorktreePathWithSettings
|
|
52
|
+
} from "./chunk-JQ7VOSTC.js";
|
|
53
|
+
import "./chunk-JNKJ7NJV.js";
|
|
54
|
+
import {
|
|
55
|
+
logger
|
|
56
|
+
} from "./chunk-GEHQXLEI.js";
|
|
57
|
+
|
|
58
|
+
// src/lib/ValidationRunner.ts
|
|
59
|
+
var ValidationRunner = class {
|
|
60
|
+
/**
|
|
61
|
+
* Run all validations in sequence: typecheck → lint → test
|
|
62
|
+
* Fails fast on first error
|
|
63
|
+
*/
|
|
64
|
+
async runValidations(worktreePath, options = {}) {
|
|
65
|
+
const startTime = Date.now();
|
|
66
|
+
const steps = [];
|
|
67
|
+
if (!options.skipTypecheck) {
|
|
68
|
+
const typecheckResult = await this.runTypecheck(
|
|
69
|
+
worktreePath,
|
|
70
|
+
options.dryRun ?? false
|
|
71
|
+
);
|
|
72
|
+
steps.push(typecheckResult);
|
|
73
|
+
if (!typecheckResult.passed && !typecheckResult.skipped) {
|
|
74
|
+
return {
|
|
75
|
+
success: false,
|
|
76
|
+
steps,
|
|
77
|
+
totalDuration: Date.now() - startTime
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
if (!options.skipLint) {
|
|
82
|
+
const lintResult = await this.runLint(worktreePath, options.dryRun ?? false);
|
|
83
|
+
steps.push(lintResult);
|
|
84
|
+
if (!lintResult.passed && !lintResult.skipped) {
|
|
85
|
+
return { success: false, steps, totalDuration: Date.now() - startTime };
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
if (!options.skipTests) {
|
|
89
|
+
const testResult = await this.runTests(
|
|
90
|
+
worktreePath,
|
|
91
|
+
options.dryRun ?? false
|
|
92
|
+
);
|
|
93
|
+
steps.push(testResult);
|
|
94
|
+
if (!testResult.passed && !testResult.skipped) {
|
|
95
|
+
return { success: false, steps, totalDuration: Date.now() - startTime };
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
return { success: true, steps, totalDuration: Date.now() - startTime };
|
|
99
|
+
}
|
|
100
|
+
/**
|
|
101
|
+
* Run typecheck validation
|
|
102
|
+
*/
|
|
103
|
+
async runTypecheck(worktreePath, dryRun) {
|
|
104
|
+
const stepStartTime = Date.now();
|
|
105
|
+
try {
|
|
106
|
+
const pkgJson = await readPackageJson(worktreePath);
|
|
107
|
+
const hasTypecheckScript = hasScript(pkgJson, "typecheck");
|
|
108
|
+
if (!hasTypecheckScript) {
|
|
109
|
+
logger.debug("Skipping typecheck - no typecheck script found");
|
|
110
|
+
return {
|
|
111
|
+
step: "typecheck",
|
|
112
|
+
passed: true,
|
|
113
|
+
skipped: true,
|
|
114
|
+
duration: Date.now() - stepStartTime
|
|
115
|
+
};
|
|
116
|
+
}
|
|
117
|
+
} catch (error) {
|
|
118
|
+
if (error instanceof Error && error.message.includes("package.json not found")) {
|
|
119
|
+
logger.debug("Skipping typecheck - no package.json found (non-Node.js project)");
|
|
120
|
+
return {
|
|
121
|
+
step: "typecheck",
|
|
122
|
+
passed: true,
|
|
123
|
+
skipped: true,
|
|
124
|
+
duration: Date.now() - stepStartTime
|
|
125
|
+
};
|
|
126
|
+
}
|
|
127
|
+
throw error;
|
|
128
|
+
}
|
|
129
|
+
const packageManager = await detectPackageManager(worktreePath);
|
|
130
|
+
if (dryRun) {
|
|
131
|
+
const command = packageManager === "npm" ? "npm run typecheck" : `${packageManager} typecheck`;
|
|
132
|
+
logger.info(`[DRY RUN] Would run: ${command}`);
|
|
133
|
+
return {
|
|
134
|
+
step: "typecheck",
|
|
135
|
+
passed: true,
|
|
136
|
+
skipped: false,
|
|
137
|
+
duration: Date.now() - stepStartTime
|
|
138
|
+
};
|
|
139
|
+
}
|
|
140
|
+
logger.info("Running typecheck...");
|
|
141
|
+
try {
|
|
142
|
+
await runScript("typecheck", worktreePath, [], { quiet: true });
|
|
143
|
+
logger.success("Typecheck passed");
|
|
144
|
+
return {
|
|
145
|
+
step: "typecheck",
|
|
146
|
+
passed: true,
|
|
147
|
+
skipped: false,
|
|
148
|
+
duration: Date.now() - stepStartTime
|
|
149
|
+
};
|
|
150
|
+
} catch {
|
|
151
|
+
const fixed = await this.attemptClaudeFix(
|
|
152
|
+
"typecheck",
|
|
153
|
+
worktreePath,
|
|
154
|
+
packageManager
|
|
155
|
+
);
|
|
156
|
+
if (fixed) {
|
|
157
|
+
return {
|
|
158
|
+
step: "typecheck",
|
|
159
|
+
passed: true,
|
|
160
|
+
skipped: false,
|
|
161
|
+
duration: Date.now() - stepStartTime
|
|
162
|
+
};
|
|
163
|
+
}
|
|
164
|
+
const runCommand = packageManager === "npm" ? "npm run typecheck" : `${packageManager} typecheck`;
|
|
165
|
+
throw new Error(
|
|
166
|
+
`Error: Typecheck failed.
|
|
167
|
+
Fix type errors before merging.
|
|
168
|
+
|
|
169
|
+
Run '${runCommand}' to see detailed errors.`
|
|
170
|
+
);
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
/**
|
|
174
|
+
* Run lint validation
|
|
175
|
+
*/
|
|
176
|
+
async runLint(worktreePath, dryRun) {
|
|
177
|
+
const stepStartTime = Date.now();
|
|
178
|
+
try {
|
|
179
|
+
const pkgJson = await readPackageJson(worktreePath);
|
|
180
|
+
const hasLintScript = hasScript(pkgJson, "lint");
|
|
181
|
+
if (!hasLintScript) {
|
|
182
|
+
logger.debug("Skipping lint - no lint script found");
|
|
183
|
+
return {
|
|
184
|
+
step: "lint",
|
|
185
|
+
passed: true,
|
|
186
|
+
skipped: true,
|
|
187
|
+
duration: Date.now() - stepStartTime
|
|
188
|
+
};
|
|
189
|
+
}
|
|
190
|
+
} catch (error) {
|
|
191
|
+
if (error instanceof Error && error.message.includes("package.json not found")) {
|
|
192
|
+
logger.debug("Skipping lint - no package.json found (non-Node.js project)");
|
|
193
|
+
return {
|
|
194
|
+
step: "lint",
|
|
195
|
+
passed: true,
|
|
196
|
+
skipped: true,
|
|
197
|
+
duration: Date.now() - stepStartTime
|
|
198
|
+
};
|
|
199
|
+
}
|
|
200
|
+
throw error;
|
|
201
|
+
}
|
|
202
|
+
const packageManager = await detectPackageManager(worktreePath);
|
|
203
|
+
if (dryRun) {
|
|
204
|
+
const command = packageManager === "npm" ? "npm run lint" : `${packageManager} lint`;
|
|
205
|
+
logger.info(`[DRY RUN] Would run: ${command}`);
|
|
206
|
+
return {
|
|
207
|
+
step: "lint",
|
|
208
|
+
passed: true,
|
|
209
|
+
skipped: false,
|
|
210
|
+
duration: Date.now() - stepStartTime
|
|
211
|
+
};
|
|
212
|
+
}
|
|
213
|
+
logger.info("Running lint...");
|
|
214
|
+
try {
|
|
215
|
+
await runScript("lint", worktreePath, [], { quiet: true });
|
|
216
|
+
logger.success("Linting passed");
|
|
217
|
+
return {
|
|
218
|
+
step: "lint",
|
|
219
|
+
passed: true,
|
|
220
|
+
skipped: false,
|
|
221
|
+
duration: Date.now() - stepStartTime
|
|
222
|
+
};
|
|
223
|
+
} catch {
|
|
224
|
+
const fixed = await this.attemptClaudeFix(
|
|
225
|
+
"lint",
|
|
226
|
+
worktreePath,
|
|
227
|
+
packageManager
|
|
228
|
+
);
|
|
229
|
+
if (fixed) {
|
|
230
|
+
return {
|
|
231
|
+
step: "lint",
|
|
232
|
+
passed: true,
|
|
233
|
+
skipped: false,
|
|
234
|
+
duration: Date.now() - stepStartTime
|
|
235
|
+
};
|
|
236
|
+
}
|
|
237
|
+
const runCommand = packageManager === "npm" ? "npm run lint" : `${packageManager} lint`;
|
|
238
|
+
throw new Error(
|
|
239
|
+
`Error: Linting failed.
|
|
240
|
+
Fix linting errors before merging.
|
|
241
|
+
|
|
242
|
+
Run '${runCommand}' to see detailed errors.`
|
|
243
|
+
);
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
/**
|
|
247
|
+
* Run test validation
|
|
248
|
+
*/
|
|
249
|
+
async runTests(worktreePath, dryRun) {
|
|
250
|
+
const stepStartTime = Date.now();
|
|
251
|
+
try {
|
|
252
|
+
const pkgJson = await readPackageJson(worktreePath);
|
|
253
|
+
const hasTestScript = hasScript(pkgJson, "test");
|
|
254
|
+
if (!hasTestScript) {
|
|
255
|
+
logger.debug("Skipping tests - no test script found");
|
|
256
|
+
return {
|
|
257
|
+
step: "test",
|
|
258
|
+
passed: true,
|
|
259
|
+
skipped: true,
|
|
260
|
+
duration: Date.now() - stepStartTime
|
|
261
|
+
};
|
|
262
|
+
}
|
|
263
|
+
} catch (error) {
|
|
264
|
+
if (error instanceof Error && error.message.includes("package.json not found")) {
|
|
265
|
+
logger.debug("Skipping tests - no package.json found (non-Node.js project)");
|
|
266
|
+
return {
|
|
267
|
+
step: "test",
|
|
268
|
+
passed: true,
|
|
269
|
+
skipped: true,
|
|
270
|
+
duration: Date.now() - stepStartTime
|
|
271
|
+
};
|
|
272
|
+
}
|
|
273
|
+
throw error;
|
|
274
|
+
}
|
|
275
|
+
const packageManager = await detectPackageManager(worktreePath);
|
|
276
|
+
if (dryRun) {
|
|
277
|
+
const command = packageManager === "npm" ? "npm run test" : `${packageManager} test`;
|
|
278
|
+
logger.info(`[DRY RUN] Would run: ${command}`);
|
|
279
|
+
return {
|
|
280
|
+
step: "test",
|
|
281
|
+
passed: true,
|
|
282
|
+
skipped: false,
|
|
283
|
+
duration: Date.now() - stepStartTime
|
|
284
|
+
};
|
|
285
|
+
}
|
|
286
|
+
logger.info("Running tests...");
|
|
287
|
+
try {
|
|
288
|
+
await runScript("test", worktreePath, [], { quiet: true });
|
|
289
|
+
logger.success("Tests passed");
|
|
290
|
+
return {
|
|
291
|
+
step: "test",
|
|
292
|
+
passed: true,
|
|
293
|
+
skipped: false,
|
|
294
|
+
duration: Date.now() - stepStartTime
|
|
295
|
+
};
|
|
296
|
+
} catch {
|
|
297
|
+
const fixed = await this.attemptClaudeFix(
|
|
298
|
+
"test",
|
|
299
|
+
worktreePath,
|
|
300
|
+
packageManager
|
|
301
|
+
);
|
|
302
|
+
if (fixed) {
|
|
303
|
+
return {
|
|
304
|
+
step: "test",
|
|
305
|
+
passed: true,
|
|
306
|
+
skipped: false,
|
|
307
|
+
duration: Date.now() - stepStartTime
|
|
308
|
+
};
|
|
309
|
+
}
|
|
310
|
+
const runCommand = packageManager === "npm" ? "npm run test" : `${packageManager} test`;
|
|
311
|
+
throw new Error(
|
|
312
|
+
`Error: Tests failed.
|
|
313
|
+
Fix test failures before merging.
|
|
314
|
+
|
|
315
|
+
Run '${runCommand}' to see detailed errors.`
|
|
316
|
+
);
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
/**
|
|
320
|
+
* Attempt to fix validation errors using Claude
|
|
321
|
+
* Pattern based on MergeManager.attemptClaudeConflictResolution
|
|
322
|
+
*
|
|
323
|
+
* @param validationType - Type of validation that failed ('typecheck' | 'lint' | 'test')
|
|
324
|
+
* @param worktreePath - Path to the worktree
|
|
325
|
+
* @param packageManager - Detected package manager
|
|
326
|
+
* @returns true if Claude fixed the issue, false otherwise
|
|
327
|
+
*/
|
|
328
|
+
async attemptClaudeFix(validationType, worktreePath, packageManager) {
|
|
329
|
+
const isClaudeAvailable = await detectClaudeCli();
|
|
330
|
+
if (!isClaudeAvailable) {
|
|
331
|
+
logger.debug("Claude CLI not available, skipping auto-fix");
|
|
332
|
+
return false;
|
|
333
|
+
}
|
|
334
|
+
const validationCommand = this.getValidationCommand(validationType, packageManager);
|
|
335
|
+
const prompt = this.getClaudePrompt(validationType, validationCommand);
|
|
336
|
+
const validationTypeCapitalized = validationType.charAt(0).toUpperCase() + validationType.slice(1);
|
|
337
|
+
logger.info(`Launching Claude to help fix ${validationTypeCapitalized} errors...`);
|
|
338
|
+
try {
|
|
339
|
+
await launchClaude(prompt, {
|
|
340
|
+
addDir: worktreePath,
|
|
341
|
+
headless: false,
|
|
342
|
+
// Interactive mode
|
|
343
|
+
permissionMode: "acceptEdits",
|
|
344
|
+
// Auto-accept edits
|
|
345
|
+
model: "sonnet"
|
|
346
|
+
// Use Sonnet model
|
|
347
|
+
});
|
|
348
|
+
logger.info(`Re-running ${validationTypeCapitalized} after Claude's fixes...`);
|
|
349
|
+
try {
|
|
350
|
+
await runScript(validationType, worktreePath, [], { quiet: true });
|
|
351
|
+
logger.success(`${validationTypeCapitalized} passed after Claude auto-fix`);
|
|
352
|
+
return true;
|
|
353
|
+
} catch {
|
|
354
|
+
logger.warn(`${validationTypeCapitalized} still failing after Claude's help`);
|
|
355
|
+
return false;
|
|
356
|
+
}
|
|
357
|
+
} catch (error) {
|
|
358
|
+
logger.warn("Claude auto-fix failed", {
|
|
359
|
+
error: error instanceof Error ? error.message : String(error)
|
|
360
|
+
});
|
|
361
|
+
return false;
|
|
362
|
+
}
|
|
363
|
+
}
|
|
364
|
+
/**
|
|
365
|
+
* Get validation command string for prompts
|
|
366
|
+
*/
|
|
367
|
+
getValidationCommand(validationType, packageManager) {
|
|
368
|
+
if (packageManager === "npm") {
|
|
369
|
+
return `npm run ${validationType}`;
|
|
370
|
+
}
|
|
371
|
+
return `${packageManager} ${validationType}`;
|
|
372
|
+
}
|
|
373
|
+
/**
|
|
374
|
+
* Get Claude prompt for specific validation type
|
|
375
|
+
* Matches bash script prompts exactly
|
|
376
|
+
*/
|
|
377
|
+
getClaudePrompt(validationType, validationCommand) {
|
|
378
|
+
switch (validationType) {
|
|
379
|
+
case "typecheck":
|
|
380
|
+
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 to continue the validation process.`;
|
|
381
|
+
case "lint":
|
|
382
|
+
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 to continue the validation process.`;
|
|
383
|
+
case "test":
|
|
384
|
+
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 to continue the validation process.`;
|
|
385
|
+
}
|
|
386
|
+
}
|
|
387
|
+
};
|
|
388
|
+
|
|
389
|
+
// src/lib/CommitManager.ts
|
|
390
|
+
var CommitManager = class {
|
|
391
|
+
/**
|
|
392
|
+
* Detect uncommitted changes in a worktree
|
|
393
|
+
* Parses git status --porcelain output into structured GitStatus
|
|
394
|
+
*/
|
|
395
|
+
async detectUncommittedChanges(worktreePath) {
|
|
396
|
+
const porcelainOutput = await executeGitCommand(["status", "--porcelain"], {
|
|
397
|
+
cwd: worktreePath
|
|
398
|
+
});
|
|
399
|
+
const { stagedFiles, unstagedFiles } = this.parseGitStatus(porcelainOutput);
|
|
400
|
+
const currentBranch = await executeGitCommand(["branch", "--show-current"], {
|
|
401
|
+
cwd: worktreePath
|
|
402
|
+
});
|
|
403
|
+
return {
|
|
404
|
+
hasUncommittedChanges: stagedFiles.length > 0 || unstagedFiles.length > 0,
|
|
405
|
+
unstagedFiles,
|
|
406
|
+
stagedFiles,
|
|
407
|
+
currentBranch: currentBranch.trim(),
|
|
408
|
+
// Defer these to future enhancement
|
|
409
|
+
isAheadOfRemote: false,
|
|
410
|
+
isBehindRemote: false
|
|
411
|
+
};
|
|
412
|
+
}
|
|
413
|
+
/**
|
|
414
|
+
* Stage all changes and commit with Claude-generated or simple message
|
|
415
|
+
* Tries Claude first, falls back to simple message if Claude unavailable or fails
|
|
416
|
+
*/
|
|
417
|
+
async commitChanges(worktreePath, options) {
|
|
418
|
+
if (options.dryRun) {
|
|
419
|
+
logger.info("[DRY RUN] Would run: git add -A");
|
|
420
|
+
logger.info("[DRY RUN] Would generate commit message with Claude (if available)");
|
|
421
|
+
const fallbackMessage = this.generateFallbackMessage(options);
|
|
422
|
+
const verifyFlag = options.skipVerify ? " --no-verify" : "";
|
|
423
|
+
logger.info(`[DRY RUN] Would commit with message${verifyFlag}: ${fallbackMessage}`);
|
|
424
|
+
return;
|
|
425
|
+
}
|
|
426
|
+
await executeGitCommand(["add", "-A"], { cwd: worktreePath });
|
|
427
|
+
let message = null;
|
|
428
|
+
if (!options.message) {
|
|
429
|
+
try {
|
|
430
|
+
message = await this.generateClaudeCommitMessage(worktreePath, options.issueNumber);
|
|
431
|
+
} catch (error) {
|
|
432
|
+
logger.debug("Claude commit message generation failed, using fallback", { error });
|
|
433
|
+
}
|
|
434
|
+
}
|
|
435
|
+
message ??= this.generateFallbackMessage(options);
|
|
436
|
+
if (options.skipVerify) {
|
|
437
|
+
logger.warn("\u26A0\uFE0F Skipping pre-commit hooks (--no-verify configured in settings)");
|
|
438
|
+
}
|
|
439
|
+
try {
|
|
440
|
+
if (options.noReview || options.message) {
|
|
441
|
+
const commitArgs = ["commit", "-m", message];
|
|
442
|
+
if (options.skipVerify) {
|
|
443
|
+
commitArgs.push("--no-verify");
|
|
444
|
+
}
|
|
445
|
+
await executeGitCommand(commitArgs, { cwd: worktreePath });
|
|
446
|
+
} else {
|
|
447
|
+
logger.info("Opening git editor for commit message review...");
|
|
448
|
+
const commitArgs = ["commit", "-e", "-m", message];
|
|
449
|
+
if (options.skipVerify) {
|
|
450
|
+
commitArgs.push("--no-verify");
|
|
451
|
+
}
|
|
452
|
+
await executeGitCommand(commitArgs, {
|
|
453
|
+
cwd: worktreePath,
|
|
454
|
+
stdio: "inherit",
|
|
455
|
+
timeout: 3e5
|
|
456
|
+
// 5 minutes for interactive editing
|
|
457
|
+
});
|
|
458
|
+
}
|
|
459
|
+
} catch (error) {
|
|
460
|
+
if (error instanceof Error && error.message.includes("nothing to commit")) {
|
|
461
|
+
logger.info("No changes to commit");
|
|
462
|
+
return;
|
|
463
|
+
}
|
|
464
|
+
throw error;
|
|
465
|
+
}
|
|
466
|
+
}
|
|
467
|
+
/**
|
|
468
|
+
* Generate simple fallback commit message when Claude unavailable
|
|
469
|
+
* Used as fallback for Claude-powered commit messages
|
|
470
|
+
*/
|
|
471
|
+
generateFallbackMessage(options) {
|
|
472
|
+
if (options.message) {
|
|
473
|
+
return options.message;
|
|
474
|
+
}
|
|
475
|
+
if (options.issueNumber) {
|
|
476
|
+
return `WIP: Auto-commit for issue #${options.issueNumber}
|
|
477
|
+
|
|
478
|
+
Fixes #${options.issueNumber}`;
|
|
479
|
+
} else {
|
|
480
|
+
return "WIP: Auto-commit uncommitted changes";
|
|
481
|
+
}
|
|
482
|
+
}
|
|
483
|
+
/**
|
|
484
|
+
* Parse git status --porcelain output
|
|
485
|
+
* Format: "XY filename" where X=index, Y=worktree
|
|
486
|
+
* Examples:
|
|
487
|
+
* "M file.ts" - staged modification
|
|
488
|
+
* " M file.ts" - unstaged modification
|
|
489
|
+
* "MM file.ts" - both staged and unstaged
|
|
490
|
+
* "?? file.ts" - untracked
|
|
491
|
+
*/
|
|
492
|
+
parseGitStatus(porcelainOutput) {
|
|
493
|
+
const stagedFiles = [];
|
|
494
|
+
const unstagedFiles = [];
|
|
495
|
+
if (!porcelainOutput.trim()) {
|
|
496
|
+
return { stagedFiles, unstagedFiles };
|
|
497
|
+
}
|
|
498
|
+
const lines = porcelainOutput.split("\n").filter((line) => line.trim());
|
|
499
|
+
for (const line of lines) {
|
|
500
|
+
if (line.length < 3) continue;
|
|
501
|
+
const indexStatus = line[0];
|
|
502
|
+
const worktreeStatus = line[1];
|
|
503
|
+
const filename = line.substring(3);
|
|
504
|
+
if (indexStatus !== " " && indexStatus !== "?") {
|
|
505
|
+
stagedFiles.push(filename);
|
|
506
|
+
}
|
|
507
|
+
if (worktreeStatus !== " " || line.startsWith("??")) {
|
|
508
|
+
unstagedFiles.push(filename);
|
|
509
|
+
}
|
|
510
|
+
}
|
|
511
|
+
return { stagedFiles, unstagedFiles };
|
|
512
|
+
}
|
|
513
|
+
/**
|
|
514
|
+
* Generate commit message using Claude AI
|
|
515
|
+
* Claude examines the git repository directly via --add-dir option
|
|
516
|
+
* Returns null if Claude unavailable or fails validation
|
|
517
|
+
*/
|
|
518
|
+
async generateClaudeCommitMessage(worktreePath, issueNumber) {
|
|
519
|
+
const startTime = Date.now();
|
|
520
|
+
logger.info("Starting Claude commit message generation...", {
|
|
521
|
+
worktreePath: worktreePath.split("/").pop(),
|
|
522
|
+
// Just show the folder name for privacy
|
|
523
|
+
issueNumber
|
|
524
|
+
});
|
|
525
|
+
logger.debug("Checking Claude CLI availability...");
|
|
526
|
+
const isClaudeAvailable = await detectClaudeCli();
|
|
527
|
+
if (!isClaudeAvailable) {
|
|
528
|
+
logger.info("Claude CLI not available, skipping Claude commit message generation");
|
|
529
|
+
return null;
|
|
530
|
+
}
|
|
531
|
+
logger.debug("Claude CLI is available");
|
|
532
|
+
logger.debug("Building commit message prompt...");
|
|
533
|
+
const prompt = this.buildCommitMessagePrompt(issueNumber);
|
|
534
|
+
logger.debug("Prompt built", { promptLength: prompt.length });
|
|
535
|
+
logger.debug("Claude prompt content:", {
|
|
536
|
+
prompt,
|
|
537
|
+
truncatedPreview: prompt.substring(0, 500) + (prompt.length > 500 ? "...[truncated]" : "")
|
|
538
|
+
});
|
|
539
|
+
try {
|
|
540
|
+
logger.info("Calling Claude CLI for commit message generation...");
|
|
541
|
+
const claudeStartTime = Date.now();
|
|
542
|
+
const claudeOptions = {
|
|
543
|
+
headless: true,
|
|
544
|
+
addDir: worktreePath,
|
|
545
|
+
model: "claude-haiku-4-5-20251001",
|
|
546
|
+
// Fast, cost-effective model
|
|
547
|
+
timeout: 12e4
|
|
548
|
+
// 120 second timeout
|
|
549
|
+
};
|
|
550
|
+
logger.debug("Claude CLI call parameters:", {
|
|
551
|
+
options: claudeOptions,
|
|
552
|
+
worktreePathForAnalysis: worktreePath,
|
|
553
|
+
addDirContents: "Will include entire worktree directory for analysis"
|
|
554
|
+
});
|
|
555
|
+
const result = await launchClaude(prompt, claudeOptions);
|
|
556
|
+
const claudeDuration = Date.now() - claudeStartTime;
|
|
557
|
+
logger.debug("Claude API call completed", { duration: `${claudeDuration}ms` });
|
|
558
|
+
if (typeof result !== "string") {
|
|
559
|
+
logger.warn("Claude returned non-string result", { resultType: typeof result });
|
|
560
|
+
return null;
|
|
561
|
+
}
|
|
562
|
+
logger.debug("Raw Claude output received", {
|
|
563
|
+
outputLength: result.length,
|
|
564
|
+
preview: result.substring(0, 200) + (result.length > 200 ? "..." : "")
|
|
565
|
+
});
|
|
566
|
+
logger.debug("Sanitizing Claude output...");
|
|
567
|
+
const sanitized = this.sanitizeClaudeOutput(result);
|
|
568
|
+
logger.debug("Output sanitized", {
|
|
569
|
+
originalLength: result.length,
|
|
570
|
+
sanitizedLength: sanitized.length,
|
|
571
|
+
sanitized: sanitized.substring(0, 200) + (sanitized.length > 200 ? "..." : "")
|
|
572
|
+
});
|
|
573
|
+
if (!sanitized) {
|
|
574
|
+
logger.warn("Claude returned empty message after sanitization");
|
|
575
|
+
return null;
|
|
576
|
+
}
|
|
577
|
+
let finalMessage = sanitized;
|
|
578
|
+
if (issueNumber) {
|
|
579
|
+
if (!finalMessage.includes(`Fixes #${issueNumber}`)) {
|
|
580
|
+
finalMessage = `${finalMessage}
|
|
581
|
+
|
|
582
|
+
Fixes #${issueNumber}`;
|
|
583
|
+
logger.debug(`Added "Fixes #${issueNumber}" trailer to commit message`);
|
|
584
|
+
} else {
|
|
585
|
+
logger.debug(`"Fixes #${issueNumber}" already present in commit message`);
|
|
586
|
+
}
|
|
587
|
+
}
|
|
588
|
+
const totalDuration = Date.now() - startTime;
|
|
589
|
+
logger.info("Claude commit message generated successfully", {
|
|
590
|
+
message: finalMessage,
|
|
591
|
+
totalDuration: `${totalDuration}ms`,
|
|
592
|
+
claudeApiDuration: `${claudeDuration}ms`
|
|
593
|
+
});
|
|
594
|
+
return finalMessage;
|
|
595
|
+
} catch (error) {
|
|
596
|
+
const totalDuration = Date.now() - startTime;
|
|
597
|
+
const errorMessage = error instanceof Error ? error.message : "Unknown error";
|
|
598
|
+
if (errorMessage.includes("timed out") || errorMessage.includes("timeout")) {
|
|
599
|
+
logger.warn("Claude commit message generation timed out after 45 seconds", {
|
|
600
|
+
totalDuration: `${totalDuration}ms`,
|
|
601
|
+
worktreePath: worktreePath.split("/").pop()
|
|
602
|
+
});
|
|
603
|
+
} else {
|
|
604
|
+
logger.warn("Failed to generate commit message with Claude", {
|
|
605
|
+
error: errorMessage,
|
|
606
|
+
totalDuration: `${totalDuration}ms`,
|
|
607
|
+
worktreePath: worktreePath.split("/").pop()
|
|
608
|
+
});
|
|
609
|
+
}
|
|
610
|
+
return null;
|
|
611
|
+
}
|
|
612
|
+
}
|
|
613
|
+
/**
|
|
614
|
+
* Build structured XML prompt for commit message generation
|
|
615
|
+
* Uses XML format for clear task definition and output expectations
|
|
616
|
+
*/
|
|
617
|
+
buildCommitMessagePrompt(issueNumber) {
|
|
618
|
+
const issueContext = issueNumber ? `
|
|
619
|
+
<IssueContext>
|
|
620
|
+
This commit is associated with GitHub issue #${issueNumber}.
|
|
621
|
+
If the changes appear to resolve the issue, include "Fixes #${issueNumber}" at the end of the first line of commit message.
|
|
622
|
+
</IssueContext>` : "";
|
|
623
|
+
return `<Task>
|
|
624
|
+
You are a software engineer writing a commit message for this repository.
|
|
625
|
+
Examine the staged changes in the git repository and generate a concise, meaningful commit message.
|
|
626
|
+
</Task>
|
|
627
|
+
|
|
628
|
+
<Requirements>
|
|
629
|
+
<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.
|
|
630
|
+
|
|
631
|
+
Add 2 newlines, then add a bullet-point form description of the changes made, each change on a new line.</Format>
|
|
632
|
+
<Mood>Use imperative mood (e.g., "Add feature" not "Added feature")</Mood>
|
|
633
|
+
<Focus>Be specific about what was changed and why</Focus>
|
|
634
|
+
<Conciseness>Keep message under 72 characters for subject line when possible</Conciseness>
|
|
635
|
+
<NoMeta>CRITICAL: Do NOT include ANY explanatory text, analysis, or meta-commentary. Output ONLY the raw commit message.</NoMeta>
|
|
636
|
+
<Examples>
|
|
637
|
+
Good: "Add user authentication with JWT tokens. Fixes #42
|
|
638
|
+
|
|
639
|
+
- Implement login and registration endpoints
|
|
640
|
+
- Secure routes with JWT middleware
|
|
641
|
+
- Update user model to store hashed passwords"
|
|
642
|
+
Good: "Fix navigation bug in sidebar menu."
|
|
643
|
+
Bad: "Based on the changes, I'll create: Add user authentication"
|
|
644
|
+
Bad: "Looking at the files, this commit should be: Fix navigation bug"
|
|
645
|
+
</Examples>
|
|
646
|
+
${issueContext}
|
|
647
|
+
</Requirements>
|
|
648
|
+
|
|
649
|
+
<Output>
|
|
650
|
+
IMPORTANT: Your entire response will be used directly as the git commit message.
|
|
651
|
+
Do not include any explanatory text before or after the commit message.
|
|
652
|
+
Start your response immediately with the commit message text.
|
|
653
|
+
</Output>`;
|
|
654
|
+
}
|
|
655
|
+
/**
|
|
656
|
+
* Sanitize Claude output to remove meta-commentary and clean formatting
|
|
657
|
+
* Handles cases where Claude includes explanatory text despite instructions
|
|
658
|
+
*/
|
|
659
|
+
sanitizeClaudeOutput(rawOutput) {
|
|
660
|
+
let cleaned = rawOutput.trim();
|
|
661
|
+
const metaPatterns = [
|
|
662
|
+
/^.*?based on.*?changes.*?:/i,
|
|
663
|
+
/^.*?looking at.*?files.*?:/i,
|
|
664
|
+
/^.*?examining.*?:/i,
|
|
665
|
+
/^.*?analyzing.*?:/i,
|
|
666
|
+
/^.*?i'll.*?generate.*?:/i,
|
|
667
|
+
/^.*?let me.*?:/i,
|
|
668
|
+
/^.*?the commit message.*?should be.*?:/i,
|
|
669
|
+
/^.*?here.*?is.*?commit.*?message.*?:/i
|
|
670
|
+
];
|
|
671
|
+
for (const pattern of metaPatterns) {
|
|
672
|
+
cleaned = cleaned.replace(pattern, "").trim();
|
|
673
|
+
}
|
|
674
|
+
if (cleaned.includes(":")) {
|
|
675
|
+
const colonIndex = cleaned.indexOf(":");
|
|
676
|
+
const beforeColon = cleaned.substring(0, colonIndex).trim().toLowerCase();
|
|
677
|
+
const metaIndicators = [
|
|
678
|
+
"here is the commit message",
|
|
679
|
+
"commit message",
|
|
680
|
+
"here is",
|
|
681
|
+
"the message should be",
|
|
682
|
+
"i suggest",
|
|
683
|
+
"my suggestion"
|
|
684
|
+
];
|
|
685
|
+
const isMetaCommentary = metaIndicators.some((indicator) => beforeColon.includes(indicator));
|
|
686
|
+
if (isMetaCommentary) {
|
|
687
|
+
const afterColon = cleaned.substring(colonIndex + 1).trim();
|
|
688
|
+
if (afterColon && afterColon.length > 10) {
|
|
689
|
+
cleaned = afterColon;
|
|
690
|
+
}
|
|
691
|
+
}
|
|
692
|
+
}
|
|
693
|
+
if (cleaned.startsWith('"') && cleaned.endsWith('"') || cleaned.startsWith("'") && cleaned.endsWith("'")) {
|
|
694
|
+
cleaned = cleaned.slice(1, -1).trim();
|
|
695
|
+
}
|
|
696
|
+
return cleaned;
|
|
697
|
+
}
|
|
698
|
+
};
|
|
699
|
+
|
|
700
|
+
// src/lib/MergeManager.ts
|
|
701
|
+
var MergeManager = class {
|
|
702
|
+
constructor(settingsManager) {
|
|
703
|
+
this.settingsManager = settingsManager ?? new SettingsManager();
|
|
704
|
+
}
|
|
705
|
+
/**
|
|
706
|
+
* Get the main branch name from settings (defaults to 'main')
|
|
707
|
+
* @private
|
|
708
|
+
*/
|
|
709
|
+
async getMainBranch() {
|
|
710
|
+
const settings = await this.settingsManager.loadSettings();
|
|
711
|
+
return settings.mainBranch ?? "main";
|
|
712
|
+
}
|
|
713
|
+
/**
|
|
714
|
+
* Rebase current branch on main with fail-fast on conflicts
|
|
715
|
+
* Ports bash/merge-and-clean.sh lines 781-913
|
|
716
|
+
*
|
|
717
|
+
* @param worktreePath - Path to the worktree
|
|
718
|
+
* @param options - Merge options (dryRun, force)
|
|
719
|
+
* @throws Error if main branch doesn't exist, uncommitted changes exist, or conflicts occur
|
|
720
|
+
*/
|
|
721
|
+
async rebaseOnMain(worktreePath, options = {}) {
|
|
722
|
+
const { dryRun = false, force = false } = options;
|
|
723
|
+
const mainBranch = await this.getMainBranch();
|
|
724
|
+
logger.info(`Starting rebase on ${mainBranch} branch...`);
|
|
725
|
+
try {
|
|
726
|
+
await executeGitCommand(["show-ref", "--verify", "--quiet", `refs/heads/${mainBranch}`], {
|
|
727
|
+
cwd: worktreePath
|
|
728
|
+
});
|
|
729
|
+
} catch {
|
|
730
|
+
throw new Error(
|
|
731
|
+
`Main branch "${mainBranch}" does not exist. Cannot rebase.
|
|
732
|
+
Ensure the repository has a "${mainBranch}" branch or create it first.`
|
|
733
|
+
);
|
|
734
|
+
}
|
|
735
|
+
const statusOutput = await executeGitCommand(["status", "--porcelain"], {
|
|
736
|
+
cwd: worktreePath
|
|
737
|
+
});
|
|
738
|
+
if (statusOutput.trim()) {
|
|
739
|
+
throw new Error(
|
|
740
|
+
"Uncommitted changes detected. Please commit or stash changes before rebasing.\nRun: git status to see uncommitted changes\nOr: il finish will automatically commit them for you"
|
|
741
|
+
);
|
|
742
|
+
}
|
|
743
|
+
const mergeBase = await executeGitCommand(["merge-base", mainBranch, "HEAD"], {
|
|
744
|
+
cwd: worktreePath
|
|
745
|
+
});
|
|
746
|
+
const mainHead = await executeGitCommand(["rev-parse", mainBranch], {
|
|
747
|
+
cwd: worktreePath
|
|
748
|
+
});
|
|
749
|
+
const mergeBaseTrimmed = mergeBase.trim();
|
|
750
|
+
const mainHeadTrimmed = mainHead.trim();
|
|
751
|
+
if (mergeBaseTrimmed === mainHeadTrimmed) {
|
|
752
|
+
logger.success(`Branch is already up to date with ${mainBranch}. No rebase needed.`);
|
|
753
|
+
return;
|
|
754
|
+
}
|
|
755
|
+
const commitsOutput = await executeGitCommand(["log", "--oneline", `${mainBranch}..HEAD`], {
|
|
756
|
+
cwd: worktreePath
|
|
757
|
+
});
|
|
758
|
+
const commits = commitsOutput.trim();
|
|
759
|
+
const commitLines = commits ? commits.split("\n") : [];
|
|
760
|
+
if (commits) {
|
|
761
|
+
logger.info(`Found ${commitLines.length} commit(s) to rebase:`);
|
|
762
|
+
commitLines.forEach((commit) => logger.info(` ${commit}`));
|
|
763
|
+
} else {
|
|
764
|
+
logger.info(`${mainBranch} branch has moved forward. Rebasing to update branch...`);
|
|
765
|
+
}
|
|
766
|
+
if (!force && !dryRun) {
|
|
767
|
+
logger.info("Proceeding with rebase... (use --force to skip confirmations)");
|
|
768
|
+
}
|
|
769
|
+
if (dryRun) {
|
|
770
|
+
logger.info(`[DRY RUN] Would execute: git rebase ${mainBranch}`);
|
|
771
|
+
if (commitLines.length > 0) {
|
|
772
|
+
logger.info(`[DRY RUN] This would rebase ${commitLines.length} commit(s)`);
|
|
773
|
+
}
|
|
774
|
+
return;
|
|
775
|
+
}
|
|
776
|
+
try {
|
|
777
|
+
await executeGitCommand(["rebase", mainBranch], { cwd: worktreePath });
|
|
778
|
+
logger.success("Rebase completed successfully!");
|
|
779
|
+
} catch (error) {
|
|
780
|
+
const conflictedFiles = await this.detectConflictedFiles(worktreePath);
|
|
781
|
+
if (conflictedFiles.length > 0) {
|
|
782
|
+
logger.info("Merge conflicts detected, attempting Claude-assisted resolution...");
|
|
783
|
+
const resolved = await this.attemptClaudeConflictResolution(
|
|
784
|
+
worktreePath,
|
|
785
|
+
conflictedFiles
|
|
786
|
+
);
|
|
787
|
+
if (resolved) {
|
|
788
|
+
logger.success("Conflicts resolved with Claude assistance, rebase completed");
|
|
789
|
+
return;
|
|
790
|
+
}
|
|
791
|
+
const conflictError = this.formatConflictError(conflictedFiles);
|
|
792
|
+
throw new Error(conflictError);
|
|
793
|
+
}
|
|
794
|
+
throw new Error(
|
|
795
|
+
`Rebase failed: ${error instanceof Error ? error.message : String(error)}
|
|
796
|
+
Run: git status for more details
|
|
797
|
+
Or: git rebase --abort to cancel the rebase`
|
|
798
|
+
);
|
|
799
|
+
}
|
|
800
|
+
}
|
|
801
|
+
/**
|
|
802
|
+
* Validate that fast-forward merge is possible
|
|
803
|
+
* Ports bash/merge-and-clean.sh lines 957-968
|
|
804
|
+
*
|
|
805
|
+
* @param branchName - Name of the branch to merge
|
|
806
|
+
* @param mainWorktreePath - Path where main branch is checked out
|
|
807
|
+
* @throws Error if fast-forward is not possible
|
|
808
|
+
*/
|
|
809
|
+
async validateFastForwardPossible(branchName, mainWorktreePath) {
|
|
810
|
+
const mainBranch = await this.getMainBranch();
|
|
811
|
+
const mergeBase = await executeGitCommand(["merge-base", mainBranch, branchName], {
|
|
812
|
+
cwd: mainWorktreePath
|
|
813
|
+
});
|
|
814
|
+
const mainHead = await executeGitCommand(["rev-parse", mainBranch], {
|
|
815
|
+
cwd: mainWorktreePath
|
|
816
|
+
});
|
|
817
|
+
const mergeBaseTrimmed = mergeBase.trim();
|
|
818
|
+
const mainHeadTrimmed = mainHead.trim();
|
|
819
|
+
if (mergeBaseTrimmed !== mainHeadTrimmed) {
|
|
820
|
+
throw new Error(
|
|
821
|
+
`Cannot perform fast-forward merge.
|
|
822
|
+
The ${mainBranch} branch has moved forward since this branch was created.
|
|
823
|
+
Merge base: ${mergeBaseTrimmed}
|
|
824
|
+
Main HEAD: ${mainHeadTrimmed}
|
|
825
|
+
|
|
826
|
+
To fix this:
|
|
827
|
+
1. Rebase the branch on ${mainBranch}: git rebase ${mainBranch}
|
|
828
|
+
2. Or use: il finish to automatically rebase and merge
|
|
829
|
+
`
|
|
830
|
+
);
|
|
831
|
+
}
|
|
832
|
+
}
|
|
833
|
+
/**
|
|
834
|
+
* Perform fast-forward only merge
|
|
835
|
+
* Ports bash/merge-and-clean.sh lines 938-994
|
|
836
|
+
*
|
|
837
|
+
* @param branchName - Name of the branch to merge
|
|
838
|
+
* @param worktreePath - Path to the worktree
|
|
839
|
+
* @param options - Merge options (dryRun, force)
|
|
840
|
+
* @throws Error if checkout, validation, or merge fails
|
|
841
|
+
*/
|
|
842
|
+
async performFastForwardMerge(branchName, worktreePath, options = {}) {
|
|
843
|
+
const { dryRun = false, force = false } = options;
|
|
844
|
+
const mainBranch = await this.getMainBranch();
|
|
845
|
+
logger.info("Starting fast-forward merge...");
|
|
846
|
+
const mainWorktreePath = options.repoRoot ?? await findMainWorktreePathWithSettings(worktreePath, this.settingsManager);
|
|
847
|
+
logger.debug(`Using ${mainBranch} branch location: ${mainWorktreePath}`);
|
|
848
|
+
const currentBranch = await executeGitCommand(["branch", "--show-current"], {
|
|
849
|
+
cwd: mainWorktreePath
|
|
850
|
+
});
|
|
851
|
+
if (currentBranch.trim() !== mainBranch) {
|
|
852
|
+
throw new Error(
|
|
853
|
+
`Expected ${mainBranch} branch but found: ${currentBranch.trim()}
|
|
854
|
+
At location: ${mainWorktreePath}
|
|
855
|
+
This indicates the main worktree detection failed.`
|
|
856
|
+
);
|
|
857
|
+
}
|
|
858
|
+
await this.validateFastForwardPossible(branchName, mainWorktreePath);
|
|
859
|
+
const commitsOutput = await executeGitCommand(["log", "--oneline", `${mainBranch}..${branchName}`], {
|
|
860
|
+
cwd: mainWorktreePath
|
|
861
|
+
});
|
|
862
|
+
const commits = commitsOutput.trim();
|
|
863
|
+
if (!commits) {
|
|
864
|
+
logger.success(`Branch is already merged into ${mainBranch}. No merge needed.`);
|
|
865
|
+
return;
|
|
866
|
+
}
|
|
867
|
+
const commitLines = commits.split("\n");
|
|
868
|
+
logger.info(`Found ${commitLines.length} commit(s) to merge:`);
|
|
869
|
+
commitLines.forEach((commit) => logger.info(` ${commit}`));
|
|
870
|
+
if (!force && !dryRun) {
|
|
871
|
+
logger.info("Proceeding with fast-forward merge... (use --force to skip confirmations)");
|
|
872
|
+
}
|
|
873
|
+
if (dryRun) {
|
|
874
|
+
logger.info(`[DRY RUN] Would execute: git merge --ff-only ${branchName}`);
|
|
875
|
+
logger.info(`[DRY RUN] This would merge ${commitLines.length} commit(s)`);
|
|
876
|
+
return;
|
|
877
|
+
}
|
|
878
|
+
try {
|
|
879
|
+
await executeGitCommand(["merge", "--ff-only", branchName], { cwd: mainWorktreePath });
|
|
880
|
+
logger.success(`Fast-forward merge completed! Merged ${commitLines.length} commit(s).`);
|
|
881
|
+
} catch (error) {
|
|
882
|
+
throw new Error(
|
|
883
|
+
`Fast-forward merge failed: ${error instanceof Error ? error.message : String(error)}
|
|
884
|
+
|
|
885
|
+
To recover:
|
|
886
|
+
1. Check merge status: git status
|
|
887
|
+
2. Abort merge if needed: git merge --abort
|
|
888
|
+
3. Verify branch is rebased: git rebase main
|
|
889
|
+
4. Try merge again: il finish`
|
|
890
|
+
);
|
|
891
|
+
}
|
|
892
|
+
}
|
|
893
|
+
/**
|
|
894
|
+
* Helper: Detect conflicted files after failed rebase
|
|
895
|
+
* @private
|
|
896
|
+
*/
|
|
897
|
+
async detectConflictedFiles(worktreePath) {
|
|
898
|
+
try {
|
|
899
|
+
const output = await executeGitCommand(["diff", "--name-only", "--diff-filter=U"], {
|
|
900
|
+
cwd: worktreePath
|
|
901
|
+
});
|
|
902
|
+
return output.trim().split("\n").filter((file) => file.length > 0);
|
|
903
|
+
} catch {
|
|
904
|
+
return [];
|
|
905
|
+
}
|
|
906
|
+
}
|
|
907
|
+
/**
|
|
908
|
+
* Helper: Format conflict error message with manual resolution steps
|
|
909
|
+
* @private
|
|
910
|
+
*/
|
|
911
|
+
formatConflictError(conflictedFiles) {
|
|
912
|
+
const fileList = conflictedFiles.map((file) => ` \u2022 ${file}`).join("\n");
|
|
913
|
+
return "Rebase failed - merge conflicts detected in:\n" + fileList + "\n\nTo resolve manually:\n 1. Fix conflicts in the files above\n 2. Stage resolved files: git add <files>\n 3. Continue rebase: git rebase --continue\n 4. Or abort rebase: git rebase --abort\n 5. Then re-run: il finish <issue-number>";
|
|
914
|
+
}
|
|
915
|
+
/**
|
|
916
|
+
* Attempt to resolve conflicts using Claude
|
|
917
|
+
* Ports bash/merge-and-clean.sh lines 839-894
|
|
918
|
+
*
|
|
919
|
+
* @param worktreePath - Path to the worktree
|
|
920
|
+
* @param conflictedFiles - List of files with conflicts
|
|
921
|
+
* @returns true if conflicts resolved, false otherwise
|
|
922
|
+
* @private
|
|
923
|
+
*/
|
|
924
|
+
async attemptClaudeConflictResolution(worktreePath, conflictedFiles) {
|
|
925
|
+
const isClaudeAvailable = await detectClaudeCli();
|
|
926
|
+
if (!isClaudeAvailable) {
|
|
927
|
+
logger.debug("Claude CLI not available, skipping conflict resolution");
|
|
928
|
+
return false;
|
|
929
|
+
}
|
|
930
|
+
logger.info(`Launching Claude to resolve conflicts in ${conflictedFiles.length} file(s)...`);
|
|
931
|
+
const prompt = `Please help resolve the git rebase conflicts in this repository. Analyze the conflicted files, understand the changes from both branches, fix the conflicts, then run 'git add .' to stage the resolved files, and finally run 'git rebase --continue' to continue the rebase process. Handle the entire workflow for me.`;
|
|
932
|
+
try {
|
|
933
|
+
await launchClaude(prompt, {
|
|
934
|
+
addDir: worktreePath,
|
|
935
|
+
headless: false
|
|
936
|
+
// Interactive - runs in current terminal with stdio: inherit
|
|
937
|
+
});
|
|
938
|
+
const remainingConflicts = await this.detectConflictedFiles(worktreePath);
|
|
939
|
+
if (remainingConflicts.length > 0) {
|
|
940
|
+
logger.warn(
|
|
941
|
+
`Conflicts still exist in ${remainingConflicts.length} file(s) after Claude assistance`
|
|
942
|
+
);
|
|
943
|
+
return false;
|
|
944
|
+
}
|
|
945
|
+
const rebaseInProgress = await this.isRebaseInProgress(worktreePath);
|
|
946
|
+
if (rebaseInProgress) {
|
|
947
|
+
logger.warn("Rebase still in progress after Claude assistance");
|
|
948
|
+
return false;
|
|
949
|
+
}
|
|
950
|
+
logger.success("Claude successfully resolved conflicts and completed rebase");
|
|
951
|
+
return true;
|
|
952
|
+
} catch (error) {
|
|
953
|
+
logger.warn("Claude conflict resolution failed", {
|
|
954
|
+
error: error instanceof Error ? error.message : String(error)
|
|
955
|
+
});
|
|
956
|
+
return false;
|
|
957
|
+
}
|
|
958
|
+
}
|
|
959
|
+
/**
|
|
960
|
+
* Check if a git rebase is currently in progress
|
|
961
|
+
* Checks for .git/rebase-merge or .git/rebase-apply directories
|
|
962
|
+
* Ports bash script logic from lines 853-856
|
|
963
|
+
*
|
|
964
|
+
* @param worktreePath - Path to the worktree
|
|
965
|
+
* @returns true if rebase in progress, false otherwise
|
|
966
|
+
* @private
|
|
967
|
+
*/
|
|
968
|
+
async isRebaseInProgress(worktreePath) {
|
|
969
|
+
const fs = await import("fs/promises");
|
|
970
|
+
const path2 = await import("path");
|
|
971
|
+
const rebaseMergePath = path2.join(worktreePath, ".git", "rebase-merge");
|
|
972
|
+
const rebaseApplyPath = path2.join(worktreePath, ".git", "rebase-apply");
|
|
973
|
+
try {
|
|
974
|
+
await fs.access(rebaseMergePath);
|
|
975
|
+
return true;
|
|
976
|
+
} catch {
|
|
977
|
+
}
|
|
978
|
+
try {
|
|
979
|
+
await fs.access(rebaseApplyPath);
|
|
980
|
+
return true;
|
|
981
|
+
} catch {
|
|
982
|
+
}
|
|
983
|
+
return false;
|
|
984
|
+
}
|
|
985
|
+
};
|
|
986
|
+
|
|
987
|
+
// src/lib/BuildRunner.ts
|
|
988
|
+
var BuildRunner = class {
|
|
989
|
+
constructor(capabilityDetector) {
|
|
990
|
+
this.capabilityDetector = capabilityDetector ?? new ProjectCapabilityDetector();
|
|
991
|
+
}
|
|
992
|
+
/**
|
|
993
|
+
* Run build verification in the specified directory
|
|
994
|
+
* @param buildPath - Path where build should run (typically main worktree path)
|
|
995
|
+
* @param options - Build options
|
|
996
|
+
*/
|
|
997
|
+
async runBuild(buildPath, options = {}) {
|
|
998
|
+
const startTime = Date.now();
|
|
999
|
+
try {
|
|
1000
|
+
const pkgJson = await readPackageJson(buildPath);
|
|
1001
|
+
const hasBuildScript = hasScript(pkgJson, "build");
|
|
1002
|
+
if (!hasBuildScript) {
|
|
1003
|
+
logger.debug("Skipping build - no build script found");
|
|
1004
|
+
return {
|
|
1005
|
+
success: true,
|
|
1006
|
+
skipped: true,
|
|
1007
|
+
reason: "No build script found in package.json",
|
|
1008
|
+
duration: Date.now() - startTime
|
|
1009
|
+
};
|
|
1010
|
+
}
|
|
1011
|
+
} catch (error) {
|
|
1012
|
+
if (error instanceof Error && error.message.includes("package.json not found")) {
|
|
1013
|
+
logger.debug("Skipping build - no package.json found (non-Node.js project)");
|
|
1014
|
+
return {
|
|
1015
|
+
success: true,
|
|
1016
|
+
skipped: true,
|
|
1017
|
+
reason: "No package.json found in project",
|
|
1018
|
+
duration: Date.now() - startTime
|
|
1019
|
+
};
|
|
1020
|
+
}
|
|
1021
|
+
throw error;
|
|
1022
|
+
}
|
|
1023
|
+
const capabilities = await this.capabilityDetector.detectCapabilities(buildPath);
|
|
1024
|
+
const isCLIProject = capabilities.capabilities.includes("cli");
|
|
1025
|
+
if (!isCLIProject) {
|
|
1026
|
+
logger.debug("Skipping build - not a CLI project (no bin field)");
|
|
1027
|
+
return {
|
|
1028
|
+
success: true,
|
|
1029
|
+
skipped: true,
|
|
1030
|
+
reason: "Project is not a CLI project (no bin field in package.json)",
|
|
1031
|
+
duration: Date.now() - startTime
|
|
1032
|
+
};
|
|
1033
|
+
}
|
|
1034
|
+
const packageManager = await detectPackageManager(buildPath);
|
|
1035
|
+
if (options.dryRun) {
|
|
1036
|
+
const command = packageManager === "npm" ? "npm run build" : `${packageManager} build`;
|
|
1037
|
+
logger.info(`[DRY RUN] Would run: ${command}`);
|
|
1038
|
+
return {
|
|
1039
|
+
success: true,
|
|
1040
|
+
skipped: false,
|
|
1041
|
+
duration: Date.now() - startTime
|
|
1042
|
+
};
|
|
1043
|
+
}
|
|
1044
|
+
logger.info("Running build...");
|
|
1045
|
+
try {
|
|
1046
|
+
await runScript("build", buildPath, [], { quiet: true });
|
|
1047
|
+
logger.success("Build completed successfully");
|
|
1048
|
+
return {
|
|
1049
|
+
success: true,
|
|
1050
|
+
skipped: false,
|
|
1051
|
+
duration: Date.now() - startTime
|
|
1052
|
+
};
|
|
1053
|
+
} catch {
|
|
1054
|
+
const runCommand = packageManager === "npm" ? "npm run build" : `${packageManager} build`;
|
|
1055
|
+
throw new Error(
|
|
1056
|
+
`Error: Build failed.
|
|
1057
|
+
Fix build errors before proceeding.
|
|
1058
|
+
|
|
1059
|
+
Run '${runCommand}' to see detailed errors.`
|
|
1060
|
+
);
|
|
1061
|
+
}
|
|
1062
|
+
}
|
|
1063
|
+
};
|
|
1064
|
+
|
|
1065
|
+
// src/commands/finish.ts
|
|
1066
|
+
import path from "path";
|
|
1067
|
+
var FinishCommand = class {
|
|
1068
|
+
constructor(gitHubService, gitWorktreeManager, validationRunner, commitManager, mergeManager, identifierParser, resourceCleanup, buildRunner, settingsManager) {
|
|
1069
|
+
const envResult = loadEnvIntoProcess();
|
|
1070
|
+
if (envResult.error) {
|
|
1071
|
+
logger.debug(`Environment loading warning: ${envResult.error.message}`);
|
|
1072
|
+
}
|
|
1073
|
+
if (envResult.parsed) {
|
|
1074
|
+
logger.debug(`Loaded ${Object.keys(envResult.parsed).length} environment variables`);
|
|
1075
|
+
}
|
|
1076
|
+
this.gitHubService = gitHubService ?? new GitHubService();
|
|
1077
|
+
this.gitWorktreeManager = gitWorktreeManager ?? new GitWorktreeManager();
|
|
1078
|
+
this.validationRunner = validationRunner ?? new ValidationRunner();
|
|
1079
|
+
this.commitManager = commitManager ?? new CommitManager();
|
|
1080
|
+
this.mergeManager = mergeManager ?? new MergeManager();
|
|
1081
|
+
this.identifierParser = identifierParser ?? new IdentifierParser(this.gitWorktreeManager);
|
|
1082
|
+
this.settingsManager = settingsManager ?? new SettingsManager();
|
|
1083
|
+
if (resourceCleanup) {
|
|
1084
|
+
this.resourceCleanup = resourceCleanup;
|
|
1085
|
+
}
|
|
1086
|
+
this.buildRunner = buildRunner ?? new BuildRunner();
|
|
1087
|
+
}
|
|
1088
|
+
/**
|
|
1089
|
+
* Lazy initialization of ResourceCleanup with properly configured DatabaseManager
|
|
1090
|
+
*/
|
|
1091
|
+
async ensureResourceCleanup() {
|
|
1092
|
+
var _a, _b;
|
|
1093
|
+
if (this.resourceCleanup) {
|
|
1094
|
+
return;
|
|
1095
|
+
}
|
|
1096
|
+
const settings = await this.settingsManager.loadSettings();
|
|
1097
|
+
const databaseUrlEnvVarName = ((_b = (_a = settings.capabilities) == null ? void 0 : _a.database) == null ? void 0 : _b.databaseUrlEnvVarName) ?? "DATABASE_URL";
|
|
1098
|
+
const environmentManager = new EnvironmentManager();
|
|
1099
|
+
const neonProvider = new NeonProvider({
|
|
1100
|
+
projectId: process.env.NEON_PROJECT_ID ?? "",
|
|
1101
|
+
parentBranch: process.env.NEON_PARENT_BRANCH ?? ""
|
|
1102
|
+
});
|
|
1103
|
+
const databaseManager = new DatabaseManager(neonProvider, environmentManager, databaseUrlEnvVarName);
|
|
1104
|
+
const cliIsolationManager = new CLIIsolationManager();
|
|
1105
|
+
this.resourceCleanup = new ResourceCleanup(
|
|
1106
|
+
this.gitWorktreeManager,
|
|
1107
|
+
new ProcessManager(),
|
|
1108
|
+
databaseManager,
|
|
1109
|
+
cliIsolationManager
|
|
1110
|
+
);
|
|
1111
|
+
}
|
|
1112
|
+
/**
|
|
1113
|
+
* Main entry point for finish command
|
|
1114
|
+
*/
|
|
1115
|
+
async execute(input) {
|
|
1116
|
+
try {
|
|
1117
|
+
const parsed = await this.parseInput(input.identifier, input.options);
|
|
1118
|
+
const worktrees = await this.validateInput(parsed, input.options);
|
|
1119
|
+
logger.info(`Validated input: ${this.formatParsedInput(parsed)}`);
|
|
1120
|
+
const worktree = worktrees[0];
|
|
1121
|
+
if (!worktree) {
|
|
1122
|
+
throw new Error("No worktree found");
|
|
1123
|
+
}
|
|
1124
|
+
if (parsed.type === "pr") {
|
|
1125
|
+
if (!parsed.number) {
|
|
1126
|
+
throw new Error("Invalid PR number");
|
|
1127
|
+
}
|
|
1128
|
+
const pr = await this.gitHubService.fetchPR(parsed.number);
|
|
1129
|
+
await this.executePRWorkflow(parsed, input.options, worktree, pr);
|
|
1130
|
+
} else {
|
|
1131
|
+
await this.executeIssueWorkflow(parsed, input.options, worktree);
|
|
1132
|
+
}
|
|
1133
|
+
} catch (error) {
|
|
1134
|
+
if (error instanceof Error) {
|
|
1135
|
+
logger.error(`${error.message}`);
|
|
1136
|
+
} else {
|
|
1137
|
+
logger.error("An unknown error occurred");
|
|
1138
|
+
}
|
|
1139
|
+
throw error;
|
|
1140
|
+
}
|
|
1141
|
+
}
|
|
1142
|
+
/**
|
|
1143
|
+
* Parse input to determine type and extract relevant data
|
|
1144
|
+
* Supports auto-detection from current directory when identifier is undefined
|
|
1145
|
+
*/
|
|
1146
|
+
async parseInput(identifier, options) {
|
|
1147
|
+
if (options.pr !== void 0) {
|
|
1148
|
+
return {
|
|
1149
|
+
type: "pr",
|
|
1150
|
+
number: options.pr,
|
|
1151
|
+
originalInput: `--pr ${options.pr}`,
|
|
1152
|
+
autoDetected: false
|
|
1153
|
+
};
|
|
1154
|
+
}
|
|
1155
|
+
if (identifier == null ? void 0 : identifier.trim()) {
|
|
1156
|
+
return await this.parseExplicitInput(identifier.trim());
|
|
1157
|
+
}
|
|
1158
|
+
return await this.autoDetectFromCurrentDirectory();
|
|
1159
|
+
}
|
|
1160
|
+
/**
|
|
1161
|
+
* Parse explicit identifier input using pattern-based detection
|
|
1162
|
+
* (No GitHub API calls - uses IdentifierParser)
|
|
1163
|
+
*/
|
|
1164
|
+
async parseExplicitInput(identifier) {
|
|
1165
|
+
const prPattern = /^(?:pr|PR)[/-](\d+)$/;
|
|
1166
|
+
const prMatch = identifier.match(prPattern);
|
|
1167
|
+
if (prMatch == null ? void 0 : prMatch[1]) {
|
|
1168
|
+
return {
|
|
1169
|
+
type: "pr",
|
|
1170
|
+
number: parseInt(prMatch[1], 10),
|
|
1171
|
+
originalInput: identifier,
|
|
1172
|
+
autoDetected: false
|
|
1173
|
+
};
|
|
1174
|
+
}
|
|
1175
|
+
const parsed = await this.identifierParser.parseForPatternDetection(identifier);
|
|
1176
|
+
if (parsed.type === "description") {
|
|
1177
|
+
throw new Error("Description input type is not supported in finish command");
|
|
1178
|
+
}
|
|
1179
|
+
const result = {
|
|
1180
|
+
type: parsed.type,
|
|
1181
|
+
originalInput: parsed.originalInput,
|
|
1182
|
+
autoDetected: false
|
|
1183
|
+
};
|
|
1184
|
+
if (parsed.number !== void 0) {
|
|
1185
|
+
result.number = parsed.number;
|
|
1186
|
+
}
|
|
1187
|
+
if (parsed.branchName !== void 0) {
|
|
1188
|
+
result.branchName = parsed.branchName;
|
|
1189
|
+
}
|
|
1190
|
+
return result;
|
|
1191
|
+
}
|
|
1192
|
+
/**
|
|
1193
|
+
* Auto-detect PR or issue from current directory
|
|
1194
|
+
* Ports logic from merge-current-issue.sh lines 30-52
|
|
1195
|
+
*/
|
|
1196
|
+
async autoDetectFromCurrentDirectory() {
|
|
1197
|
+
const currentDir = path.basename(process.cwd());
|
|
1198
|
+
const prPattern = /_pr_(\d+)$/;
|
|
1199
|
+
const prMatch = currentDir.match(prPattern);
|
|
1200
|
+
if (prMatch == null ? void 0 : prMatch[1]) {
|
|
1201
|
+
const prNumber = parseInt(prMatch[1], 10);
|
|
1202
|
+
logger.debug(`Auto-detected PR #${prNumber} from directory: ${currentDir}`);
|
|
1203
|
+
return {
|
|
1204
|
+
type: "pr",
|
|
1205
|
+
number: prNumber,
|
|
1206
|
+
originalInput: currentDir,
|
|
1207
|
+
autoDetected: true
|
|
1208
|
+
};
|
|
1209
|
+
}
|
|
1210
|
+
const issuePattern = /issue-(\d+)/;
|
|
1211
|
+
const issueMatch = currentDir.match(issuePattern);
|
|
1212
|
+
if (issueMatch == null ? void 0 : issueMatch[1]) {
|
|
1213
|
+
const issueNumber = parseInt(issueMatch[1], 10);
|
|
1214
|
+
logger.debug(
|
|
1215
|
+
`Auto-detected issue #${issueNumber} from directory: ${currentDir}`
|
|
1216
|
+
);
|
|
1217
|
+
return {
|
|
1218
|
+
type: "issue",
|
|
1219
|
+
number: issueNumber,
|
|
1220
|
+
originalInput: currentDir,
|
|
1221
|
+
autoDetected: true
|
|
1222
|
+
};
|
|
1223
|
+
}
|
|
1224
|
+
const repoInfo = await this.gitWorktreeManager.getRepoInfo();
|
|
1225
|
+
const currentBranch = repoInfo.currentBranch;
|
|
1226
|
+
if (!currentBranch) {
|
|
1227
|
+
throw new Error(
|
|
1228
|
+
"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"
|
|
1229
|
+
);
|
|
1230
|
+
}
|
|
1231
|
+
const branchIssueMatch = currentBranch.match(issuePattern);
|
|
1232
|
+
if (branchIssueMatch == null ? void 0 : branchIssueMatch[1]) {
|
|
1233
|
+
const issueNumber = parseInt(branchIssueMatch[1], 10);
|
|
1234
|
+
logger.debug(
|
|
1235
|
+
`Auto-detected issue #${issueNumber} from branch: ${currentBranch}`
|
|
1236
|
+
);
|
|
1237
|
+
return {
|
|
1238
|
+
type: "issue",
|
|
1239
|
+
number: issueNumber,
|
|
1240
|
+
originalInput: currentBranch,
|
|
1241
|
+
autoDetected: true
|
|
1242
|
+
};
|
|
1243
|
+
}
|
|
1244
|
+
return {
|
|
1245
|
+
type: "branch",
|
|
1246
|
+
branchName: currentBranch,
|
|
1247
|
+
originalInput: currentBranch,
|
|
1248
|
+
autoDetected: true
|
|
1249
|
+
};
|
|
1250
|
+
}
|
|
1251
|
+
/**
|
|
1252
|
+
* Validate the parsed input based on its type
|
|
1253
|
+
*/
|
|
1254
|
+
async validateInput(parsed, options) {
|
|
1255
|
+
switch (parsed.type) {
|
|
1256
|
+
case "pr": {
|
|
1257
|
+
if (!parsed.number) {
|
|
1258
|
+
throw new Error("Invalid PR number");
|
|
1259
|
+
}
|
|
1260
|
+
const pr = await this.gitHubService.fetchPR(parsed.number);
|
|
1261
|
+
logger.debug(`Validated PR #${parsed.number} (state: ${pr.state})`);
|
|
1262
|
+
return await this.findWorktreeForIdentifier(parsed);
|
|
1263
|
+
}
|
|
1264
|
+
case "issue": {
|
|
1265
|
+
if (!parsed.number) {
|
|
1266
|
+
throw new Error("Invalid issue number");
|
|
1267
|
+
}
|
|
1268
|
+
const issue = await this.gitHubService.fetchIssue(parsed.number);
|
|
1269
|
+
if (issue.state === "closed" && !options.force) {
|
|
1270
|
+
throw new Error(
|
|
1271
|
+
`Issue #${parsed.number} is closed. Use --force to finish anyway.`
|
|
1272
|
+
);
|
|
1273
|
+
}
|
|
1274
|
+
logger.debug(`Validated issue #${parsed.number} (state: ${issue.state})`);
|
|
1275
|
+
return await this.findWorktreeForIdentifier(parsed);
|
|
1276
|
+
}
|
|
1277
|
+
case "branch": {
|
|
1278
|
+
if (!parsed.branchName) {
|
|
1279
|
+
throw new Error("Invalid branch name");
|
|
1280
|
+
}
|
|
1281
|
+
if (!this.isValidBranchName(parsed.branchName)) {
|
|
1282
|
+
throw new Error(
|
|
1283
|
+
"Invalid branch name. Use only letters, numbers, hyphens, underscores, and slashes"
|
|
1284
|
+
);
|
|
1285
|
+
}
|
|
1286
|
+
logger.debug(`Validated branch name: ${parsed.branchName}`);
|
|
1287
|
+
return await this.findWorktreeForIdentifier(parsed);
|
|
1288
|
+
}
|
|
1289
|
+
default: {
|
|
1290
|
+
const unknownType = parsed;
|
|
1291
|
+
throw new Error(`Unknown input type: ${unknownType.type}`);
|
|
1292
|
+
}
|
|
1293
|
+
}
|
|
1294
|
+
}
|
|
1295
|
+
/**
|
|
1296
|
+
* Find worktree for the given identifier using specific methods based on type
|
|
1297
|
+
* (uses precise pattern matching instead of broad substring matching)
|
|
1298
|
+
* Throws error if not found
|
|
1299
|
+
*/
|
|
1300
|
+
async findWorktreeForIdentifier(parsed) {
|
|
1301
|
+
let worktree = null;
|
|
1302
|
+
switch (parsed.type) {
|
|
1303
|
+
case "pr": {
|
|
1304
|
+
if (!parsed.number) {
|
|
1305
|
+
throw new Error("Invalid PR number");
|
|
1306
|
+
}
|
|
1307
|
+
worktree = await this.gitWorktreeManager.findWorktreeForPR(
|
|
1308
|
+
parsed.number,
|
|
1309
|
+
""
|
|
1310
|
+
);
|
|
1311
|
+
break;
|
|
1312
|
+
}
|
|
1313
|
+
case "issue": {
|
|
1314
|
+
if (!parsed.number) {
|
|
1315
|
+
throw new Error("Invalid issue number");
|
|
1316
|
+
}
|
|
1317
|
+
worktree = await this.gitWorktreeManager.findWorktreeForIssue(
|
|
1318
|
+
parsed.number
|
|
1319
|
+
);
|
|
1320
|
+
break;
|
|
1321
|
+
}
|
|
1322
|
+
case "branch": {
|
|
1323
|
+
if (!parsed.branchName) {
|
|
1324
|
+
throw new Error("Invalid branch name");
|
|
1325
|
+
}
|
|
1326
|
+
worktree = await this.gitWorktreeManager.findWorktreeForBranch(
|
|
1327
|
+
parsed.branchName
|
|
1328
|
+
);
|
|
1329
|
+
break;
|
|
1330
|
+
}
|
|
1331
|
+
default: {
|
|
1332
|
+
const unknownType = parsed;
|
|
1333
|
+
throw new Error(`Unknown input type: ${unknownType.type}`);
|
|
1334
|
+
}
|
|
1335
|
+
}
|
|
1336
|
+
if (!worktree) {
|
|
1337
|
+
throw new Error(
|
|
1338
|
+
`No worktree found for ${this.formatParsedInput(parsed)}. Use 'il list' to see available worktrees.`
|
|
1339
|
+
);
|
|
1340
|
+
}
|
|
1341
|
+
logger.debug(`Found worktree: ${worktree.path}`);
|
|
1342
|
+
return [worktree];
|
|
1343
|
+
}
|
|
1344
|
+
/**
|
|
1345
|
+
* Validate branch name format
|
|
1346
|
+
*/
|
|
1347
|
+
isValidBranchName(branch) {
|
|
1348
|
+
return /^[a-zA-Z0-9/_-]+$/.test(branch);
|
|
1349
|
+
}
|
|
1350
|
+
/**
|
|
1351
|
+
* Format parsed input for display
|
|
1352
|
+
*/
|
|
1353
|
+
formatParsedInput(parsed) {
|
|
1354
|
+
const autoLabel = parsed.autoDetected ? " (auto-detected)" : "";
|
|
1355
|
+
switch (parsed.type) {
|
|
1356
|
+
case "pr":
|
|
1357
|
+
return `PR #${parsed.number}${autoLabel}`;
|
|
1358
|
+
case "issue":
|
|
1359
|
+
return `Issue #${parsed.number}${autoLabel}`;
|
|
1360
|
+
case "branch":
|
|
1361
|
+
return `Branch '${parsed.branchName}'${autoLabel}`;
|
|
1362
|
+
default:
|
|
1363
|
+
return "Unknown input";
|
|
1364
|
+
}
|
|
1365
|
+
}
|
|
1366
|
+
/**
|
|
1367
|
+
* Execute workflow for issues and branches (merge into main)
|
|
1368
|
+
* This is the traditional workflow: validate → commit → rebase → merge → cleanup
|
|
1369
|
+
*/
|
|
1370
|
+
async executeIssueWorkflow(parsed, options, worktree) {
|
|
1371
|
+
var _a, _b;
|
|
1372
|
+
if (!options.dryRun) {
|
|
1373
|
+
logger.info("Running pre-merge validations...");
|
|
1374
|
+
await this.validationRunner.runValidations(worktree.path, {
|
|
1375
|
+
dryRun: options.dryRun ?? false
|
|
1376
|
+
});
|
|
1377
|
+
logger.success("All validations passed");
|
|
1378
|
+
} else {
|
|
1379
|
+
logger.info("[DRY RUN] Would run pre-merge validations");
|
|
1380
|
+
}
|
|
1381
|
+
const gitStatus = await this.commitManager.detectUncommittedChanges(worktree.path);
|
|
1382
|
+
if (gitStatus.hasUncommittedChanges) {
|
|
1383
|
+
if (options.dryRun) {
|
|
1384
|
+
logger.info("[DRY RUN] Would auto-commit uncommitted changes (validation passed)");
|
|
1385
|
+
} else {
|
|
1386
|
+
logger.info("Validation passed, auto-committing uncommitted changes...");
|
|
1387
|
+
const settings = await this.settingsManager.loadSettings(worktree.path);
|
|
1388
|
+
const skipVerify = ((_b = (_a = settings.workflows) == null ? void 0 : _a.issue) == null ? void 0 : _b.noVerify) ?? false;
|
|
1389
|
+
const commitOptions = {
|
|
1390
|
+
dryRun: options.dryRun ?? false,
|
|
1391
|
+
skipVerify
|
|
1392
|
+
};
|
|
1393
|
+
if (parsed.type === "issue" && parsed.number) {
|
|
1394
|
+
commitOptions.issueNumber = parsed.number;
|
|
1395
|
+
}
|
|
1396
|
+
await this.commitManager.commitChanges(worktree.path, commitOptions);
|
|
1397
|
+
logger.success("Changes committed successfully");
|
|
1398
|
+
}
|
|
1399
|
+
} else {
|
|
1400
|
+
logger.debug("No uncommitted changes found");
|
|
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 } = await import("./git-LVRZ57GJ.js");
|
|
1470
|
+
await pushBranchToRemote(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
|
+
* Perform cleanup for closed/merged PRs
|
|
1482
|
+
* Similar to performPostMergeCleanup but with different messaging
|
|
1483
|
+
*/
|
|
1484
|
+
async performPRCleanup(parsed, options, worktree) {
|
|
1485
|
+
const cleanupInput = {
|
|
1486
|
+
type: parsed.type,
|
|
1487
|
+
originalInput: parsed.originalInput,
|
|
1488
|
+
...parsed.number !== void 0 && { number: parsed.number },
|
|
1489
|
+
...parsed.branchName !== void 0 && { branchName: parsed.branchName }
|
|
1490
|
+
};
|
|
1491
|
+
const cleanupOptions = {
|
|
1492
|
+
dryRun: options.dryRun ?? false,
|
|
1493
|
+
deleteBranch: true,
|
|
1494
|
+
// Delete branch for closed/merged PRs
|
|
1495
|
+
keepDatabase: false,
|
|
1496
|
+
force: options.force ?? false
|
|
1497
|
+
};
|
|
1498
|
+
try {
|
|
1499
|
+
await this.ensureResourceCleanup();
|
|
1500
|
+
if (!this.resourceCleanup) {
|
|
1501
|
+
throw new Error("Failed to initialize ResourceCleanup");
|
|
1502
|
+
}
|
|
1503
|
+
const result = await this.resourceCleanup.cleanupWorktree(cleanupInput, cleanupOptions);
|
|
1504
|
+
this.reportCleanupResults(result);
|
|
1505
|
+
if (!result.success) {
|
|
1506
|
+
logger.warn("Some cleanup operations failed - manual cleanup may be required");
|
|
1507
|
+
this.showManualCleanupInstructions(worktree);
|
|
1508
|
+
}
|
|
1509
|
+
} catch (error) {
|
|
1510
|
+
const errorMessage = error instanceof Error ? error.message : "Unknown error";
|
|
1511
|
+
logger.warn(`Cleanup failed: ${errorMessage}`);
|
|
1512
|
+
this.showManualCleanupInstructions(worktree);
|
|
1513
|
+
throw error;
|
|
1514
|
+
}
|
|
1515
|
+
}
|
|
1516
|
+
/**
|
|
1517
|
+
* Run post-merge build verification for CLI projects
|
|
1518
|
+
* Runs in main worktree to verify merged code builds successfully
|
|
1519
|
+
*/
|
|
1520
|
+
async runPostMergeBuild(worktreePath, options) {
|
|
1521
|
+
const mainWorktreePath = await findMainWorktreePathWithSettings(worktreePath, this.settingsManager);
|
|
1522
|
+
if (options.dryRun) {
|
|
1523
|
+
logger.info("[DRY RUN] Would run post-merge build");
|
|
1524
|
+
return;
|
|
1525
|
+
}
|
|
1526
|
+
logger.info("Running post-merge build...");
|
|
1527
|
+
const result = await this.buildRunner.runBuild(mainWorktreePath, {
|
|
1528
|
+
dryRun: options.dryRun ?? false
|
|
1529
|
+
});
|
|
1530
|
+
if (result.skipped) {
|
|
1531
|
+
logger.debug(`Build skipped: ${result.reason}`);
|
|
1532
|
+
} else {
|
|
1533
|
+
logger.success("Post-merge build completed successfully");
|
|
1534
|
+
}
|
|
1535
|
+
}
|
|
1536
|
+
/**
|
|
1537
|
+
* Perform post-merge cleanup operations
|
|
1538
|
+
* Converts ParsedFinishInput to ParsedInput and calls ResourceCleanup
|
|
1539
|
+
* Handles failures gracefully without throwing
|
|
1540
|
+
*/
|
|
1541
|
+
async performPostMergeCleanup(parsed, options, worktree) {
|
|
1542
|
+
const cleanupInput = {
|
|
1543
|
+
type: parsed.type,
|
|
1544
|
+
originalInput: parsed.originalInput,
|
|
1545
|
+
...parsed.number !== void 0 && { number: parsed.number },
|
|
1546
|
+
...parsed.branchName !== void 0 && { branchName: parsed.branchName }
|
|
1547
|
+
};
|
|
1548
|
+
const cleanupOptions = {
|
|
1549
|
+
dryRun: options.dryRun ?? false,
|
|
1550
|
+
deleteBranch: true,
|
|
1551
|
+
// Delete branch after successful merge
|
|
1552
|
+
keepDatabase: false,
|
|
1553
|
+
// Clean up database after merge
|
|
1554
|
+
force: options.force ?? false
|
|
1555
|
+
};
|
|
1556
|
+
try {
|
|
1557
|
+
logger.info("Starting post-merge cleanup...");
|
|
1558
|
+
await this.ensureResourceCleanup();
|
|
1559
|
+
if (!this.resourceCleanup) {
|
|
1560
|
+
throw new Error("Failed to initialize ResourceCleanup");
|
|
1561
|
+
}
|
|
1562
|
+
const result = await this.resourceCleanup.cleanupWorktree(cleanupInput, cleanupOptions);
|
|
1563
|
+
this.reportCleanupResults(result);
|
|
1564
|
+
if (!result.success) {
|
|
1565
|
+
logger.warn("Some cleanup operations failed - manual cleanup may be required");
|
|
1566
|
+
this.showManualCleanupInstructions(worktree);
|
|
1567
|
+
} else {
|
|
1568
|
+
logger.success("Post-merge cleanup completed successfully");
|
|
1569
|
+
}
|
|
1570
|
+
} catch (error) {
|
|
1571
|
+
const errorMessage = error instanceof Error ? error.message : "Unknown error";
|
|
1572
|
+
logger.warn(`Cleanup failed: ${errorMessage}`);
|
|
1573
|
+
logger.warn("Merge completed successfully, but manual cleanup is required");
|
|
1574
|
+
this.showManualCleanupInstructions(worktree);
|
|
1575
|
+
}
|
|
1576
|
+
}
|
|
1577
|
+
/**
|
|
1578
|
+
* Report cleanup operation results to user
|
|
1579
|
+
*/
|
|
1580
|
+
reportCleanupResults(result) {
|
|
1581
|
+
if (result.operations.length === 0) {
|
|
1582
|
+
return;
|
|
1583
|
+
}
|
|
1584
|
+
logger.info("Cleanup operations:");
|
|
1585
|
+
for (const op of result.operations) {
|
|
1586
|
+
const status = op.success ? "\u2713" : "\u2717";
|
|
1587
|
+
const message = op.error ? `${op.message}: ${op.error}` : op.message;
|
|
1588
|
+
if (op.success) {
|
|
1589
|
+
logger.info(` ${status} ${message}`);
|
|
1590
|
+
} else {
|
|
1591
|
+
logger.warn(` ${status} ${message}`);
|
|
1592
|
+
}
|
|
1593
|
+
}
|
|
1594
|
+
}
|
|
1595
|
+
/**
|
|
1596
|
+
* Show manual cleanup instructions when cleanup fails
|
|
1597
|
+
*/
|
|
1598
|
+
showManualCleanupInstructions(worktree) {
|
|
1599
|
+
logger.info("\nManual cleanup commands:");
|
|
1600
|
+
logger.info(` 1. Remove worktree: git worktree remove ${worktree.path}`);
|
|
1601
|
+
logger.info(` 2. Delete branch: git branch -d ${worktree.branch}`);
|
|
1602
|
+
logger.info(` 3. Check dev servers: lsof -i :PORT (and kill if needed)`);
|
|
1603
|
+
}
|
|
1604
|
+
};
|
|
1605
|
+
export {
|
|
1606
|
+
FinishCommand
|
|
1607
|
+
};
|
|
1608
|
+
//# sourceMappingURL=finish-CY4CIH6O.js.map
|