@iloom/cli 0.1.19 → 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.
- package/README.md +290 -30
- package/dist/BranchNamingService-3OQPRSWT.js +13 -0
- package/dist/ClaudeContextManager-MUQSDY2E.js +13 -0
- package/dist/ClaudeService-HG4VQ7AW.js +12 -0
- package/dist/GitHubService-EBOETDIW.js +11 -0
- package/dist/{LoomLauncher-UMMLPIZO.js → LoomLauncher-FLEMBCSQ.js} +64 -33
- package/dist/LoomLauncher-FLEMBCSQ.js.map +1 -0
- package/dist/ProjectCapabilityDetector-34LU7JJ4.js +9 -0
- package/dist/{PromptTemplateManager-WII75TKH.js → PromptTemplateManager-A52RUAMS.js} +2 -2
- package/dist/README.md +290 -30
- package/dist/{SettingsManager-SKLUVE3K.js → SettingsManager-WHHFGSL7.js} +12 -4
- package/dist/SettingsMigrationManager-AGIIIPDQ.js +10 -0
- package/dist/agents/iloom-issue-analyze-and-plan.md +125 -35
- package/dist/agents/iloom-issue-analyzer.md +284 -32
- package/dist/agents/iloom-issue-complexity-evaluator.md +40 -21
- package/dist/agents/iloom-issue-enhancer.md +69 -48
- package/dist/agents/iloom-issue-implementer.md +36 -25
- package/dist/agents/iloom-issue-planner.md +35 -24
- package/dist/agents/iloom-issue-reviewer.md +62 -9
- package/dist/chunk-3KATJIKO.js +55 -0
- package/dist/chunk-3KATJIKO.js.map +1 -0
- package/dist/{chunk-JXQXSC45.js → chunk-3RUPPQRG.js} +1 -18
- package/dist/chunk-3RUPPQRG.js.map +1 -0
- package/dist/{chunk-PR7FKQBG.js → chunk-47KSHUCR.js} +3 -3
- package/dist/chunk-47KSHUCR.js.map +1 -0
- package/dist/{chunk-IO4WFTL2.js → chunk-4HHRTA7Q.js} +3 -3
- package/dist/{chunk-IO4WFTL2.js.map → chunk-4HHRTA7Q.js.map} +1 -1
- package/dist/chunk-5EF7Z346.js +1987 -0
- package/dist/chunk-5EF7Z346.js.map +1 -0
- package/dist/{chunk-VVH3ANF2.js → chunk-AWOFAD5O.js} +12 -12
- package/dist/chunk-AWOFAD5O.js.map +1 -0
- package/dist/{chunk-DEPYQRRB.js → chunk-C5QCTEQK.js} +2 -2
- package/dist/{chunk-CWR2SANQ.js → chunk-EBISESAP.js} +1 -1
- package/dist/{chunk-ELFT36PV.js → chunk-FIAT22G7.js} +4 -16
- package/dist/chunk-FIAT22G7.js.map +1 -0
- package/dist/{chunk-ZWXJBSUW.js → chunk-G2IEYOLQ.js} +11 -38
- package/dist/chunk-G2IEYOLQ.js.map +1 -0
- package/dist/{chunk-ZMNQBJUI.js → chunk-IP7SMKIF.js} +61 -22
- package/dist/chunk-IP7SMKIF.js.map +1 -0
- package/dist/{chunk-JNKJ7NJV.js → chunk-JKXJ7BGL.js} +6 -2
- package/dist/{chunk-JNKJ7NJV.js.map → chunk-JKXJ7BGL.js.map} +1 -1
- package/dist/{chunk-LAPY6NAE.js → chunk-JQFO7QQN.js} +68 -12
- package/dist/{chunk-LAPY6NAE.js.map → chunk-JQFO7QQN.js.map} +1 -1
- package/dist/{SettingsMigrationManager-MTQIMI54.js → chunk-KLBYVHPK.js} +3 -2
- package/dist/{chunk-KOCQAD2E.js → chunk-MAVL6PJF.js} +26 -3
- package/dist/chunk-MAVL6PJF.js.map +1 -0
- package/dist/{chunk-USVVV3FP.js → chunk-MKWYLDFK.js} +5 -5
- package/dist/chunk-ML3NRPNB.js +396 -0
- package/dist/chunk-ML3NRPNB.js.map +1 -0
- package/dist/{chunk-FXV24OYZ.js → chunk-PA6Q6AWM.js} +24 -4
- package/dist/chunk-PA6Q6AWM.js.map +1 -0
- package/dist/chunk-RO26VS3W.js +444 -0
- package/dist/chunk-RO26VS3W.js.map +1 -0
- package/dist/{chunk-PV3GAXQO.js → chunk-VAYCCUXW.js} +72 -2
- package/dist/{chunk-PV3GAXQO.js.map → chunk-VAYCCUXW.js.map} +1 -1
- package/dist/{chunk-SPYPLHMK.js → chunk-VU3QMIP2.js} +34 -2
- package/dist/chunk-VU3QMIP2.js.map +1 -0
- package/dist/{chunk-PVAVNJKS.js → chunk-WEN5C5DM.js} +10 -1
- package/dist/chunk-WEN5C5DM.js.map +1 -0
- package/dist/{chunk-PXZBAC2M.js → chunk-XXV3UFZL.js} +4 -4
- package/dist/{chunk-PXZBAC2M.js.map → chunk-XXV3UFZL.js.map} +1 -1
- package/dist/{chunk-RSRO7564.js → chunk-ZE74H5BR.js} +28 -3
- package/dist/chunk-ZE74H5BR.js.map +1 -0
- package/dist/{chunk-GZP4UGGM.js → chunk-ZM3CFL5L.js} +2 -2
- package/dist/{chunk-BLCTGFZN.js → chunk-ZT3YZB4K.js} +3 -4
- package/dist/chunk-ZT3YZB4K.js.map +1 -0
- package/dist/{claude-7LUVDZZ4.js → claude-GOP6PFC7.js} +2 -2
- package/dist/{cleanup-ZHROIBSQ.js → cleanup-7RWLBSLE.js} +86 -25
- package/dist/cleanup-7RWLBSLE.js.map +1 -0
- package/dist/cli.js +2513 -64
- package/dist/cli.js.map +1 -1
- package/dist/{contribute-3MQJ3XAQ.js → contribute-BS2L4FZR.js} +9 -6
- package/dist/{contribute-3MQJ3XAQ.js.map → contribute-BS2L4FZR.js.map} +1 -1
- package/dist/{feedback-ZOUCCHN4.js → feedback-N4ECWIPF.js} +15 -14
- package/dist/{feedback-ZOUCCHN4.js.map → feedback-N4ECWIPF.js.map} +1 -1
- package/dist/{git-OUYMVYJX.js → git-TDXKRTXM.js} +4 -2
- package/dist/{ignite-HICLZEYU.js → ignite-VM64QO3J.js} +32 -27
- package/dist/ignite-VM64QO3J.js.map +1 -0
- package/dist/index.d.ts +379 -45
- package/dist/index.js +1241 -448
- package/dist/index.js.map +1 -1
- package/dist/{init-UMKNHNV5.js → init-G3T64SC4.js} +104 -40
- package/dist/init-G3T64SC4.js.map +1 -0
- package/dist/mcp/issue-management-server.js +934 -0
- package/dist/mcp/issue-management-server.js.map +1 -0
- package/dist/{neon-helpers-ZVIRPKCI.js → neon-helpers-WPUACUVC.js} +3 -3
- package/dist/{open-ETZUFSE4.js → open-KXDXEKRZ.js} +39 -36
- package/dist/open-KXDXEKRZ.js.map +1 -0
- package/dist/{prompt-ANTQWHUF.js → prompt-7INJ7YRU.js} +4 -2
- package/dist/prompt-7INJ7YRU.js.map +1 -0
- package/dist/prompts/init-prompt.txt +563 -91
- package/dist/prompts/issue-prompt.txt +27 -27
- package/dist/{rebase-KBWFDZCN.js → rebase-Q7GMM7EI.js} +6 -6
- package/dist/{remote-GJEZWRCC.js → remote-VUNCQZ6J.js} +5 -2
- package/dist/remote-VUNCQZ6J.js.map +1 -0
- package/dist/{run-4SVQ3WEU.js → run-PAWJJCSX.js} +39 -36
- package/dist/run-PAWJJCSX.js.map +1 -0
- package/dist/schema/settings.schema.json +74 -0
- package/dist/{terminal-3D6TUAKJ.js → terminal-BIRBZ4AZ.js} +2 -2
- package/dist/terminal-BIRBZ4AZ.js.map +1 -0
- package/dist/{test-git-MKZATGZN.js → test-git-3WDLNQCA.js} +3 -3
- package/dist/{test-prefix-ZNLWDI3K.js → test-prefix-EVGAWAJW.js} +3 -3
- package/dist/{test-tabs-JRKY3QMM.js → test-tabs-RXDBZ6J7.js} +2 -2
- package/dist/{test-webserver-M2I3EV4J.js → test-webserver-DAHONWCS.js} +4 -4
- package/dist/test-webserver-DAHONWCS.js.map +1 -0
- package/package.json +2 -1
- package/dist/ClaudeContextManager-JKR4WGNU.js +0 -13
- package/dist/ClaudeService-55DQGB7T.js +0 -12
- package/dist/GitHubService-LWP4GKGH.js +0 -11
- package/dist/LoomLauncher-UMMLPIZO.js.map +0 -1
- package/dist/add-issue-X56V3XPB.js +0 -69
- package/dist/add-issue-X56V3XPB.js.map +0 -1
- package/dist/chunk-BLCTGFZN.js.map +0 -1
- package/dist/chunk-ELFT36PV.js.map +0 -1
- package/dist/chunk-FXV24OYZ.js.map +0 -1
- package/dist/chunk-H4E4THUZ.js +0 -55
- package/dist/chunk-H4E4THUZ.js.map +0 -1
- package/dist/chunk-H5LDRGVK.js +0 -642
- package/dist/chunk-H5LDRGVK.js.map +0 -1
- package/dist/chunk-JXQXSC45.js.map +0 -1
- package/dist/chunk-KOCQAD2E.js.map +0 -1
- package/dist/chunk-PR7FKQBG.js.map +0 -1
- package/dist/chunk-PVAVNJKS.js.map +0 -1
- package/dist/chunk-Q2KYPAH2.js +0 -545
- package/dist/chunk-Q2KYPAH2.js.map +0 -1
- package/dist/chunk-RSRO7564.js.map +0 -1
- package/dist/chunk-SPYPLHMK.js.map +0 -1
- package/dist/chunk-VCMMAFXQ.js +0 -54
- package/dist/chunk-VCMMAFXQ.js.map +0 -1
- package/dist/chunk-VVH3ANF2.js.map +0 -1
- package/dist/chunk-VYQLLHZ7.js +0 -239
- package/dist/chunk-VYQLLHZ7.js.map +0 -1
- package/dist/chunk-ZMNQBJUI.js.map +0 -1
- package/dist/chunk-ZWXJBSUW.js.map +0 -1
- package/dist/cleanup-ZHROIBSQ.js.map +0 -1
- package/dist/enhance-VGWUX474.js +0 -176
- package/dist/enhance-VGWUX474.js.map +0 -1
- package/dist/finish-QJSK6Z7J.js +0 -1355
- package/dist/finish-QJSK6Z7J.js.map +0 -1
- package/dist/ignite-HICLZEYU.js.map +0 -1
- package/dist/init-UMKNHNV5.js.map +0 -1
- package/dist/mcp/chunk-6SDFJ42P.js +0 -62
- package/dist/mcp/chunk-6SDFJ42P.js.map +0 -1
- package/dist/mcp/claude-YHHHLSXH.js +0 -249
- package/dist/mcp/claude-YHHHLSXH.js.map +0 -1
- package/dist/mcp/color-QS5BFCNN.js +0 -168
- package/dist/mcp/color-QS5BFCNN.js.map +0 -1
- package/dist/mcp/github-comment-server.js +0 -168
- package/dist/mcp/github-comment-server.js.map +0 -1
- package/dist/mcp/terminal-SDCMDVD7.js +0 -202
- package/dist/mcp/terminal-SDCMDVD7.js.map +0 -1
- package/dist/open-ETZUFSE4.js.map +0 -1
- package/dist/run-4SVQ3WEU.js.map +0 -1
- package/dist/start-CT2ZEFP2.js +0 -983
- package/dist/start-CT2ZEFP2.js.map +0 -1
- package/dist/test-webserver-M2I3EV4J.js.map +0 -1
- /package/dist/{ClaudeContextManager-JKR4WGNU.js.map → BranchNamingService-3OQPRSWT.js.map} +0 -0
- /package/dist/{ClaudeService-55DQGB7T.js.map → ClaudeContextManager-MUQSDY2E.js.map} +0 -0
- /package/dist/{GitHubService-LWP4GKGH.js.map → ClaudeService-HG4VQ7AW.js.map} +0 -0
- /package/dist/{PromptTemplateManager-WII75TKH.js.map → GitHubService-EBOETDIW.js.map} +0 -0
- /package/dist/{SettingsManager-SKLUVE3K.js.map → ProjectCapabilityDetector-34LU7JJ4.js.map} +0 -0
- /package/dist/{claude-7LUVDZZ4.js.map → PromptTemplateManager-A52RUAMS.js.map} +0 -0
- /package/dist/{git-OUYMVYJX.js.map → SettingsManager-WHHFGSL7.js.map} +0 -0
- /package/dist/{neon-helpers-ZVIRPKCI.js.map → SettingsMigrationManager-AGIIIPDQ.js.map} +0 -0
- /package/dist/{chunk-DEPYQRRB.js.map → chunk-C5QCTEQK.js.map} +0 -0
- /package/dist/{chunk-CWR2SANQ.js.map → chunk-EBISESAP.js.map} +0 -0
- /package/dist/{SettingsMigrationManager-MTQIMI54.js.map → chunk-KLBYVHPK.js.map} +0 -0
- /package/dist/{chunk-USVVV3FP.js.map → chunk-MKWYLDFK.js.map} +0 -0
- /package/dist/{chunk-GZP4UGGM.js.map → chunk-ZM3CFL5L.js.map} +0 -0
- /package/dist/{prompt-ANTQWHUF.js.map → claude-GOP6PFC7.js.map} +0 -0
- /package/dist/{remote-GJEZWRCC.js.map → git-TDXKRTXM.js.map} +0 -0
- /package/dist/{terminal-3D6TUAKJ.js.map → neon-helpers-WPUACUVC.js.map} +0 -0
- /package/dist/{rebase-KBWFDZCN.js.map → rebase-Q7GMM7EI.js.map} +0 -0
- /package/dist/{test-git-MKZATGZN.js.map → test-git-3WDLNQCA.js.map} +0 -0
- /package/dist/{test-prefix-ZNLWDI3K.js.map → test-prefix-EVGAWAJW.js.map} +0 -0
- /package/dist/{test-tabs-JRKY3QMM.js.map → test-tabs-RXDBZ6J7.js.map} +0 -0
package/dist/cli.js
CHANGED
|
@@ -1,11 +1,94 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
+
import {
|
|
3
|
+
IssueTrackerFactory,
|
|
4
|
+
generateIssueManagementMcpConfig
|
|
5
|
+
} from "./chunk-RO26VS3W.js";
|
|
6
|
+
import {
|
|
7
|
+
CLIIsolationManager,
|
|
8
|
+
DatabaseManager,
|
|
9
|
+
EnvironmentManager,
|
|
10
|
+
LoomManager,
|
|
11
|
+
ResourceCleanup
|
|
12
|
+
} from "./chunk-5EF7Z346.js";
|
|
13
|
+
import {
|
|
14
|
+
IdentifierParser,
|
|
15
|
+
loadEnvIntoProcess
|
|
16
|
+
} from "./chunk-IP7SMKIF.js";
|
|
17
|
+
import {
|
|
18
|
+
ProcessManager
|
|
19
|
+
} from "./chunk-VU3QMIP2.js";
|
|
20
|
+
import {
|
|
21
|
+
createNeonProviderFromSettings
|
|
22
|
+
} from "./chunk-JQFO7QQN.js";
|
|
2
23
|
import {
|
|
3
24
|
ShellCompletion
|
|
4
|
-
} from "./chunk-
|
|
25
|
+
} from "./chunk-VAYCCUXW.js";
|
|
26
|
+
import {
|
|
27
|
+
getConfiguredRepoFromSettings,
|
|
28
|
+
getEffectivePRTargetRemote,
|
|
29
|
+
hasMultipleRemotes,
|
|
30
|
+
parseGitRemotes
|
|
31
|
+
} from "./chunk-PA6Q6AWM.js";
|
|
32
|
+
import {
|
|
33
|
+
IssueEnhancementService
|
|
34
|
+
} from "./chunk-47KSHUCR.js";
|
|
35
|
+
import {
|
|
36
|
+
AgentManager
|
|
37
|
+
} from "./chunk-OC4H6HJD.js";
|
|
38
|
+
import {
|
|
39
|
+
openBrowser
|
|
40
|
+
} from "./chunk-YETJNRQM.js";
|
|
41
|
+
import {
|
|
42
|
+
MergeManager
|
|
43
|
+
} from "./chunk-AWOFAD5O.js";
|
|
5
44
|
import {
|
|
6
45
|
GitWorktreeManager
|
|
7
|
-
} from "./chunk-
|
|
8
|
-
import
|
|
46
|
+
} from "./chunk-4HHRTA7Q.js";
|
|
47
|
+
import {
|
|
48
|
+
detectPackageManager,
|
|
49
|
+
installDependencies,
|
|
50
|
+
runScript
|
|
51
|
+
} from "./chunk-ZT3YZB4K.js";
|
|
52
|
+
import {
|
|
53
|
+
ClaudeContextManager
|
|
54
|
+
} from "./chunk-C5QCTEQK.js";
|
|
55
|
+
import "./chunk-FIAT22G7.js";
|
|
56
|
+
import {
|
|
57
|
+
detectClaudeCli,
|
|
58
|
+
launchClaude
|
|
59
|
+
} from "./chunk-XXV3UFZL.js";
|
|
60
|
+
import "./chunk-WEN5C5DM.js";
|
|
61
|
+
import {
|
|
62
|
+
DefaultBranchNamingService
|
|
63
|
+
} from "./chunk-3KATJIKO.js";
|
|
64
|
+
import {
|
|
65
|
+
ProjectCapabilityDetector
|
|
66
|
+
} from "./chunk-EBISESAP.js";
|
|
67
|
+
import {
|
|
68
|
+
hasScript,
|
|
69
|
+
readPackageJson
|
|
70
|
+
} from "./chunk-2ZPFJQ3B.js";
|
|
71
|
+
import {
|
|
72
|
+
extractSettingsOverrides
|
|
73
|
+
} from "./chunk-GYCR2LOU.js";
|
|
74
|
+
import {
|
|
75
|
+
SettingsManager
|
|
76
|
+
} from "./chunk-ML3NRPNB.js";
|
|
77
|
+
import {
|
|
78
|
+
executeGitCommand,
|
|
79
|
+
extractIssueNumber,
|
|
80
|
+
findMainWorktreePathWithSettings,
|
|
81
|
+
pushBranchToRemote
|
|
82
|
+
} from "./chunk-MAVL6PJF.js";
|
|
83
|
+
import "./chunk-G2IEYOLQ.js";
|
|
84
|
+
import {
|
|
85
|
+
executeGhCommand
|
|
86
|
+
} from "./chunk-3RUPPQRG.js";
|
|
87
|
+
import {
|
|
88
|
+
promptConfirmation,
|
|
89
|
+
waitForKeypress
|
|
90
|
+
} from "./chunk-JKXJ7BGL.js";
|
|
91
|
+
import "./chunk-ZZZWQGTS.js";
|
|
9
92
|
import {
|
|
10
93
|
logger
|
|
11
94
|
} from "./chunk-GEHQXLEI.js";
|
|
@@ -13,6 +96,2342 @@ import {
|
|
|
13
96
|
// src/cli.ts
|
|
14
97
|
import { program, Option } from "commander";
|
|
15
98
|
|
|
99
|
+
// src/commands/start.ts
|
|
100
|
+
import path2 from "path";
|
|
101
|
+
|
|
102
|
+
// src/utils/first-run-setup.ts
|
|
103
|
+
import { existsSync } from "fs";
|
|
104
|
+
import { readFile } from "fs/promises";
|
|
105
|
+
import path from "path";
|
|
106
|
+
async function needsFirstRunSetup() {
|
|
107
|
+
const iloomDir = path.join(process.cwd(), ".iloom");
|
|
108
|
+
if (!existsSync(iloomDir)) {
|
|
109
|
+
return true;
|
|
110
|
+
}
|
|
111
|
+
const settingsPath = path.join(iloomDir, "settings.json");
|
|
112
|
+
const settingsLocalPath = path.join(iloomDir, "settings.local.json");
|
|
113
|
+
const hasSettings = await hasNonEmptySettings(settingsPath);
|
|
114
|
+
const hasLocalSettings = await hasNonEmptySettings(settingsLocalPath);
|
|
115
|
+
return !hasSettings && !hasLocalSettings;
|
|
116
|
+
}
|
|
117
|
+
async function hasNonEmptySettings(filePath) {
|
|
118
|
+
if (!existsSync(filePath)) return false;
|
|
119
|
+
try {
|
|
120
|
+
const content = await readFile(filePath, "utf-8");
|
|
121
|
+
const parsed = JSON.parse(content);
|
|
122
|
+
return Object.keys(parsed).length > 0;
|
|
123
|
+
} catch {
|
|
124
|
+
return false;
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
async function launchFirstRunSetup() {
|
|
128
|
+
logger.info("First-time project setup detected.");
|
|
129
|
+
logger.info(
|
|
130
|
+
"iloom will now launch an interactive configuration session with Claude."
|
|
131
|
+
);
|
|
132
|
+
const { waitForKeypress: waitForKeypress2 } = await import("./prompt-7INJ7YRU.js");
|
|
133
|
+
await waitForKeypress2("Press any key to start configuration...");
|
|
134
|
+
const { InitCommand } = await import("./init-G3T64SC4.js");
|
|
135
|
+
const initCommand = new InitCommand();
|
|
136
|
+
await initCommand.execute(
|
|
137
|
+
"Help me configure iloom settings for this project. This is my first time using iloom here. Note: Your iloom command will execute once we are done with configuration changes."
|
|
138
|
+
);
|
|
139
|
+
logger.info("Configuration complete! Continuing with your original command...");
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
// src/commands/start.ts
|
|
143
|
+
var StartCommand = class {
|
|
144
|
+
constructor(issueTracker, loomManager, _agentManager, settingsManager) {
|
|
145
|
+
this.loomManager = null;
|
|
146
|
+
this.issueTracker = issueTracker;
|
|
147
|
+
this.settingsManager = settingsManager ?? new SettingsManager();
|
|
148
|
+
this.providedLoomManager = loomManager;
|
|
149
|
+
const envResult = loadEnvIntoProcess();
|
|
150
|
+
if (envResult.error) {
|
|
151
|
+
logger.debug(`Environment loading warning: ${envResult.error.message}`);
|
|
152
|
+
}
|
|
153
|
+
if (envResult.parsed) {
|
|
154
|
+
logger.debug(`Loaded ${Object.keys(envResult.parsed).length} environment variables`);
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
/**
|
|
158
|
+
* Initialize LoomManager with the main worktree path
|
|
159
|
+
* Uses lazy initialization to ensure we have the correct path
|
|
160
|
+
*/
|
|
161
|
+
async initializeLoomManager() {
|
|
162
|
+
var _a, _b;
|
|
163
|
+
if (this.loomManager) {
|
|
164
|
+
return this.loomManager;
|
|
165
|
+
}
|
|
166
|
+
if (this.providedLoomManager) {
|
|
167
|
+
this.loomManager = this.providedLoomManager;
|
|
168
|
+
return this.loomManager;
|
|
169
|
+
}
|
|
170
|
+
const mainWorktreePath = await findMainWorktreePathWithSettings();
|
|
171
|
+
const settings = await this.settingsManager.loadSettings();
|
|
172
|
+
const environmentManager = new EnvironmentManager();
|
|
173
|
+
const neonProvider = createNeonProviderFromSettings(settings);
|
|
174
|
+
const databaseUrlEnvVarName = ((_b = (_a = settings.capabilities) == null ? void 0 : _a.database) == null ? void 0 : _b.databaseUrlEnvVarName) ?? "DATABASE_URL";
|
|
175
|
+
const databaseManager = new DatabaseManager(neonProvider, environmentManager, databaseUrlEnvVarName);
|
|
176
|
+
const branchNaming = new DefaultBranchNamingService({ useClaude: true });
|
|
177
|
+
this.loomManager = new LoomManager(
|
|
178
|
+
new GitWorktreeManager(mainWorktreePath),
|
|
179
|
+
this.issueTracker,
|
|
180
|
+
branchNaming,
|
|
181
|
+
// Add branch naming service
|
|
182
|
+
environmentManager,
|
|
183
|
+
// Reuse same instance
|
|
184
|
+
new ClaudeContextManager(),
|
|
185
|
+
new ProjectCapabilityDetector(),
|
|
186
|
+
new CLIIsolationManager(),
|
|
187
|
+
this.settingsManager,
|
|
188
|
+
// Use same instance with CLI overrides
|
|
189
|
+
databaseManager
|
|
190
|
+
// Add database manager
|
|
191
|
+
);
|
|
192
|
+
return this.loomManager;
|
|
193
|
+
}
|
|
194
|
+
/**
|
|
195
|
+
* Main entry point for the start command
|
|
196
|
+
*/
|
|
197
|
+
async execute(input) {
|
|
198
|
+
var _a, _b, _c, _d;
|
|
199
|
+
try {
|
|
200
|
+
const initialSettings = await this.settingsManager.loadSettings();
|
|
201
|
+
if (process.env.FORCE_FIRST_TIME_SETUP === "true" || await needsFirstRunSetup()) {
|
|
202
|
+
await launchFirstRunSetup();
|
|
203
|
+
const newSettings = await this.settingsManager.loadSettings();
|
|
204
|
+
const newProvider = ((_a = newSettings.issueManagement) == null ? void 0 : _a.provider) ?? "github";
|
|
205
|
+
if (newProvider !== this.issueTracker.providerName) {
|
|
206
|
+
logger.debug(`Reinitializing issue tracker: provider changed to "${newProvider}"`);
|
|
207
|
+
this.issueTracker = IssueTrackerFactory.create(newSettings);
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
let repo;
|
|
211
|
+
const multipleRemotes = await hasMultipleRemotes();
|
|
212
|
+
if (multipleRemotes) {
|
|
213
|
+
repo = await getConfiguredRepoFromSettings(initialSettings);
|
|
214
|
+
logger.info(`Using GitHub repository: ${repo}`);
|
|
215
|
+
}
|
|
216
|
+
const loomManager = await this.initializeLoomManager();
|
|
217
|
+
let parentLoom = await this.detectParentLoom(loomManager);
|
|
218
|
+
const parsed = await this.parseInput(input.identifier, repo);
|
|
219
|
+
await this.validateInput(parsed, repo);
|
|
220
|
+
if (parentLoom) {
|
|
221
|
+
const { isInteractiveEnvironment, promptConfirmation: promptConfirmation2 } = await import("./prompt-7INJ7YRU.js");
|
|
222
|
+
const parentDisplay = parentLoom.type === "issue" ? `issue #${parentLoom.identifier}` : parentLoom.type === "pr" ? `PR #${parentLoom.identifier}` : `branch ${parentLoom.identifier}`;
|
|
223
|
+
if (input.options.childLoom === true) {
|
|
224
|
+
logger.info(`Creating as child loom of ${parentDisplay} (--child-loom flag)`);
|
|
225
|
+
} else if (input.options.childLoom === false) {
|
|
226
|
+
parentLoom = null;
|
|
227
|
+
logger.info("Creating as independent loom (--no-child-loom flag)");
|
|
228
|
+
} else {
|
|
229
|
+
let createAsChild = true;
|
|
230
|
+
if (isInteractiveEnvironment()) {
|
|
231
|
+
createAsChild = await promptConfirmation2(
|
|
232
|
+
`You are not in your main worktree. Create as a child loom of ${parentDisplay}?`,
|
|
233
|
+
true
|
|
234
|
+
// Default yes
|
|
235
|
+
);
|
|
236
|
+
} else {
|
|
237
|
+
logger.error(`Non-interactive environment detected, use either --child-loom or --no-child-loom to specify behavior`);
|
|
238
|
+
process.exit(1);
|
|
239
|
+
}
|
|
240
|
+
if (!createAsChild) {
|
|
241
|
+
parentLoom = null;
|
|
242
|
+
logger.info("Creating as independent loom");
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
} else if (input.options.childLoom === true) {
|
|
246
|
+
logger.debug("--child-loom flag provided but not running from inside an existing loom (ignored)");
|
|
247
|
+
}
|
|
248
|
+
if (parsed.type === "description") {
|
|
249
|
+
logger.info("Creating GitHub issue from description...");
|
|
250
|
+
const body = input.options.body ?? "";
|
|
251
|
+
const result = await this.issueTracker.createIssue(
|
|
252
|
+
parsed.originalInput,
|
|
253
|
+
// Use description as title
|
|
254
|
+
body
|
|
255
|
+
// Use provided body or empty
|
|
256
|
+
);
|
|
257
|
+
logger.success(`Created issue #${result.number}: ${result.url}`);
|
|
258
|
+
parsed.type = "issue";
|
|
259
|
+
parsed.number = result.number;
|
|
260
|
+
}
|
|
261
|
+
if (input.options.oneShot === "bypassPermissions") {
|
|
262
|
+
const { promptConfirmation: promptConfirmation2 } = await import("./prompt-7INJ7YRU.js");
|
|
263
|
+
const confirmed = await promptConfirmation2(
|
|
264
|
+
"\u26A0\uFE0F WARNING: bypassPermissions mode will allow Claude to execute all tool calls without confirmation. This can be dangerous. Do you want to proceed?"
|
|
265
|
+
);
|
|
266
|
+
if (!confirmed) {
|
|
267
|
+
logger.info("Operation cancelled by user");
|
|
268
|
+
process.exit(0);
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
const cliOverrides = extractSettingsOverrides();
|
|
272
|
+
const settings = await this.settingsManager.loadSettings(void 0, cliOverrides);
|
|
273
|
+
const workflowType = parsed.type === "branch" ? "regular" : parsed.type;
|
|
274
|
+
const workflowConfig = (_b = settings.workflows) == null ? void 0 : _b[workflowType];
|
|
275
|
+
const { extractRawSetArguments, getExecutablePath } = await import("./cli-overrides-XFZWY7CM.js");
|
|
276
|
+
const setArguments = extractRawSetArguments();
|
|
277
|
+
const executablePath = getExecutablePath();
|
|
278
|
+
logger.info(`\u2705 Validated input: ${this.formatParsedInput(parsed)}`);
|
|
279
|
+
const identifier = parsed.type === "branch" ? parsed.branchName ?? "" : parsed.number ?? 0;
|
|
280
|
+
const enableClaude = input.options.claude ?? (workflowConfig == null ? void 0 : workflowConfig.startAiAgent) ?? true;
|
|
281
|
+
const enableCode = input.options.code ?? (workflowConfig == null ? void 0 : workflowConfig.startIde) ?? true;
|
|
282
|
+
const enableDevServer = input.options.devServer ?? (workflowConfig == null ? void 0 : workflowConfig.startDevServer) ?? true;
|
|
283
|
+
const enableTerminal = input.options.terminal ?? (workflowConfig == null ? void 0 : workflowConfig.startTerminal) ?? false;
|
|
284
|
+
logger.debug("Final workflow config values:", {
|
|
285
|
+
enableClaude,
|
|
286
|
+
enableCode,
|
|
287
|
+
enableDevServer,
|
|
288
|
+
enableTerminal
|
|
289
|
+
});
|
|
290
|
+
const loom = await loomManager.createIloom({
|
|
291
|
+
type: parsed.type,
|
|
292
|
+
identifier,
|
|
293
|
+
originalInput: parsed.originalInput,
|
|
294
|
+
...parentLoom && { parentLoom },
|
|
295
|
+
options: {
|
|
296
|
+
enableClaude,
|
|
297
|
+
enableCode,
|
|
298
|
+
enableDevServer,
|
|
299
|
+
enableTerminal,
|
|
300
|
+
...input.options.oneShot && { oneShot: input.options.oneShot },
|
|
301
|
+
...setArguments.length > 0 && { setArguments },
|
|
302
|
+
...executablePath && { executablePath }
|
|
303
|
+
}
|
|
304
|
+
});
|
|
305
|
+
logger.success(`\u2705 Created loom: ${loom.id} at ${loom.path}`);
|
|
306
|
+
logger.info(` Branch: ${loom.branch}`);
|
|
307
|
+
if ((_c = loom.capabilities) == null ? void 0 : _c.includes("web")) {
|
|
308
|
+
logger.info(` Port: ${loom.port}`);
|
|
309
|
+
}
|
|
310
|
+
if ((_d = loom.issueData) == null ? void 0 : _d.title) {
|
|
311
|
+
logger.info(` Title: ${loom.issueData.title}`);
|
|
312
|
+
}
|
|
313
|
+
} catch (error) {
|
|
314
|
+
if (error instanceof Error) {
|
|
315
|
+
logger.error(`\u274C ${error.message}`);
|
|
316
|
+
} else {
|
|
317
|
+
logger.error("\u274C An unknown error occurred");
|
|
318
|
+
}
|
|
319
|
+
throw error;
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
/**
|
|
323
|
+
* Parse input to determine type and extract relevant data
|
|
324
|
+
*/
|
|
325
|
+
async parseInput(identifier, repo) {
|
|
326
|
+
const trimmedIdentifier = identifier.trim();
|
|
327
|
+
if (!trimmedIdentifier) {
|
|
328
|
+
throw new Error("Missing required argument: identifier");
|
|
329
|
+
}
|
|
330
|
+
const spaceCount = (trimmedIdentifier.match(/ /g) ?? []).length;
|
|
331
|
+
if (trimmedIdentifier.length > 25 && spaceCount > 2) {
|
|
332
|
+
return {
|
|
333
|
+
type: "description",
|
|
334
|
+
originalInput: trimmedIdentifier
|
|
335
|
+
};
|
|
336
|
+
}
|
|
337
|
+
const prPattern = /^pr[/-](\d+)$/i;
|
|
338
|
+
const prMatch = trimmedIdentifier.match(prPattern);
|
|
339
|
+
if (prMatch == null ? void 0 : prMatch[1]) {
|
|
340
|
+
return {
|
|
341
|
+
type: "pr",
|
|
342
|
+
number: parseInt(prMatch[1], 10),
|
|
343
|
+
originalInput: trimmedIdentifier
|
|
344
|
+
};
|
|
345
|
+
}
|
|
346
|
+
const linearPattern = /^([A-Z]{2,}-\d+)$/i;
|
|
347
|
+
const linearMatch = trimmedIdentifier.match(linearPattern);
|
|
348
|
+
if (linearMatch == null ? void 0 : linearMatch[1]) {
|
|
349
|
+
const detection = await this.issueTracker.detectInputType(
|
|
350
|
+
trimmedIdentifier,
|
|
351
|
+
repo
|
|
352
|
+
);
|
|
353
|
+
if (detection.type === "issue" && detection.identifier) {
|
|
354
|
+
return {
|
|
355
|
+
type: "issue",
|
|
356
|
+
number: detection.identifier,
|
|
357
|
+
// Keep as string for Linear
|
|
358
|
+
originalInput: trimmedIdentifier
|
|
359
|
+
};
|
|
360
|
+
}
|
|
361
|
+
throw new Error(
|
|
362
|
+
`Could not find Linear issue ${linearMatch[1].toUpperCase()}`
|
|
363
|
+
);
|
|
364
|
+
}
|
|
365
|
+
const numericPattern = /^#?(\d+)$/;
|
|
366
|
+
const numericMatch = trimmedIdentifier.match(numericPattern);
|
|
367
|
+
if (numericMatch == null ? void 0 : numericMatch[1]) {
|
|
368
|
+
const number = parseInt(numericMatch[1], 10);
|
|
369
|
+
const detection = await this.issueTracker.detectInputType(
|
|
370
|
+
trimmedIdentifier,
|
|
371
|
+
repo
|
|
372
|
+
);
|
|
373
|
+
if (detection.type === "pr") {
|
|
374
|
+
return {
|
|
375
|
+
type: "pr",
|
|
376
|
+
number: detection.identifier ? parseInt(detection.identifier, 10) : number,
|
|
377
|
+
originalInput: trimmedIdentifier
|
|
378
|
+
};
|
|
379
|
+
} else if (detection.type === "issue") {
|
|
380
|
+
return {
|
|
381
|
+
type: "issue",
|
|
382
|
+
number: detection.identifier ? parseInt(detection.identifier, 10) : number,
|
|
383
|
+
originalInput: trimmedIdentifier
|
|
384
|
+
};
|
|
385
|
+
} else {
|
|
386
|
+
throw new Error(`Could not find issue or PR #${number}`);
|
|
387
|
+
}
|
|
388
|
+
}
|
|
389
|
+
return {
|
|
390
|
+
type: "branch",
|
|
391
|
+
branchName: trimmedIdentifier,
|
|
392
|
+
originalInput: trimmedIdentifier
|
|
393
|
+
};
|
|
394
|
+
}
|
|
395
|
+
/**
|
|
396
|
+
* Validate the parsed input based on its type
|
|
397
|
+
*/
|
|
398
|
+
async validateInput(parsed, repo) {
|
|
399
|
+
switch (parsed.type) {
|
|
400
|
+
case "pr": {
|
|
401
|
+
if (!parsed.number) {
|
|
402
|
+
throw new Error("Invalid PR number");
|
|
403
|
+
}
|
|
404
|
+
if (!this.issueTracker.supportsPullRequests || !this.issueTracker.fetchPR || !this.issueTracker.validatePRState) {
|
|
405
|
+
throw new Error("Issue tracker does not support pull requests");
|
|
406
|
+
}
|
|
407
|
+
const pr = await this.issueTracker.fetchPR(parsed.number, repo);
|
|
408
|
+
await this.issueTracker.validatePRState(pr);
|
|
409
|
+
logger.debug(`Validated PR #${parsed.number}`);
|
|
410
|
+
break;
|
|
411
|
+
}
|
|
412
|
+
case "issue": {
|
|
413
|
+
if (!parsed.number) {
|
|
414
|
+
throw new Error("Invalid issue number");
|
|
415
|
+
}
|
|
416
|
+
const issue = await this.issueTracker.fetchIssue(parsed.number, repo);
|
|
417
|
+
await this.issueTracker.validateIssueState(issue);
|
|
418
|
+
logger.debug(`Validated issue #${parsed.number}`);
|
|
419
|
+
break;
|
|
420
|
+
}
|
|
421
|
+
case "branch": {
|
|
422
|
+
if (!parsed.branchName) {
|
|
423
|
+
throw new Error("Invalid branch name");
|
|
424
|
+
}
|
|
425
|
+
if (!this.isValidBranchName(parsed.branchName)) {
|
|
426
|
+
throw new Error(
|
|
427
|
+
"Invalid branch name. Use only letters, numbers, hyphens, underscores, and slashes"
|
|
428
|
+
);
|
|
429
|
+
}
|
|
430
|
+
logger.debug(`Validated branch name: ${parsed.branchName}`);
|
|
431
|
+
break;
|
|
432
|
+
}
|
|
433
|
+
case "description": {
|
|
434
|
+
logger.debug("Detected description input", {
|
|
435
|
+
length: parsed.originalInput.length
|
|
436
|
+
});
|
|
437
|
+
break;
|
|
438
|
+
}
|
|
439
|
+
default: {
|
|
440
|
+
const unknownType = parsed;
|
|
441
|
+
throw new Error(`Unknown input type: ${unknownType.type}`);
|
|
442
|
+
}
|
|
443
|
+
}
|
|
444
|
+
}
|
|
445
|
+
/**
|
|
446
|
+
* Validate branch name format
|
|
447
|
+
*/
|
|
448
|
+
isValidBranchName(branch) {
|
|
449
|
+
return /^[a-zA-Z0-9/_-]+$/.test(branch);
|
|
450
|
+
}
|
|
451
|
+
/**
|
|
452
|
+
* Format parsed input for display
|
|
453
|
+
*/
|
|
454
|
+
formatParsedInput(parsed) {
|
|
455
|
+
switch (parsed.type) {
|
|
456
|
+
case "pr":
|
|
457
|
+
return `PR #${parsed.number}`;
|
|
458
|
+
case "issue":
|
|
459
|
+
return `Issue #${parsed.number}`;
|
|
460
|
+
case "branch":
|
|
461
|
+
return `Branch '${parsed.branchName}'`;
|
|
462
|
+
case "description":
|
|
463
|
+
return `Description: ${parsed.originalInput.slice(0, 50)}...`;
|
|
464
|
+
default:
|
|
465
|
+
return "Unknown input";
|
|
466
|
+
}
|
|
467
|
+
}
|
|
468
|
+
/**
|
|
469
|
+
* Detect if running from inside an existing loom worktree
|
|
470
|
+
* Returns parent loom info if detected, null otherwise
|
|
471
|
+
*/
|
|
472
|
+
async detectParentLoom(loomManager) {
|
|
473
|
+
try {
|
|
474
|
+
const cwd = process.cwd();
|
|
475
|
+
const looms = await loomManager.listLooms();
|
|
476
|
+
if (!looms) {
|
|
477
|
+
return null;
|
|
478
|
+
}
|
|
479
|
+
const mainWorktreePath = await findMainWorktreePathWithSettings();
|
|
480
|
+
const parentLoom = looms.find((loom) => {
|
|
481
|
+
if (loom.path === mainWorktreePath) {
|
|
482
|
+
return false;
|
|
483
|
+
}
|
|
484
|
+
return cwd === loom.path || cwd.startsWith(loom.path + path2.sep);
|
|
485
|
+
});
|
|
486
|
+
if (!parentLoom) {
|
|
487
|
+
return null;
|
|
488
|
+
}
|
|
489
|
+
logger.debug(`Detected parent loom: ${parentLoom.type} ${parentLoom.identifier} at ${parentLoom.path}`);
|
|
490
|
+
const result = {
|
|
491
|
+
type: parentLoom.type,
|
|
492
|
+
identifier: parentLoom.identifier,
|
|
493
|
+
branchName: parentLoom.branch,
|
|
494
|
+
worktreePath: parentLoom.path
|
|
495
|
+
};
|
|
496
|
+
if (parentLoom.databaseBranch) {
|
|
497
|
+
result.databaseBranch = parentLoom.databaseBranch;
|
|
498
|
+
}
|
|
499
|
+
if (!result.databaseBranch) {
|
|
500
|
+
const databaseBranch = await loomManager.getDatabaseBranchForLoom(parentLoom.path);
|
|
501
|
+
if (databaseBranch) {
|
|
502
|
+
result.databaseBranch = databaseBranch;
|
|
503
|
+
logger.debug(`Detected parent database branch: ${databaseBranch}`);
|
|
504
|
+
}
|
|
505
|
+
}
|
|
506
|
+
return result;
|
|
507
|
+
} catch (error) {
|
|
508
|
+
logger.debug(`Failed to detect parent loom: ${error instanceof Error ? error.message : "Unknown error"}`);
|
|
509
|
+
return null;
|
|
510
|
+
}
|
|
511
|
+
}
|
|
512
|
+
};
|
|
513
|
+
|
|
514
|
+
// src/commands/add-issue.ts
|
|
515
|
+
var AddIssueCommand = class {
|
|
516
|
+
constructor(enhancementService, settingsManager) {
|
|
517
|
+
this.enhancementService = enhancementService;
|
|
518
|
+
this.settingsManager = settingsManager ?? new SettingsManager();
|
|
519
|
+
}
|
|
520
|
+
/**
|
|
521
|
+
* Execute the add-issue command workflow:
|
|
522
|
+
* 1. Validate description format
|
|
523
|
+
* 2. Enhance description with Claude AI
|
|
524
|
+
* 3. Create GitHub issue
|
|
525
|
+
* 4. Wait for keypress and open browser for review
|
|
526
|
+
* 5. Return issue number
|
|
527
|
+
*/
|
|
528
|
+
async execute(input) {
|
|
529
|
+
const { description } = input;
|
|
530
|
+
const settings = await this.settingsManager.loadSettings();
|
|
531
|
+
let repo;
|
|
532
|
+
const multipleRemotes = await hasMultipleRemotes();
|
|
533
|
+
if (multipleRemotes) {
|
|
534
|
+
repo = await getConfiguredRepoFromSettings(settings);
|
|
535
|
+
logger.info(`Using GitHub repository: ${repo}`);
|
|
536
|
+
}
|
|
537
|
+
if (!description || !this.enhancementService.validateDescription(description)) {
|
|
538
|
+
throw new Error("Description is required and must be more than 30 characters with at least 3 words");
|
|
539
|
+
}
|
|
540
|
+
const enhancedDescription = await this.enhancementService.enhanceDescription(description);
|
|
541
|
+
const result = await this.enhancementService.createEnhancedIssue(
|
|
542
|
+
description,
|
|
543
|
+
enhancedDescription,
|
|
544
|
+
repo
|
|
545
|
+
);
|
|
546
|
+
await this.enhancementService.waitForReviewAndOpen(result.number);
|
|
547
|
+
return result.number;
|
|
548
|
+
}
|
|
549
|
+
};
|
|
550
|
+
|
|
551
|
+
// src/commands/enhance.ts
|
|
552
|
+
var EnhanceCommand = class {
|
|
553
|
+
constructor(issueTracker, agentManager, settingsManager) {
|
|
554
|
+
this.issueTracker = issueTracker;
|
|
555
|
+
this.agentManager = agentManager ?? new AgentManager();
|
|
556
|
+
this.settingsManager = settingsManager ?? new SettingsManager();
|
|
557
|
+
}
|
|
558
|
+
/**
|
|
559
|
+
* Execute the enhance command workflow:
|
|
560
|
+
* 1. Validate issue number
|
|
561
|
+
* 2. Fetch issue to verify it exists
|
|
562
|
+
* 3. Load agent configurations
|
|
563
|
+
* 4. Invoke Claude CLI with enhancer agent
|
|
564
|
+
* 5. Parse response to determine outcome
|
|
565
|
+
* 6. Handle browser interaction based on outcome
|
|
566
|
+
*/
|
|
567
|
+
async execute(input) {
|
|
568
|
+
const { issueNumber, options } = input;
|
|
569
|
+
const { author } = options;
|
|
570
|
+
const settings = await this.settingsManager.loadSettings();
|
|
571
|
+
let repo;
|
|
572
|
+
const multipleRemotes = await hasMultipleRemotes();
|
|
573
|
+
if (multipleRemotes) {
|
|
574
|
+
repo = await getConfiguredRepoFromSettings(settings);
|
|
575
|
+
logger.info(`Using GitHub repository: ${repo}`);
|
|
576
|
+
}
|
|
577
|
+
this.validateIssueNumber(issueNumber);
|
|
578
|
+
logger.info(`Fetching issue #${issueNumber}...`);
|
|
579
|
+
const issue = await this.issueTracker.fetchIssue(issueNumber, repo);
|
|
580
|
+
logger.debug("Issue fetched successfully", { number: issue.number, title: issue.title });
|
|
581
|
+
logger.debug("Loading agent configurations...");
|
|
582
|
+
const loadedAgents = await this.agentManager.loadAgents(settings);
|
|
583
|
+
const agents = this.agentManager.formatForCli(loadedAgents);
|
|
584
|
+
let mcpConfig;
|
|
585
|
+
let allowedTools;
|
|
586
|
+
let disallowedTools;
|
|
587
|
+
try {
|
|
588
|
+
const provider = this.issueTracker.providerName;
|
|
589
|
+
mcpConfig = await generateIssueManagementMcpConfig("issue", repo, provider, settings);
|
|
590
|
+
logger.debug("Generated MCP configuration for issue management:", { mcpConfig });
|
|
591
|
+
allowedTools = [
|
|
592
|
+
"mcp__issue_management__get_issue",
|
|
593
|
+
"mcp__issue_management__get_comment",
|
|
594
|
+
"mcp__issue_management__create_comment",
|
|
595
|
+
"mcp__issue_management__update_comment"
|
|
596
|
+
];
|
|
597
|
+
disallowedTools = ["Bash(gh api:*)"];
|
|
598
|
+
logger.debug("Configured tool filtering for issue workflow", { allowedTools, disallowedTools });
|
|
599
|
+
} catch (error) {
|
|
600
|
+
logger.warn(`Failed to generate MCP config: ${error instanceof Error ? error.message : "Unknown error"}`);
|
|
601
|
+
}
|
|
602
|
+
logger.info("Invoking enhancer agent. This may take a moment...");
|
|
603
|
+
const prompt = this.constructPrompt(issueNumber, author);
|
|
604
|
+
const response = await launchClaude(prompt, {
|
|
605
|
+
headless: true,
|
|
606
|
+
model: "sonnet",
|
|
607
|
+
agents,
|
|
608
|
+
...mcpConfig && { mcpConfig },
|
|
609
|
+
...allowedTools && { allowedTools },
|
|
610
|
+
...disallowedTools && { disallowedTools }
|
|
611
|
+
});
|
|
612
|
+
const result = this.parseEnhancerResponse(response);
|
|
613
|
+
if (!result.enhanced) {
|
|
614
|
+
logger.success("Issue already has thorough description. No enhancement needed.");
|
|
615
|
+
return;
|
|
616
|
+
}
|
|
617
|
+
logger.success(`Issue #${issueNumber} enhanced successfully!`);
|
|
618
|
+
logger.info(`Enhanced specification available at: ${result.url}`);
|
|
619
|
+
if (!options.noBrowser && result.url) {
|
|
620
|
+
await this.promptAndOpenBrowser(result.url);
|
|
621
|
+
}
|
|
622
|
+
}
|
|
623
|
+
/**
|
|
624
|
+
* Validate that issue number is a valid positive integer
|
|
625
|
+
*/
|
|
626
|
+
validateIssueNumber(issueNumber) {
|
|
627
|
+
if (issueNumber === void 0 || issueNumber === null) {
|
|
628
|
+
throw new Error("Issue number is required");
|
|
629
|
+
}
|
|
630
|
+
if (typeof issueNumber === "number") {
|
|
631
|
+
if (Number.isNaN(issueNumber) || issueNumber <= 0 || !Number.isInteger(issueNumber)) {
|
|
632
|
+
throw new Error("Issue number must be a valid positive integer");
|
|
633
|
+
}
|
|
634
|
+
}
|
|
635
|
+
if (typeof issueNumber === "string" && issueNumber.trim().length === 0) {
|
|
636
|
+
throw new Error("Issue identifier cannot be empty");
|
|
637
|
+
}
|
|
638
|
+
}
|
|
639
|
+
/**
|
|
640
|
+
* Construct the prompt for the orchestrating Claude instance.
|
|
641
|
+
* This prompt is very clear about expected output format to ensure reliable parsing.
|
|
642
|
+
*/
|
|
643
|
+
constructPrompt(issueNumber, author) {
|
|
644
|
+
const authorInstruction = author ? `
|
|
645
|
+
IMPORTANT: When you create your analysis comment, tag @${author} in the "Questions for Reporter" section if you have questions.
|
|
646
|
+
` : "";
|
|
647
|
+
return `Execute @agent-iloom-issue-enhancer ${issueNumber}${authorInstruction}
|
|
648
|
+
|
|
649
|
+
## OUTPUT REQUIREMENTS
|
|
650
|
+
* If the issue was not enhanced, return ONLY: "No enhancement needed"
|
|
651
|
+
* If the issue WAS enhanced, return ONLY: <FULL URL OF THE COMMENT INCLUDING COMMENT ID>
|
|
652
|
+
* If you encounter permission/authentication/access errors, return ONLY: "Permission denied: <specific error description>"
|
|
653
|
+
* IMPORTANT: Return ONLY one of the above - DO NOT include commentary such as "I created a comment at <URL>" or "I examined the issue and found no enhancement was necessary"
|
|
654
|
+
* CONTEXT: Your output is going to be parsed programmatically, so adherence to the output requirements is CRITICAL.`;
|
|
655
|
+
}
|
|
656
|
+
/**
|
|
657
|
+
* Parse the response from the enhancer agent.
|
|
658
|
+
* Returns either { enhanced: false } or { enhanced: true, url: "..." }
|
|
659
|
+
* Throws specific errors for permission issues.
|
|
660
|
+
*/
|
|
661
|
+
parseEnhancerResponse(response) {
|
|
662
|
+
if (!response || typeof response !== "string") {
|
|
663
|
+
throw new Error("No response from enhancer agent");
|
|
664
|
+
}
|
|
665
|
+
const trimmed = response.trim();
|
|
666
|
+
logger.debug(`RESPONSE FROM ENHANCER AGENT: '${trimmed}'`);
|
|
667
|
+
if (trimmed.toLowerCase().startsWith("permission denied:")) {
|
|
668
|
+
const errorMessage = trimmed.substring("permission denied:".length).trim();
|
|
669
|
+
throw new Error(`Permission denied: ${errorMessage}`);
|
|
670
|
+
}
|
|
671
|
+
if (trimmed.toLowerCase().includes("no enhancement needed")) {
|
|
672
|
+
return { enhanced: false };
|
|
673
|
+
}
|
|
674
|
+
const urlPattern = /https:\/\/github\.com\/[^/]+\/[^/]+\/issues\/\d+#issuecomment-\d+/;
|
|
675
|
+
const match = trimmed.match(urlPattern);
|
|
676
|
+
if (match) {
|
|
677
|
+
return { enhanced: true, url: match[0] };
|
|
678
|
+
}
|
|
679
|
+
throw new Error(`Unexpected response from enhancer agent: ${trimmed}`);
|
|
680
|
+
}
|
|
681
|
+
/**
|
|
682
|
+
* Prompt user and open browser to view enhanced issue.
|
|
683
|
+
* Matches the pattern from the issue specification.
|
|
684
|
+
*/
|
|
685
|
+
async promptAndOpenBrowser(commentUrl) {
|
|
686
|
+
try {
|
|
687
|
+
const key = await waitForKeypress(
|
|
688
|
+
"Press q to quit or any other key to view the enhanced issue in a web browser..."
|
|
689
|
+
);
|
|
690
|
+
if (key.toLowerCase() === "q") {
|
|
691
|
+
logger.info("Skipping browser opening");
|
|
692
|
+
return;
|
|
693
|
+
}
|
|
694
|
+
await openBrowser(commentUrl);
|
|
695
|
+
} catch (error) {
|
|
696
|
+
logger.warn(`Failed to open browser: ${error instanceof Error ? error.message : "Unknown error"}`);
|
|
697
|
+
}
|
|
698
|
+
}
|
|
699
|
+
};
|
|
700
|
+
|
|
701
|
+
// src/lib/ValidationRunner.ts
|
|
702
|
+
var ValidationRunner = class {
|
|
703
|
+
/**
|
|
704
|
+
* Run all validations in sequence: typecheck → lint → test
|
|
705
|
+
* Fails fast on first error
|
|
706
|
+
*/
|
|
707
|
+
async runValidations(worktreePath, options = {}) {
|
|
708
|
+
const startTime = Date.now();
|
|
709
|
+
const steps = [];
|
|
710
|
+
if (!options.skipTypecheck) {
|
|
711
|
+
const typecheckResult = await this.runTypecheck(
|
|
712
|
+
worktreePath,
|
|
713
|
+
options.dryRun ?? false
|
|
714
|
+
);
|
|
715
|
+
steps.push(typecheckResult);
|
|
716
|
+
if (!typecheckResult.passed && !typecheckResult.skipped) {
|
|
717
|
+
return {
|
|
718
|
+
success: false,
|
|
719
|
+
steps,
|
|
720
|
+
totalDuration: Date.now() - startTime
|
|
721
|
+
};
|
|
722
|
+
}
|
|
723
|
+
}
|
|
724
|
+
if (!options.skipLint) {
|
|
725
|
+
const lintResult = await this.runLint(worktreePath, options.dryRun ?? false);
|
|
726
|
+
steps.push(lintResult);
|
|
727
|
+
if (!lintResult.passed && !lintResult.skipped) {
|
|
728
|
+
return { success: false, steps, totalDuration: Date.now() - startTime };
|
|
729
|
+
}
|
|
730
|
+
}
|
|
731
|
+
if (!options.skipTests) {
|
|
732
|
+
const testResult = await this.runTests(
|
|
733
|
+
worktreePath,
|
|
734
|
+
options.dryRun ?? false
|
|
735
|
+
);
|
|
736
|
+
steps.push(testResult);
|
|
737
|
+
if (!testResult.passed && !testResult.skipped) {
|
|
738
|
+
return { success: false, steps, totalDuration: Date.now() - startTime };
|
|
739
|
+
}
|
|
740
|
+
}
|
|
741
|
+
return { success: true, steps, totalDuration: Date.now() - startTime };
|
|
742
|
+
}
|
|
743
|
+
/**
|
|
744
|
+
* Run typecheck validation
|
|
745
|
+
*/
|
|
746
|
+
async runTypecheck(worktreePath, dryRun) {
|
|
747
|
+
const stepStartTime = Date.now();
|
|
748
|
+
try {
|
|
749
|
+
const pkgJson = await readPackageJson(worktreePath);
|
|
750
|
+
const hasTypecheckScript = hasScript(pkgJson, "typecheck");
|
|
751
|
+
if (!hasTypecheckScript) {
|
|
752
|
+
logger.debug("Skipping typecheck - no typecheck script found");
|
|
753
|
+
return {
|
|
754
|
+
step: "typecheck",
|
|
755
|
+
passed: true,
|
|
756
|
+
skipped: true,
|
|
757
|
+
duration: Date.now() - stepStartTime
|
|
758
|
+
};
|
|
759
|
+
}
|
|
760
|
+
} catch (error) {
|
|
761
|
+
if (error instanceof Error && error.message.includes("package.json not found")) {
|
|
762
|
+
logger.debug("Skipping typecheck - no package.json found (non-Node.js project)");
|
|
763
|
+
return {
|
|
764
|
+
step: "typecheck",
|
|
765
|
+
passed: true,
|
|
766
|
+
skipped: true,
|
|
767
|
+
duration: Date.now() - stepStartTime
|
|
768
|
+
};
|
|
769
|
+
}
|
|
770
|
+
throw error;
|
|
771
|
+
}
|
|
772
|
+
const packageManager = await detectPackageManager(worktreePath);
|
|
773
|
+
if (dryRun) {
|
|
774
|
+
const command = packageManager === "npm" ? "npm run typecheck" : `${packageManager} typecheck`;
|
|
775
|
+
logger.info(`[DRY RUN] Would run: ${command}`);
|
|
776
|
+
return {
|
|
777
|
+
step: "typecheck",
|
|
778
|
+
passed: true,
|
|
779
|
+
skipped: false,
|
|
780
|
+
duration: Date.now() - stepStartTime
|
|
781
|
+
};
|
|
782
|
+
}
|
|
783
|
+
logger.info("Running typecheck...");
|
|
784
|
+
try {
|
|
785
|
+
await runScript("typecheck", worktreePath, [], { quiet: true });
|
|
786
|
+
logger.success("Typecheck passed");
|
|
787
|
+
return {
|
|
788
|
+
step: "typecheck",
|
|
789
|
+
passed: true,
|
|
790
|
+
skipped: false,
|
|
791
|
+
duration: Date.now() - stepStartTime
|
|
792
|
+
};
|
|
793
|
+
} catch {
|
|
794
|
+
const fixed = await this.attemptClaudeFix(
|
|
795
|
+
"typecheck",
|
|
796
|
+
worktreePath,
|
|
797
|
+
packageManager
|
|
798
|
+
);
|
|
799
|
+
if (fixed) {
|
|
800
|
+
return {
|
|
801
|
+
step: "typecheck",
|
|
802
|
+
passed: true,
|
|
803
|
+
skipped: false,
|
|
804
|
+
duration: Date.now() - stepStartTime
|
|
805
|
+
};
|
|
806
|
+
}
|
|
807
|
+
const runCommand = packageManager === "npm" ? "npm run typecheck" : `${packageManager} typecheck`;
|
|
808
|
+
throw new Error(
|
|
809
|
+
`Error: Typecheck failed.
|
|
810
|
+
Fix type errors before merging.
|
|
811
|
+
|
|
812
|
+
Run '${runCommand}' to see detailed errors.`
|
|
813
|
+
);
|
|
814
|
+
}
|
|
815
|
+
}
|
|
816
|
+
/**
|
|
817
|
+
* Run lint validation
|
|
818
|
+
*/
|
|
819
|
+
async runLint(worktreePath, dryRun) {
|
|
820
|
+
const stepStartTime = Date.now();
|
|
821
|
+
try {
|
|
822
|
+
const pkgJson = await readPackageJson(worktreePath);
|
|
823
|
+
const hasLintScript = hasScript(pkgJson, "lint");
|
|
824
|
+
if (!hasLintScript) {
|
|
825
|
+
logger.debug("Skipping lint - no lint script found");
|
|
826
|
+
return {
|
|
827
|
+
step: "lint",
|
|
828
|
+
passed: true,
|
|
829
|
+
skipped: true,
|
|
830
|
+
duration: Date.now() - stepStartTime
|
|
831
|
+
};
|
|
832
|
+
}
|
|
833
|
+
} catch (error) {
|
|
834
|
+
if (error instanceof Error && error.message.includes("package.json not found")) {
|
|
835
|
+
logger.debug("Skipping lint - no package.json found (non-Node.js project)");
|
|
836
|
+
return {
|
|
837
|
+
step: "lint",
|
|
838
|
+
passed: true,
|
|
839
|
+
skipped: true,
|
|
840
|
+
duration: Date.now() - stepStartTime
|
|
841
|
+
};
|
|
842
|
+
}
|
|
843
|
+
throw error;
|
|
844
|
+
}
|
|
845
|
+
const packageManager = await detectPackageManager(worktreePath);
|
|
846
|
+
if (dryRun) {
|
|
847
|
+
const command = packageManager === "npm" ? "npm run lint" : `${packageManager} lint`;
|
|
848
|
+
logger.info(`[DRY RUN] Would run: ${command}`);
|
|
849
|
+
return {
|
|
850
|
+
step: "lint",
|
|
851
|
+
passed: true,
|
|
852
|
+
skipped: false,
|
|
853
|
+
duration: Date.now() - stepStartTime
|
|
854
|
+
};
|
|
855
|
+
}
|
|
856
|
+
logger.info("Running lint...");
|
|
857
|
+
try {
|
|
858
|
+
await runScript("lint", worktreePath, [], { quiet: true });
|
|
859
|
+
logger.success("Linting passed");
|
|
860
|
+
return {
|
|
861
|
+
step: "lint",
|
|
862
|
+
passed: true,
|
|
863
|
+
skipped: false,
|
|
864
|
+
duration: Date.now() - stepStartTime
|
|
865
|
+
};
|
|
866
|
+
} catch {
|
|
867
|
+
const fixed = await this.attemptClaudeFix(
|
|
868
|
+
"lint",
|
|
869
|
+
worktreePath,
|
|
870
|
+
packageManager
|
|
871
|
+
);
|
|
872
|
+
if (fixed) {
|
|
873
|
+
return {
|
|
874
|
+
step: "lint",
|
|
875
|
+
passed: true,
|
|
876
|
+
skipped: false,
|
|
877
|
+
duration: Date.now() - stepStartTime
|
|
878
|
+
};
|
|
879
|
+
}
|
|
880
|
+
const runCommand = packageManager === "npm" ? "npm run lint" : `${packageManager} lint`;
|
|
881
|
+
throw new Error(
|
|
882
|
+
`Error: Linting failed.
|
|
883
|
+
Fix linting errors before merging.
|
|
884
|
+
|
|
885
|
+
Run '${runCommand}' to see detailed errors.`
|
|
886
|
+
);
|
|
887
|
+
}
|
|
888
|
+
}
|
|
889
|
+
/**
|
|
890
|
+
* Run test validation
|
|
891
|
+
*/
|
|
892
|
+
async runTests(worktreePath, dryRun) {
|
|
893
|
+
const stepStartTime = Date.now();
|
|
894
|
+
try {
|
|
895
|
+
const pkgJson = await readPackageJson(worktreePath);
|
|
896
|
+
const hasTestScript = hasScript(pkgJson, "test");
|
|
897
|
+
if (!hasTestScript) {
|
|
898
|
+
logger.debug("Skipping tests - no test script found");
|
|
899
|
+
return {
|
|
900
|
+
step: "test",
|
|
901
|
+
passed: true,
|
|
902
|
+
skipped: true,
|
|
903
|
+
duration: Date.now() - stepStartTime
|
|
904
|
+
};
|
|
905
|
+
}
|
|
906
|
+
} catch (error) {
|
|
907
|
+
if (error instanceof Error && error.message.includes("package.json not found")) {
|
|
908
|
+
logger.debug("Skipping tests - no package.json found (non-Node.js project)");
|
|
909
|
+
return {
|
|
910
|
+
step: "test",
|
|
911
|
+
passed: true,
|
|
912
|
+
skipped: true,
|
|
913
|
+
duration: Date.now() - stepStartTime
|
|
914
|
+
};
|
|
915
|
+
}
|
|
916
|
+
throw error;
|
|
917
|
+
}
|
|
918
|
+
const packageManager = await detectPackageManager(worktreePath);
|
|
919
|
+
if (dryRun) {
|
|
920
|
+
const command = packageManager === "npm" ? "npm run test" : `${packageManager} test`;
|
|
921
|
+
logger.info(`[DRY RUN] Would run: ${command}`);
|
|
922
|
+
return {
|
|
923
|
+
step: "test",
|
|
924
|
+
passed: true,
|
|
925
|
+
skipped: false,
|
|
926
|
+
duration: Date.now() - stepStartTime
|
|
927
|
+
};
|
|
928
|
+
}
|
|
929
|
+
logger.info("Running tests...");
|
|
930
|
+
try {
|
|
931
|
+
await runScript("test", worktreePath, [], { quiet: true });
|
|
932
|
+
logger.success("Tests passed");
|
|
933
|
+
return {
|
|
934
|
+
step: "test",
|
|
935
|
+
passed: true,
|
|
936
|
+
skipped: false,
|
|
937
|
+
duration: Date.now() - stepStartTime
|
|
938
|
+
};
|
|
939
|
+
} catch {
|
|
940
|
+
const fixed = await this.attemptClaudeFix(
|
|
941
|
+
"test",
|
|
942
|
+
worktreePath,
|
|
943
|
+
packageManager
|
|
944
|
+
);
|
|
945
|
+
if (fixed) {
|
|
946
|
+
return {
|
|
947
|
+
step: "test",
|
|
948
|
+
passed: true,
|
|
949
|
+
skipped: false,
|
|
950
|
+
duration: Date.now() - stepStartTime
|
|
951
|
+
};
|
|
952
|
+
}
|
|
953
|
+
const runCommand = packageManager === "npm" ? "npm run test" : `${packageManager} test`;
|
|
954
|
+
throw new Error(
|
|
955
|
+
`Error: Tests failed.
|
|
956
|
+
Fix test failures before merging.
|
|
957
|
+
|
|
958
|
+
Run '${runCommand}' to see detailed errors.`
|
|
959
|
+
);
|
|
960
|
+
}
|
|
961
|
+
}
|
|
962
|
+
/**
|
|
963
|
+
* Attempt to fix validation errors using Claude
|
|
964
|
+
* Pattern based on MergeManager.attemptClaudeConflictResolution
|
|
965
|
+
*
|
|
966
|
+
* @param validationType - Type of validation that failed ('typecheck' | 'lint' | 'test')
|
|
967
|
+
* @param worktreePath - Path to the worktree
|
|
968
|
+
* @param packageManager - Detected package manager
|
|
969
|
+
* @returns true if Claude fixed the issue, false otherwise
|
|
970
|
+
*/
|
|
971
|
+
async attemptClaudeFix(validationType, worktreePath, packageManager) {
|
|
972
|
+
const isClaudeAvailable = await detectClaudeCli();
|
|
973
|
+
if (!isClaudeAvailable) {
|
|
974
|
+
logger.debug("Claude CLI not available, skipping auto-fix");
|
|
975
|
+
return false;
|
|
976
|
+
}
|
|
977
|
+
const validationCommand = this.getValidationCommand(validationType, packageManager);
|
|
978
|
+
const prompt = this.getClaudePrompt(validationType, validationCommand);
|
|
979
|
+
const validationTypeCapitalized = validationType.charAt(0).toUpperCase() + validationType.slice(1);
|
|
980
|
+
logger.info(`Launching Claude to help fix ${validationTypeCapitalized} errors...`);
|
|
981
|
+
try {
|
|
982
|
+
await launchClaude(prompt, {
|
|
983
|
+
addDir: worktreePath,
|
|
984
|
+
headless: false,
|
|
985
|
+
// Interactive mode
|
|
986
|
+
permissionMode: "acceptEdits",
|
|
987
|
+
// Auto-accept edits
|
|
988
|
+
model: "sonnet"
|
|
989
|
+
// Use Sonnet model
|
|
990
|
+
});
|
|
991
|
+
logger.info(`Re-running ${validationTypeCapitalized} after Claude's fixes...`);
|
|
992
|
+
try {
|
|
993
|
+
await runScript(validationType, worktreePath, [], { quiet: true });
|
|
994
|
+
logger.success(`${validationTypeCapitalized} passed after Claude auto-fix`);
|
|
995
|
+
return true;
|
|
996
|
+
} catch {
|
|
997
|
+
logger.warn(`${validationTypeCapitalized} still failing after Claude's help`);
|
|
998
|
+
return false;
|
|
999
|
+
}
|
|
1000
|
+
} catch (error) {
|
|
1001
|
+
logger.warn("Claude auto-fix failed", {
|
|
1002
|
+
error: error instanceof Error ? error.message : String(error)
|
|
1003
|
+
});
|
|
1004
|
+
return false;
|
|
1005
|
+
}
|
|
1006
|
+
}
|
|
1007
|
+
/**
|
|
1008
|
+
* Get validation command string for prompts
|
|
1009
|
+
*/
|
|
1010
|
+
getValidationCommand(validationType, packageManager) {
|
|
1011
|
+
if (packageManager === "npm") {
|
|
1012
|
+
return `npm run ${validationType}`;
|
|
1013
|
+
}
|
|
1014
|
+
return `${packageManager} ${validationType}`;
|
|
1015
|
+
}
|
|
1016
|
+
/**
|
|
1017
|
+
* Get Claude prompt for specific validation type
|
|
1018
|
+
* Matches bash script prompts exactly
|
|
1019
|
+
*/
|
|
1020
|
+
getClaudePrompt(validationType, validationCommand) {
|
|
1021
|
+
switch (validationType) {
|
|
1022
|
+
case "typecheck":
|
|
1023
|
+
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.`;
|
|
1024
|
+
case "lint":
|
|
1025
|
+
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.`;
|
|
1026
|
+
case "test":
|
|
1027
|
+
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.`;
|
|
1028
|
+
}
|
|
1029
|
+
}
|
|
1030
|
+
};
|
|
1031
|
+
|
|
1032
|
+
// src/lib/CommitManager.ts
|
|
1033
|
+
var CommitManager = class {
|
|
1034
|
+
/**
|
|
1035
|
+
* Detect uncommitted changes in a worktree
|
|
1036
|
+
* Parses git status --porcelain output into structured GitStatus
|
|
1037
|
+
*/
|
|
1038
|
+
async detectUncommittedChanges(worktreePath) {
|
|
1039
|
+
const porcelainOutput = await executeGitCommand(["status", "--porcelain"], {
|
|
1040
|
+
cwd: worktreePath
|
|
1041
|
+
});
|
|
1042
|
+
const { stagedFiles, unstagedFiles } = this.parseGitStatus(porcelainOutput);
|
|
1043
|
+
const currentBranch = await executeGitCommand(["branch", "--show-current"], {
|
|
1044
|
+
cwd: worktreePath
|
|
1045
|
+
});
|
|
1046
|
+
return {
|
|
1047
|
+
hasUncommittedChanges: stagedFiles.length > 0 || unstagedFiles.length > 0,
|
|
1048
|
+
unstagedFiles,
|
|
1049
|
+
stagedFiles,
|
|
1050
|
+
currentBranch: currentBranch.trim(),
|
|
1051
|
+
// Defer these to future enhancement
|
|
1052
|
+
isAheadOfRemote: false,
|
|
1053
|
+
isBehindRemote: false
|
|
1054
|
+
};
|
|
1055
|
+
}
|
|
1056
|
+
/**
|
|
1057
|
+
* Stage all changes and commit with Claude-generated or simple message
|
|
1058
|
+
* Tries Claude first, falls back to simple message if Claude unavailable or fails
|
|
1059
|
+
*/
|
|
1060
|
+
async commitChanges(worktreePath, options) {
|
|
1061
|
+
if (options.dryRun) {
|
|
1062
|
+
logger.info("[DRY RUN] Would run: git add -A");
|
|
1063
|
+
logger.info("[DRY RUN] Would generate commit message with Claude (if available)");
|
|
1064
|
+
const fallbackMessage = this.generateFallbackMessage(options);
|
|
1065
|
+
const verifyFlag = options.skipVerify ? " --no-verify" : "";
|
|
1066
|
+
logger.info(`[DRY RUN] Would commit with message${verifyFlag}: ${fallbackMessage}`);
|
|
1067
|
+
return;
|
|
1068
|
+
}
|
|
1069
|
+
await executeGitCommand(["add", "-A"], { cwd: worktreePath });
|
|
1070
|
+
let message = null;
|
|
1071
|
+
if (!options.message) {
|
|
1072
|
+
try {
|
|
1073
|
+
message = await this.generateClaudeCommitMessage(worktreePath, options.issueNumber);
|
|
1074
|
+
} catch (error) {
|
|
1075
|
+
logger.debug("Claude commit message generation failed, using fallback", { error });
|
|
1076
|
+
}
|
|
1077
|
+
}
|
|
1078
|
+
message ??= this.generateFallbackMessage(options);
|
|
1079
|
+
if (options.skipVerify) {
|
|
1080
|
+
logger.warn("\u26A0\uFE0F Skipping pre-commit hooks (--no-verify configured in settings)");
|
|
1081
|
+
}
|
|
1082
|
+
try {
|
|
1083
|
+
if (options.noReview || options.message) {
|
|
1084
|
+
const commitArgs = ["commit", "-m", message];
|
|
1085
|
+
if (options.skipVerify) {
|
|
1086
|
+
commitArgs.push("--no-verify");
|
|
1087
|
+
}
|
|
1088
|
+
await executeGitCommand(commitArgs, { cwd: worktreePath });
|
|
1089
|
+
} else {
|
|
1090
|
+
logger.info("Opening git editor for commit message review...");
|
|
1091
|
+
const commitArgs = ["commit", "-e", "-m", message];
|
|
1092
|
+
if (options.skipVerify) {
|
|
1093
|
+
commitArgs.push("--no-verify");
|
|
1094
|
+
}
|
|
1095
|
+
await executeGitCommand(commitArgs, {
|
|
1096
|
+
cwd: worktreePath,
|
|
1097
|
+
stdio: "inherit",
|
|
1098
|
+
timeout: 3e5
|
|
1099
|
+
// 5 minutes for interactive editing
|
|
1100
|
+
});
|
|
1101
|
+
}
|
|
1102
|
+
} catch (error) {
|
|
1103
|
+
if (error instanceof Error && error.message.includes("nothing to commit")) {
|
|
1104
|
+
logger.info("No changes to commit");
|
|
1105
|
+
return;
|
|
1106
|
+
}
|
|
1107
|
+
throw error;
|
|
1108
|
+
}
|
|
1109
|
+
}
|
|
1110
|
+
/**
|
|
1111
|
+
* Generate simple fallback commit message when Claude unavailable
|
|
1112
|
+
* Used as fallback for Claude-powered commit messages
|
|
1113
|
+
*/
|
|
1114
|
+
generateFallbackMessage(options) {
|
|
1115
|
+
if (options.message) {
|
|
1116
|
+
return options.message;
|
|
1117
|
+
}
|
|
1118
|
+
if (options.issueNumber) {
|
|
1119
|
+
return `WIP: Auto-commit for issue #${options.issueNumber}
|
|
1120
|
+
|
|
1121
|
+
Fixes #${options.issueNumber}`;
|
|
1122
|
+
} else {
|
|
1123
|
+
return "WIP: Auto-commit uncommitted changes";
|
|
1124
|
+
}
|
|
1125
|
+
}
|
|
1126
|
+
/**
|
|
1127
|
+
* Parse git status --porcelain output
|
|
1128
|
+
* Format: "XY filename" where X=index, Y=worktree
|
|
1129
|
+
* Examples:
|
|
1130
|
+
* "M file.ts" - staged modification
|
|
1131
|
+
* " M file.ts" - unstaged modification
|
|
1132
|
+
* "MM file.ts" - both staged and unstaged
|
|
1133
|
+
* "?? file.ts" - untracked
|
|
1134
|
+
*/
|
|
1135
|
+
parseGitStatus(porcelainOutput) {
|
|
1136
|
+
const stagedFiles = [];
|
|
1137
|
+
const unstagedFiles = [];
|
|
1138
|
+
if (!porcelainOutput.trim()) {
|
|
1139
|
+
return { stagedFiles, unstagedFiles };
|
|
1140
|
+
}
|
|
1141
|
+
const lines = porcelainOutput.split("\n").filter((line) => line.trim());
|
|
1142
|
+
for (const line of lines) {
|
|
1143
|
+
if (line.length < 3) continue;
|
|
1144
|
+
const indexStatus = line[0];
|
|
1145
|
+
const worktreeStatus = line[1];
|
|
1146
|
+
const filename = line.substring(3);
|
|
1147
|
+
if (indexStatus !== " " && indexStatus !== "?") {
|
|
1148
|
+
stagedFiles.push(filename);
|
|
1149
|
+
}
|
|
1150
|
+
if (worktreeStatus !== " " || line.startsWith("??")) {
|
|
1151
|
+
unstagedFiles.push(filename);
|
|
1152
|
+
}
|
|
1153
|
+
}
|
|
1154
|
+
return { stagedFiles, unstagedFiles };
|
|
1155
|
+
}
|
|
1156
|
+
/**
|
|
1157
|
+
* Generate commit message using Claude AI
|
|
1158
|
+
* Claude examines the git repository directly via --add-dir option
|
|
1159
|
+
* Returns null if Claude unavailable or fails validation
|
|
1160
|
+
*/
|
|
1161
|
+
async generateClaudeCommitMessage(worktreePath, issueNumber) {
|
|
1162
|
+
const startTime = Date.now();
|
|
1163
|
+
logger.info("Starting Claude commit message generation...", {
|
|
1164
|
+
worktreePath: worktreePath.split("/").pop(),
|
|
1165
|
+
// Just show the folder name for privacy
|
|
1166
|
+
issueNumber
|
|
1167
|
+
});
|
|
1168
|
+
logger.debug("Checking Claude CLI availability...");
|
|
1169
|
+
const isClaudeAvailable = await detectClaudeCli();
|
|
1170
|
+
if (!isClaudeAvailable) {
|
|
1171
|
+
logger.info("Claude CLI not available, skipping Claude commit message generation");
|
|
1172
|
+
return null;
|
|
1173
|
+
}
|
|
1174
|
+
logger.debug("Claude CLI is available");
|
|
1175
|
+
logger.debug("Building commit message prompt...");
|
|
1176
|
+
const prompt = this.buildCommitMessagePrompt(issueNumber);
|
|
1177
|
+
logger.debug("Prompt built", { promptLength: prompt.length });
|
|
1178
|
+
logger.debug("Claude prompt content:", {
|
|
1179
|
+
prompt,
|
|
1180
|
+
truncatedPreview: prompt.substring(0, 500) + (prompt.length > 500 ? "...[truncated]" : "")
|
|
1181
|
+
});
|
|
1182
|
+
try {
|
|
1183
|
+
logger.info("Calling Claude CLI for commit message generation...");
|
|
1184
|
+
const claudeStartTime = Date.now();
|
|
1185
|
+
const claudeOptions = {
|
|
1186
|
+
headless: true,
|
|
1187
|
+
addDir: worktreePath,
|
|
1188
|
+
model: "claude-haiku-4-5-20251001",
|
|
1189
|
+
// Fast, cost-effective model
|
|
1190
|
+
timeout: 12e4
|
|
1191
|
+
// 120 second timeout
|
|
1192
|
+
};
|
|
1193
|
+
logger.debug("Claude CLI call parameters:", {
|
|
1194
|
+
options: claudeOptions,
|
|
1195
|
+
worktreePathForAnalysis: worktreePath,
|
|
1196
|
+
addDirContents: "Will include entire worktree directory for analysis"
|
|
1197
|
+
});
|
|
1198
|
+
const result = await launchClaude(prompt, claudeOptions);
|
|
1199
|
+
const claudeDuration = Date.now() - claudeStartTime;
|
|
1200
|
+
logger.debug("Claude API call completed", { duration: `${claudeDuration}ms` });
|
|
1201
|
+
if (typeof result !== "string") {
|
|
1202
|
+
logger.warn("Claude returned non-string result", { resultType: typeof result });
|
|
1203
|
+
return null;
|
|
1204
|
+
}
|
|
1205
|
+
logger.debug("Raw Claude output received", {
|
|
1206
|
+
outputLength: result.length,
|
|
1207
|
+
preview: result.substring(0, 200) + (result.length > 200 ? "..." : "")
|
|
1208
|
+
});
|
|
1209
|
+
logger.debug("Sanitizing Claude output...");
|
|
1210
|
+
const sanitized = this.sanitizeClaudeOutput(result);
|
|
1211
|
+
logger.debug("Output sanitized", {
|
|
1212
|
+
originalLength: result.length,
|
|
1213
|
+
sanitizedLength: sanitized.length,
|
|
1214
|
+
sanitized: sanitized.substring(0, 200) + (sanitized.length > 200 ? "..." : "")
|
|
1215
|
+
});
|
|
1216
|
+
if (!sanitized) {
|
|
1217
|
+
logger.warn("Claude returned empty message after sanitization");
|
|
1218
|
+
return null;
|
|
1219
|
+
}
|
|
1220
|
+
let finalMessage = sanitized;
|
|
1221
|
+
if (issueNumber) {
|
|
1222
|
+
if (!finalMessage.includes(`Fixes #${issueNumber}`)) {
|
|
1223
|
+
finalMessage = `${finalMessage}
|
|
1224
|
+
|
|
1225
|
+
Fixes #${issueNumber}`;
|
|
1226
|
+
logger.debug(`Added "Fixes #${issueNumber}" trailer to commit message`);
|
|
1227
|
+
} else {
|
|
1228
|
+
logger.debug(`"Fixes #${issueNumber}" already present in commit message`);
|
|
1229
|
+
}
|
|
1230
|
+
}
|
|
1231
|
+
const totalDuration = Date.now() - startTime;
|
|
1232
|
+
logger.info("Claude commit message generated successfully", {
|
|
1233
|
+
message: finalMessage,
|
|
1234
|
+
totalDuration: `${totalDuration}ms`,
|
|
1235
|
+
claudeApiDuration: `${claudeDuration}ms`
|
|
1236
|
+
});
|
|
1237
|
+
return finalMessage;
|
|
1238
|
+
} catch (error) {
|
|
1239
|
+
const totalDuration = Date.now() - startTime;
|
|
1240
|
+
const errorMessage = error instanceof Error ? error.message : "Unknown error";
|
|
1241
|
+
if (errorMessage.includes("timed out") || errorMessage.includes("timeout")) {
|
|
1242
|
+
logger.warn("Claude commit message generation timed out after 45 seconds", {
|
|
1243
|
+
totalDuration: `${totalDuration}ms`,
|
|
1244
|
+
worktreePath: worktreePath.split("/").pop()
|
|
1245
|
+
});
|
|
1246
|
+
} else {
|
|
1247
|
+
logger.warn("Failed to generate commit message with Claude", {
|
|
1248
|
+
error: errorMessage,
|
|
1249
|
+
totalDuration: `${totalDuration}ms`,
|
|
1250
|
+
worktreePath: worktreePath.split("/").pop()
|
|
1251
|
+
});
|
|
1252
|
+
}
|
|
1253
|
+
return null;
|
|
1254
|
+
}
|
|
1255
|
+
}
|
|
1256
|
+
/**
|
|
1257
|
+
* Build structured XML prompt for commit message generation
|
|
1258
|
+
* Uses XML format for clear task definition and output expectations
|
|
1259
|
+
*/
|
|
1260
|
+
buildCommitMessagePrompt(issueNumber) {
|
|
1261
|
+
const issueContext = issueNumber ? `
|
|
1262
|
+
<IssueContext>
|
|
1263
|
+
This commit is associated with GitHub issue #${issueNumber}.
|
|
1264
|
+
If the changes appear to resolve the issue, include "Fixes #${issueNumber}" at the end of the first line of commit message.
|
|
1265
|
+
</IssueContext>` : "";
|
|
1266
|
+
return `<Task>
|
|
1267
|
+
You are a software engineer writing a commit message for this repository.
|
|
1268
|
+
Examine the staged changes in the git repository and generate a concise, meaningful commit message.
|
|
1269
|
+
</Task>
|
|
1270
|
+
|
|
1271
|
+
<Requirements>
|
|
1272
|
+
<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.
|
|
1273
|
+
|
|
1274
|
+
Add 2 newlines, then add a bullet-point form description of the changes made, each change on a new line.</Format>
|
|
1275
|
+
<Mood>Use imperative mood (e.g., "Add feature" not "Added feature")</Mood>
|
|
1276
|
+
<Focus>Be specific about what was changed and why</Focus>
|
|
1277
|
+
<Conciseness>Keep message under 72 characters for subject line when possible</Conciseness>
|
|
1278
|
+
<NoMeta>CRITICAL: Do NOT include ANY explanatory text, analysis, or meta-commentary. Output ONLY the raw commit message.</NoMeta>
|
|
1279
|
+
<Examples>
|
|
1280
|
+
Good: "Add user authentication with JWT tokens. Fixes #42
|
|
1281
|
+
|
|
1282
|
+
- Implement login and registration endpoints
|
|
1283
|
+
- Secure routes with JWT middleware
|
|
1284
|
+
- Update user model to store hashed passwords"
|
|
1285
|
+
Good: "Fix navigation bug in sidebar menu."
|
|
1286
|
+
Bad: "Based on the changes, I'll create: Add user authentication"
|
|
1287
|
+
Bad: "Looking at the files, this commit should be: Fix navigation bug"
|
|
1288
|
+
</Examples>
|
|
1289
|
+
${issueContext}
|
|
1290
|
+
</Requirements>
|
|
1291
|
+
|
|
1292
|
+
<Output>
|
|
1293
|
+
IMPORTANT: Your entire response will be used directly as the git commit message.
|
|
1294
|
+
Do not include any explanatory text before or after the commit message.
|
|
1295
|
+
Start your response immediately with the commit message text.
|
|
1296
|
+
</Output>`;
|
|
1297
|
+
}
|
|
1298
|
+
/**
|
|
1299
|
+
* Sanitize Claude output to remove meta-commentary and clean formatting
|
|
1300
|
+
* Handles cases where Claude includes explanatory text despite instructions
|
|
1301
|
+
*/
|
|
1302
|
+
sanitizeClaudeOutput(rawOutput) {
|
|
1303
|
+
let cleaned = rawOutput.trim();
|
|
1304
|
+
const metaPatterns = [
|
|
1305
|
+
/^.*?based on.*?changes.*?:/i,
|
|
1306
|
+
/^.*?looking at.*?files.*?:/i,
|
|
1307
|
+
/^.*?examining.*?:/i,
|
|
1308
|
+
/^.*?analyzing.*?:/i,
|
|
1309
|
+
/^.*?i'll.*?generate.*?:/i,
|
|
1310
|
+
/^.*?let me.*?:/i,
|
|
1311
|
+
/^.*?the commit message.*?should be.*?:/i,
|
|
1312
|
+
/^.*?here.*?is.*?commit.*?message.*?:/i
|
|
1313
|
+
];
|
|
1314
|
+
for (const pattern of metaPatterns) {
|
|
1315
|
+
cleaned = cleaned.replace(pattern, "").trim();
|
|
1316
|
+
}
|
|
1317
|
+
if (cleaned.includes(":")) {
|
|
1318
|
+
const colonIndex = cleaned.indexOf(":");
|
|
1319
|
+
const beforeColon = cleaned.substring(0, colonIndex).trim().toLowerCase();
|
|
1320
|
+
const metaIndicators = [
|
|
1321
|
+
"here is the commit message",
|
|
1322
|
+
"commit message",
|
|
1323
|
+
"here is",
|
|
1324
|
+
"the message should be",
|
|
1325
|
+
"i suggest",
|
|
1326
|
+
"my suggestion"
|
|
1327
|
+
];
|
|
1328
|
+
const isMetaCommentary = metaIndicators.some((indicator) => beforeColon.includes(indicator));
|
|
1329
|
+
if (isMetaCommentary) {
|
|
1330
|
+
const afterColon = cleaned.substring(colonIndex + 1).trim();
|
|
1331
|
+
if (afterColon && afterColon.length > 10) {
|
|
1332
|
+
cleaned = afterColon;
|
|
1333
|
+
}
|
|
1334
|
+
}
|
|
1335
|
+
}
|
|
1336
|
+
if (cleaned.startsWith('"') && cleaned.endsWith('"') || cleaned.startsWith("'") && cleaned.endsWith("'")) {
|
|
1337
|
+
cleaned = cleaned.slice(1, -1).trim();
|
|
1338
|
+
}
|
|
1339
|
+
return cleaned;
|
|
1340
|
+
}
|
|
1341
|
+
};
|
|
1342
|
+
|
|
1343
|
+
// src/lib/BuildRunner.ts
|
|
1344
|
+
var BuildRunner = class {
|
|
1345
|
+
constructor(capabilityDetector) {
|
|
1346
|
+
this.capabilityDetector = capabilityDetector ?? new ProjectCapabilityDetector();
|
|
1347
|
+
}
|
|
1348
|
+
/**
|
|
1349
|
+
* Run build verification in the specified directory
|
|
1350
|
+
* @param buildPath - Path where build should run (typically main worktree path)
|
|
1351
|
+
* @param options - Build options
|
|
1352
|
+
*/
|
|
1353
|
+
async runBuild(buildPath, options = {}) {
|
|
1354
|
+
const startTime = Date.now();
|
|
1355
|
+
try {
|
|
1356
|
+
const pkgJson = await readPackageJson(buildPath);
|
|
1357
|
+
const hasBuildScript = hasScript(pkgJson, "build");
|
|
1358
|
+
if (!hasBuildScript) {
|
|
1359
|
+
logger.debug("Skipping build - no build script found");
|
|
1360
|
+
return {
|
|
1361
|
+
success: true,
|
|
1362
|
+
skipped: true,
|
|
1363
|
+
reason: "No build script found in package.json",
|
|
1364
|
+
duration: Date.now() - startTime
|
|
1365
|
+
};
|
|
1366
|
+
}
|
|
1367
|
+
} catch (error) {
|
|
1368
|
+
if (error instanceof Error && error.message.includes("package.json not found")) {
|
|
1369
|
+
logger.debug("Skipping build - no package.json found (non-Node.js project)");
|
|
1370
|
+
return {
|
|
1371
|
+
success: true,
|
|
1372
|
+
skipped: true,
|
|
1373
|
+
reason: "No package.json found in project",
|
|
1374
|
+
duration: Date.now() - startTime
|
|
1375
|
+
};
|
|
1376
|
+
}
|
|
1377
|
+
throw error;
|
|
1378
|
+
}
|
|
1379
|
+
const capabilities = await this.capabilityDetector.detectCapabilities(buildPath);
|
|
1380
|
+
const isCLIProject = capabilities.capabilities.includes("cli");
|
|
1381
|
+
if (!isCLIProject) {
|
|
1382
|
+
logger.debug("Skipping build - not a CLI project (no bin field)");
|
|
1383
|
+
return {
|
|
1384
|
+
success: true,
|
|
1385
|
+
skipped: true,
|
|
1386
|
+
reason: "Project is not a CLI project (no bin field in package.json)",
|
|
1387
|
+
duration: Date.now() - startTime
|
|
1388
|
+
};
|
|
1389
|
+
}
|
|
1390
|
+
const packageManager = await detectPackageManager(buildPath);
|
|
1391
|
+
if (options.dryRun) {
|
|
1392
|
+
const command = packageManager === "npm" ? "npm run build" : `${packageManager} build`;
|
|
1393
|
+
logger.info(`[DRY RUN] Would run: ${command}`);
|
|
1394
|
+
return {
|
|
1395
|
+
success: true,
|
|
1396
|
+
skipped: false,
|
|
1397
|
+
duration: Date.now() - startTime
|
|
1398
|
+
};
|
|
1399
|
+
}
|
|
1400
|
+
logger.info("Running build...");
|
|
1401
|
+
try {
|
|
1402
|
+
await runScript("build", buildPath, [], { quiet: true });
|
|
1403
|
+
logger.success("Build completed successfully");
|
|
1404
|
+
return {
|
|
1405
|
+
success: true,
|
|
1406
|
+
skipped: false,
|
|
1407
|
+
duration: Date.now() - startTime
|
|
1408
|
+
};
|
|
1409
|
+
} catch {
|
|
1410
|
+
const runCommand = packageManager === "npm" ? "npm run build" : `${packageManager} build`;
|
|
1411
|
+
throw new Error(
|
|
1412
|
+
`Error: Build failed.
|
|
1413
|
+
Fix build errors before proceeding.
|
|
1414
|
+
|
|
1415
|
+
Run '${runCommand}' to see detailed errors.`
|
|
1416
|
+
);
|
|
1417
|
+
}
|
|
1418
|
+
}
|
|
1419
|
+
};
|
|
1420
|
+
|
|
1421
|
+
// src/lib/PRManager.ts
|
|
1422
|
+
var PRManager = class {
|
|
1423
|
+
constructor(settings) {
|
|
1424
|
+
this.settings = settings;
|
|
1425
|
+
}
|
|
1426
|
+
/**
|
|
1427
|
+
* Check if a PR already exists for the given branch
|
|
1428
|
+
* @param branchName - Branch to check
|
|
1429
|
+
* @param cwd - Working directory
|
|
1430
|
+
* @returns Existing PR info or null if none found
|
|
1431
|
+
*/
|
|
1432
|
+
async checkForExistingPR(branchName, cwd) {
|
|
1433
|
+
try {
|
|
1434
|
+
const prList = await executeGhCommand(
|
|
1435
|
+
["pr", "list", "--head", branchName, "--state", "open", "--json", "number,url"],
|
|
1436
|
+
cwd ? { cwd } : void 0
|
|
1437
|
+
);
|
|
1438
|
+
if (prList.length > 0) {
|
|
1439
|
+
return prList[0] ?? null;
|
|
1440
|
+
}
|
|
1441
|
+
return null;
|
|
1442
|
+
} catch (error) {
|
|
1443
|
+
logger.debug("Error checking for existing PR", { error });
|
|
1444
|
+
return null;
|
|
1445
|
+
}
|
|
1446
|
+
}
|
|
1447
|
+
/**
|
|
1448
|
+
* Generate PR body using Claude if available, otherwise use simple template
|
|
1449
|
+
* @param issueNumber - Issue number to include in body
|
|
1450
|
+
* @param worktreePath - Path to worktree for context
|
|
1451
|
+
* @returns PR body markdown
|
|
1452
|
+
*/
|
|
1453
|
+
async generatePRBody(issueNumber, worktreePath) {
|
|
1454
|
+
const hasClaudeCli = await detectClaudeCli();
|
|
1455
|
+
if (hasClaudeCli) {
|
|
1456
|
+
try {
|
|
1457
|
+
const prompt = this.buildPRBodyPrompt(issueNumber);
|
|
1458
|
+
const body2 = await launchClaude(prompt, {
|
|
1459
|
+
headless: true,
|
|
1460
|
+
addDir: worktreePath,
|
|
1461
|
+
timeout: 3e4
|
|
1462
|
+
});
|
|
1463
|
+
if (body2 && typeof body2 === "string" && body2.trim()) {
|
|
1464
|
+
const sanitized = this.sanitizeClaudeOutput(body2);
|
|
1465
|
+
if (sanitized) {
|
|
1466
|
+
return sanitized;
|
|
1467
|
+
}
|
|
1468
|
+
}
|
|
1469
|
+
} catch (error) {
|
|
1470
|
+
logger.debug("Claude PR body generation failed, using template", { error });
|
|
1471
|
+
}
|
|
1472
|
+
}
|
|
1473
|
+
let body = "This PR contains changes from the iloom workflow.\n\n";
|
|
1474
|
+
if (issueNumber) {
|
|
1475
|
+
body += `Fixes #${issueNumber}`;
|
|
1476
|
+
}
|
|
1477
|
+
return body;
|
|
1478
|
+
}
|
|
1479
|
+
/**
|
|
1480
|
+
* Build structured XML prompt for PR body generation
|
|
1481
|
+
* Uses XML format for clear task definition and output expectations
|
|
1482
|
+
*/
|
|
1483
|
+
buildPRBodyPrompt(issueNumber) {
|
|
1484
|
+
const issueContext = issueNumber ? `
|
|
1485
|
+
<IssueContext>
|
|
1486
|
+
This PR is associated with GitHub issue #${issueNumber}.
|
|
1487
|
+
Include "Fixes #${issueNumber}" at the end of the body on its own line.
|
|
1488
|
+
</IssueContext>` : "";
|
|
1489
|
+
return `<Task>
|
|
1490
|
+
You are a software engineer writing a pull request body for this repository.
|
|
1491
|
+
Examine the changes in the git repository and generate a concise, professional PR description.
|
|
1492
|
+
</Task>
|
|
1493
|
+
|
|
1494
|
+
<Requirements>
|
|
1495
|
+
<Format>Write 2-3 sentences summarizing what was changed and why.${issueNumber ? `
|
|
1496
|
+
|
|
1497
|
+
End with "Fixes #${issueNumber}" on its own line.` : ""}</Format>
|
|
1498
|
+
<Tone>Professional and concise</Tone>
|
|
1499
|
+
<Focus>Summarize the changes and their purpose</Focus>
|
|
1500
|
+
<NoMeta>CRITICAL: Do NOT include ANY explanatory text, analysis, or meta-commentary. Output ONLY the raw PR body text.</NoMeta>
|
|
1501
|
+
<Examples>
|
|
1502
|
+
Good: "Add user authentication with JWT tokens to secure the API endpoints. This includes login and registration endpoints with proper password hashing.
|
|
1503
|
+
|
|
1504
|
+
Fixes #42"
|
|
1505
|
+
Good: "Fix navigation bug in sidebar menu that caused incorrect highlighting on nested routes."
|
|
1506
|
+
Bad: "Here's the PR body:
|
|
1507
|
+
|
|
1508
|
+
---
|
|
1509
|
+
|
|
1510
|
+
Add user authentication..."
|
|
1511
|
+
Bad: "Based on the changes, I'll write: Fix navigation bug..."
|
|
1512
|
+
</Examples>
|
|
1513
|
+
${issueContext}
|
|
1514
|
+
</Requirements>
|
|
1515
|
+
|
|
1516
|
+
<Output>
|
|
1517
|
+
IMPORTANT: Your entire response will be used directly as the GitHub pull request body.
|
|
1518
|
+
Do not include any explanatory text, headers, or separators before or after the body.
|
|
1519
|
+
Start your response immediately with the PR body text.
|
|
1520
|
+
</Output>`;
|
|
1521
|
+
}
|
|
1522
|
+
/**
|
|
1523
|
+
* Sanitize Claude output to remove meta-commentary and clean formatting
|
|
1524
|
+
* Handles cases where Claude includes explanatory text despite instructions
|
|
1525
|
+
*/
|
|
1526
|
+
sanitizeClaudeOutput(rawOutput) {
|
|
1527
|
+
let cleaned = rawOutput.trim();
|
|
1528
|
+
const metaPatterns = [
|
|
1529
|
+
/^.*?based on.*?changes.*?:/i,
|
|
1530
|
+
/^.*?looking at.*?files.*?:/i,
|
|
1531
|
+
/^.*?examining.*?:/i,
|
|
1532
|
+
/^.*?analyzing.*?:/i,
|
|
1533
|
+
/^.*?i'll.*?generate.*?:/i,
|
|
1534
|
+
/^.*?let me.*?:/i,
|
|
1535
|
+
/^.*?here.*?is.*?(?:the\s+)?(?:pr|pull request).*?body.*?:/i,
|
|
1536
|
+
/^.*?here's.*?(?:the\s+)?(?:pr|pull request).*?body.*?:/i
|
|
1537
|
+
];
|
|
1538
|
+
for (const pattern of metaPatterns) {
|
|
1539
|
+
cleaned = cleaned.replace(pattern, "").trim();
|
|
1540
|
+
}
|
|
1541
|
+
cleaned = cleaned.replace(/^[-=]{3,}\s*/m, "").trim();
|
|
1542
|
+
if (cleaned.includes(":")) {
|
|
1543
|
+
const colonIndex = cleaned.indexOf(":");
|
|
1544
|
+
const beforeColon = cleaned.substring(0, colonIndex).trim().toLowerCase();
|
|
1545
|
+
const metaIndicators = [
|
|
1546
|
+
"here is the pr body",
|
|
1547
|
+
"here is the pull request body",
|
|
1548
|
+
"pr body",
|
|
1549
|
+
"pull request body",
|
|
1550
|
+
"here is",
|
|
1551
|
+
"here's",
|
|
1552
|
+
"the body should be",
|
|
1553
|
+
"i suggest",
|
|
1554
|
+
"my suggestion"
|
|
1555
|
+
];
|
|
1556
|
+
const isMetaCommentary = metaIndicators.some((indicator) => beforeColon.includes(indicator));
|
|
1557
|
+
if (isMetaCommentary) {
|
|
1558
|
+
const afterColon = cleaned.substring(colonIndex + 1).trim();
|
|
1559
|
+
const afterSeparator = afterColon.replace(/^[-=]{3,}\s*/m, "").trim();
|
|
1560
|
+
if (afterSeparator && afterSeparator.length > 10) {
|
|
1561
|
+
cleaned = afterSeparator;
|
|
1562
|
+
}
|
|
1563
|
+
}
|
|
1564
|
+
}
|
|
1565
|
+
if (cleaned.startsWith('"') && cleaned.endsWith('"') || cleaned.startsWith("'") && cleaned.endsWith("'")) {
|
|
1566
|
+
cleaned = cleaned.slice(1, -1).trim();
|
|
1567
|
+
}
|
|
1568
|
+
return cleaned;
|
|
1569
|
+
}
|
|
1570
|
+
/**
|
|
1571
|
+
* Create a GitHub PR for the branch
|
|
1572
|
+
* @param branchName - Branch to create PR from (used as --head)
|
|
1573
|
+
* @param title - PR title
|
|
1574
|
+
* @param body - PR body
|
|
1575
|
+
* @param baseBranch - Base branch to target (usually main/master)
|
|
1576
|
+
* @param cwd - Working directory
|
|
1577
|
+
* @returns PR URL
|
|
1578
|
+
*/
|
|
1579
|
+
async createPR(branchName, title, body, baseBranch, cwd) {
|
|
1580
|
+
try {
|
|
1581
|
+
const targetRemote = await getEffectivePRTargetRemote(this.settings, cwd);
|
|
1582
|
+
let headValue = branchName;
|
|
1583
|
+
if (targetRemote !== "origin") {
|
|
1584
|
+
const remotes = await parseGitRemotes(cwd);
|
|
1585
|
+
const originRemote = remotes.find((r) => r.name === "origin");
|
|
1586
|
+
if (originRemote) {
|
|
1587
|
+
headValue = `${originRemote.owner}:${branchName}`;
|
|
1588
|
+
logger.debug(`Fork workflow detected, using head: ${headValue}`);
|
|
1589
|
+
}
|
|
1590
|
+
}
|
|
1591
|
+
const args = ["pr", "create", "--head", headValue, "--title", title, "--body", body, "--base", baseBranch];
|
|
1592
|
+
if (targetRemote !== "origin") {
|
|
1593
|
+
const repo = await getConfiguredRepoFromSettings(this.settings, cwd);
|
|
1594
|
+
args.push("--repo", repo);
|
|
1595
|
+
}
|
|
1596
|
+
const result = await executeGhCommand(args, cwd ? { cwd } : void 0);
|
|
1597
|
+
const url = typeof result === "string" ? result.trim() : String(result).trim();
|
|
1598
|
+
if (!url.includes("github.com") || !url.includes("/pull/")) {
|
|
1599
|
+
throw new Error(`Unexpected response from gh pr create: ${url}`);
|
|
1600
|
+
}
|
|
1601
|
+
return url;
|
|
1602
|
+
} catch (error) {
|
|
1603
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
1604
|
+
if (errorMessage.includes("Head sha can't be blank") || errorMessage.includes("No commits between")) {
|
|
1605
|
+
throw new Error(
|
|
1606
|
+
`Failed to create pull request: ${errorMessage}
|
|
1607
|
+
|
|
1608
|
+
This error typically occurs when:
|
|
1609
|
+
- The branch was not fully pushed to the remote
|
|
1610
|
+
- There's a race condition between push and PR creation
|
|
1611
|
+
- The branch has no commits ahead of the base branch
|
|
1612
|
+
|
|
1613
|
+
Try running: git push -u origin ${branchName}
|
|
1614
|
+
Then retry: il finish`
|
|
1615
|
+
);
|
|
1616
|
+
}
|
|
1617
|
+
throw new Error(`Failed to create pull request: ${errorMessage}`);
|
|
1618
|
+
}
|
|
1619
|
+
}
|
|
1620
|
+
/**
|
|
1621
|
+
* Open PR URL in browser
|
|
1622
|
+
* @param url - PR URL to open
|
|
1623
|
+
*/
|
|
1624
|
+
async openPRInBrowser(url) {
|
|
1625
|
+
try {
|
|
1626
|
+
await openBrowser(url);
|
|
1627
|
+
logger.debug("Opened PR in browser", { url });
|
|
1628
|
+
} catch (error) {
|
|
1629
|
+
logger.warn("Failed to open PR in browser", { error });
|
|
1630
|
+
}
|
|
1631
|
+
}
|
|
1632
|
+
/**
|
|
1633
|
+
* Complete PR workflow: check for existing, create if needed, optionally open in browser
|
|
1634
|
+
* @param branchName - Branch to create PR from
|
|
1635
|
+
* @param title - PR title
|
|
1636
|
+
* @param issueNumber - Optional issue number for body generation
|
|
1637
|
+
* @param baseBranch - Base branch to target
|
|
1638
|
+
* @param worktreePath - Path to worktree
|
|
1639
|
+
* @param openInBrowser - Whether to open PR in browser
|
|
1640
|
+
* @returns PR creation result
|
|
1641
|
+
*/
|
|
1642
|
+
async createOrOpenPR(branchName, title, issueNumber, baseBranch, worktreePath, openInBrowser) {
|
|
1643
|
+
const existingPR = await this.checkForExistingPR(branchName, worktreePath);
|
|
1644
|
+
if (existingPR) {
|
|
1645
|
+
logger.info(`Pull request already exists: ${existingPR.url}`);
|
|
1646
|
+
if (openInBrowser) {
|
|
1647
|
+
await this.openPRInBrowser(existingPR.url);
|
|
1648
|
+
}
|
|
1649
|
+
return {
|
|
1650
|
+
url: existingPR.url,
|
|
1651
|
+
number: existingPR.number,
|
|
1652
|
+
wasExisting: true
|
|
1653
|
+
};
|
|
1654
|
+
}
|
|
1655
|
+
const body = await this.generatePRBody(issueNumber, worktreePath);
|
|
1656
|
+
logger.info("Creating pull request...");
|
|
1657
|
+
const url = await this.createPR(branchName, title, body, baseBranch, worktreePath);
|
|
1658
|
+
const prNumber = this.extractPRNumberFromUrl(url);
|
|
1659
|
+
if (openInBrowser) {
|
|
1660
|
+
await this.openPRInBrowser(url);
|
|
1661
|
+
}
|
|
1662
|
+
return {
|
|
1663
|
+
url,
|
|
1664
|
+
number: prNumber,
|
|
1665
|
+
wasExisting: false
|
|
1666
|
+
};
|
|
1667
|
+
}
|
|
1668
|
+
/**
|
|
1669
|
+
* Extract PR number from GitHub PR URL
|
|
1670
|
+
* @param url - PR URL (e.g., https://github.com/owner/repo/pull/123)
|
|
1671
|
+
* @returns PR number
|
|
1672
|
+
*/
|
|
1673
|
+
extractPRNumberFromUrl(url) {
|
|
1674
|
+
const match = url.match(/\/pull\/(\d+)/);
|
|
1675
|
+
if (match == null ? void 0 : match[1]) {
|
|
1676
|
+
return parseInt(match[1], 10);
|
|
1677
|
+
}
|
|
1678
|
+
throw new Error(`Could not extract PR number from URL: ${url}`);
|
|
1679
|
+
}
|
|
1680
|
+
};
|
|
1681
|
+
|
|
1682
|
+
// src/commands/finish.ts
|
|
1683
|
+
import path3 from "path";
|
|
1684
|
+
var FinishCommand = class {
|
|
1685
|
+
constructor(issueTracker, gitWorktreeManager, validationRunner, commitManager, mergeManager, identifierParser, resourceCleanup, buildRunner, settingsManager, loomManager) {
|
|
1686
|
+
const envResult = loadEnvIntoProcess();
|
|
1687
|
+
if (envResult.error) {
|
|
1688
|
+
logger.debug(`Environment loading warning: ${envResult.error.message}`);
|
|
1689
|
+
}
|
|
1690
|
+
if (envResult.parsed) {
|
|
1691
|
+
logger.debug(`Loaded ${Object.keys(envResult.parsed).length} environment variables`);
|
|
1692
|
+
}
|
|
1693
|
+
this.issueTracker = issueTracker;
|
|
1694
|
+
this.gitWorktreeManager = gitWorktreeManager ?? new GitWorktreeManager();
|
|
1695
|
+
this.validationRunner = validationRunner ?? new ValidationRunner();
|
|
1696
|
+
this.commitManager = commitManager ?? new CommitManager();
|
|
1697
|
+
this.mergeManager = mergeManager ?? new MergeManager();
|
|
1698
|
+
this.identifierParser = identifierParser ?? new IdentifierParser(this.gitWorktreeManager);
|
|
1699
|
+
this.settingsManager = settingsManager ?? new SettingsManager();
|
|
1700
|
+
if (resourceCleanup) {
|
|
1701
|
+
this.resourceCleanup = resourceCleanup;
|
|
1702
|
+
}
|
|
1703
|
+
this.buildRunner = buildRunner ?? new BuildRunner();
|
|
1704
|
+
if (loomManager) {
|
|
1705
|
+
this.loomManager = loomManager;
|
|
1706
|
+
}
|
|
1707
|
+
}
|
|
1708
|
+
/**
|
|
1709
|
+
* Lazy initialization of ResourceCleanup with properly configured DatabaseManager
|
|
1710
|
+
*/
|
|
1711
|
+
async ensureResourceCleanup() {
|
|
1712
|
+
var _a, _b;
|
|
1713
|
+
if (this.resourceCleanup && this.loomManager) {
|
|
1714
|
+
return;
|
|
1715
|
+
}
|
|
1716
|
+
const settings = await this.settingsManager.loadSettings();
|
|
1717
|
+
const databaseUrlEnvVarName = ((_b = (_a = settings.capabilities) == null ? void 0 : _a.database) == null ? void 0 : _b.databaseUrlEnvVarName) ?? "DATABASE_URL";
|
|
1718
|
+
const environmentManager = new EnvironmentManager();
|
|
1719
|
+
const neonProvider = createNeonProviderFromSettings(settings);
|
|
1720
|
+
const databaseManager = new DatabaseManager(neonProvider, environmentManager, databaseUrlEnvVarName);
|
|
1721
|
+
const cliIsolationManager = new CLIIsolationManager();
|
|
1722
|
+
const { DefaultBranchNamingService: DefaultBranchNamingService2 } = await import("./BranchNamingService-3OQPRSWT.js");
|
|
1723
|
+
this.loomManager ??= new LoomManager(
|
|
1724
|
+
this.gitWorktreeManager,
|
|
1725
|
+
this.issueTracker,
|
|
1726
|
+
new DefaultBranchNamingService2({ useClaude: true }),
|
|
1727
|
+
environmentManager,
|
|
1728
|
+
new ClaudeContextManager(),
|
|
1729
|
+
new ProjectCapabilityDetector(),
|
|
1730
|
+
cliIsolationManager,
|
|
1731
|
+
this.settingsManager,
|
|
1732
|
+
databaseManager
|
|
1733
|
+
);
|
|
1734
|
+
this.resourceCleanup ??= new ResourceCleanup(
|
|
1735
|
+
this.gitWorktreeManager,
|
|
1736
|
+
new ProcessManager(),
|
|
1737
|
+
databaseManager,
|
|
1738
|
+
cliIsolationManager
|
|
1739
|
+
);
|
|
1740
|
+
}
|
|
1741
|
+
/**
|
|
1742
|
+
* Check for child looms and exit gracefully if any exist
|
|
1743
|
+
* Always checks the TARGET loom (the one being finished), not the current directory's loom
|
|
1744
|
+
*
|
|
1745
|
+
* @param parsed - The parsed input identifying the loom being finished
|
|
1746
|
+
*/
|
|
1747
|
+
async checkForChildLooms(parsed) {
|
|
1748
|
+
await this.ensureResourceCleanup();
|
|
1749
|
+
if (!this.loomManager) {
|
|
1750
|
+
throw new Error("Failed to initialize LoomManager");
|
|
1751
|
+
}
|
|
1752
|
+
let targetBranch;
|
|
1753
|
+
if (parsed.branchName) {
|
|
1754
|
+
targetBranch = parsed.branchName;
|
|
1755
|
+
} else if (parsed.type === "issue" && parsed.number !== void 0) {
|
|
1756
|
+
const worktree = await this.gitWorktreeManager.findWorktreeForIssue(parsed.number);
|
|
1757
|
+
targetBranch = worktree == null ? void 0 : worktree.branch;
|
|
1758
|
+
} else if (parsed.type === "pr" && parsed.number !== void 0) {
|
|
1759
|
+
const prNumber = typeof parsed.number === "number" ? parsed.number : Number(parsed.number);
|
|
1760
|
+
if (isNaN(prNumber) || !isFinite(prNumber)) {
|
|
1761
|
+
throw new Error(`Invalid PR number: ${parsed.number}. PR numbers must be numeric.`);
|
|
1762
|
+
}
|
|
1763
|
+
const worktree = await this.gitWorktreeManager.findWorktreeForPR(prNumber, "");
|
|
1764
|
+
targetBranch = worktree == null ? void 0 : worktree.branch;
|
|
1765
|
+
}
|
|
1766
|
+
if (!targetBranch) {
|
|
1767
|
+
logger.debug(`Cannot determine target branch for child loom check`);
|
|
1768
|
+
return;
|
|
1769
|
+
}
|
|
1770
|
+
const hasChildLooms = await this.loomManager.checkAndWarnChildLooms(targetBranch);
|
|
1771
|
+
if (hasChildLooms) {
|
|
1772
|
+
logger.error("Cannot finish loom while child looms exist. Please finish child looms first.");
|
|
1773
|
+
process.exit(1);
|
|
1774
|
+
}
|
|
1775
|
+
}
|
|
1776
|
+
/**
|
|
1777
|
+
* Main entry point for finish command
|
|
1778
|
+
*/
|
|
1779
|
+
async execute(input) {
|
|
1780
|
+
try {
|
|
1781
|
+
const settings = await this.settingsManager.loadSettings();
|
|
1782
|
+
let repo;
|
|
1783
|
+
const multipleRemotes = await hasMultipleRemotes();
|
|
1784
|
+
if (multipleRemotes) {
|
|
1785
|
+
repo = await getConfiguredRepoFromSettings(settings);
|
|
1786
|
+
logger.info(`Using GitHub repository: ${repo}`);
|
|
1787
|
+
}
|
|
1788
|
+
const parsed = await this.parseInput(input.identifier, input.options);
|
|
1789
|
+
await this.checkForChildLooms(parsed);
|
|
1790
|
+
const worktrees = await this.validateInput(parsed, input.options, repo);
|
|
1791
|
+
logger.info(`Validated input: ${this.formatParsedInput(parsed)}`);
|
|
1792
|
+
const worktree = worktrees[0];
|
|
1793
|
+
if (!worktree) {
|
|
1794
|
+
throw new Error("No worktree found");
|
|
1795
|
+
}
|
|
1796
|
+
if (parsed.type === "pr") {
|
|
1797
|
+
if (!parsed.number) {
|
|
1798
|
+
throw new Error("Invalid PR number");
|
|
1799
|
+
}
|
|
1800
|
+
if (!this.issueTracker.supportsPullRequests || !this.issueTracker.fetchPR) {
|
|
1801
|
+
throw new Error("Issue tracker does not support pull requests");
|
|
1802
|
+
}
|
|
1803
|
+
const pr = await this.issueTracker.fetchPR(parsed.number, repo);
|
|
1804
|
+
await this.executePRWorkflow(parsed, input.options, worktree, pr);
|
|
1805
|
+
} else {
|
|
1806
|
+
await this.executeIssueWorkflow(parsed, input.options, worktree);
|
|
1807
|
+
}
|
|
1808
|
+
} catch (error) {
|
|
1809
|
+
if (error instanceof Error) {
|
|
1810
|
+
logger.error(`${error.message}`);
|
|
1811
|
+
} else {
|
|
1812
|
+
logger.error("An unknown error occurred");
|
|
1813
|
+
}
|
|
1814
|
+
throw error;
|
|
1815
|
+
}
|
|
1816
|
+
}
|
|
1817
|
+
/**
|
|
1818
|
+
* Parse input to determine type and extract relevant data
|
|
1819
|
+
* Supports auto-detection from current directory when identifier is undefined
|
|
1820
|
+
*/
|
|
1821
|
+
async parseInput(identifier, options) {
|
|
1822
|
+
if (options.pr !== void 0) {
|
|
1823
|
+
return {
|
|
1824
|
+
type: "pr",
|
|
1825
|
+
number: options.pr,
|
|
1826
|
+
originalInput: `--pr ${options.pr}`,
|
|
1827
|
+
autoDetected: false
|
|
1828
|
+
};
|
|
1829
|
+
}
|
|
1830
|
+
if (identifier == null ? void 0 : identifier.trim()) {
|
|
1831
|
+
return await this.parseExplicitInput(identifier.trim());
|
|
1832
|
+
}
|
|
1833
|
+
return await this.autoDetectFromCurrentDirectory();
|
|
1834
|
+
}
|
|
1835
|
+
/**
|
|
1836
|
+
* Parse explicit identifier input using pattern-based detection
|
|
1837
|
+
* (No GitHub API calls - uses IdentifierParser)
|
|
1838
|
+
*/
|
|
1839
|
+
async parseExplicitInput(identifier) {
|
|
1840
|
+
const prPattern = /^(?:pr|PR)[/-](\d+)$/;
|
|
1841
|
+
const prMatch = identifier.match(prPattern);
|
|
1842
|
+
if (prMatch == null ? void 0 : prMatch[1]) {
|
|
1843
|
+
return {
|
|
1844
|
+
type: "pr",
|
|
1845
|
+
number: parseInt(prMatch[1], 10),
|
|
1846
|
+
originalInput: identifier,
|
|
1847
|
+
autoDetected: false
|
|
1848
|
+
};
|
|
1849
|
+
}
|
|
1850
|
+
const parsed = await this.identifierParser.parseForPatternDetection(identifier);
|
|
1851
|
+
if (parsed.type === "description") {
|
|
1852
|
+
throw new Error("Description input type is not supported in finish command");
|
|
1853
|
+
}
|
|
1854
|
+
const result = {
|
|
1855
|
+
type: parsed.type,
|
|
1856
|
+
originalInput: parsed.originalInput,
|
|
1857
|
+
autoDetected: false
|
|
1858
|
+
};
|
|
1859
|
+
if (parsed.number !== void 0) {
|
|
1860
|
+
result.number = parsed.number;
|
|
1861
|
+
}
|
|
1862
|
+
if (parsed.branchName !== void 0) {
|
|
1863
|
+
result.branchName = parsed.branchName;
|
|
1864
|
+
}
|
|
1865
|
+
return result;
|
|
1866
|
+
}
|
|
1867
|
+
/**
|
|
1868
|
+
* Auto-detect PR or issue from current directory
|
|
1869
|
+
* Ports logic from merge-current-issue.sh lines 30-52
|
|
1870
|
+
*/
|
|
1871
|
+
async autoDetectFromCurrentDirectory() {
|
|
1872
|
+
const currentDir = path3.basename(process.cwd());
|
|
1873
|
+
const prPattern = /_pr_(\d+)$/;
|
|
1874
|
+
const prMatch = currentDir.match(prPattern);
|
|
1875
|
+
if (prMatch == null ? void 0 : prMatch[1]) {
|
|
1876
|
+
const prNumber = parseInt(prMatch[1], 10);
|
|
1877
|
+
logger.debug(`Auto-detected PR #${prNumber} from directory: ${currentDir}`);
|
|
1878
|
+
return {
|
|
1879
|
+
type: "pr",
|
|
1880
|
+
number: prNumber,
|
|
1881
|
+
originalInput: currentDir,
|
|
1882
|
+
autoDetected: true
|
|
1883
|
+
};
|
|
1884
|
+
}
|
|
1885
|
+
const issueNumber = extractIssueNumber(currentDir);
|
|
1886
|
+
if (issueNumber !== null) {
|
|
1887
|
+
logger.debug(
|
|
1888
|
+
`Auto-detected issue #${issueNumber} from directory: ${currentDir}`
|
|
1889
|
+
);
|
|
1890
|
+
return {
|
|
1891
|
+
type: "issue",
|
|
1892
|
+
number: issueNumber,
|
|
1893
|
+
originalInput: currentDir,
|
|
1894
|
+
autoDetected: true
|
|
1895
|
+
};
|
|
1896
|
+
}
|
|
1897
|
+
const repoInfo = await this.gitWorktreeManager.getRepoInfo();
|
|
1898
|
+
const currentBranch = repoInfo.currentBranch;
|
|
1899
|
+
if (!currentBranch) {
|
|
1900
|
+
throw new Error(
|
|
1901
|
+
"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"
|
|
1902
|
+
);
|
|
1903
|
+
}
|
|
1904
|
+
const branchIssueNumber = extractIssueNumber(currentBranch);
|
|
1905
|
+
if (branchIssueNumber !== null) {
|
|
1906
|
+
logger.debug(
|
|
1907
|
+
`Auto-detected issue #${branchIssueNumber} from branch: ${currentBranch}`
|
|
1908
|
+
);
|
|
1909
|
+
return {
|
|
1910
|
+
type: "issue",
|
|
1911
|
+
number: branchIssueNumber,
|
|
1912
|
+
originalInput: currentBranch,
|
|
1913
|
+
autoDetected: true
|
|
1914
|
+
};
|
|
1915
|
+
}
|
|
1916
|
+
return {
|
|
1917
|
+
type: "branch",
|
|
1918
|
+
branchName: currentBranch,
|
|
1919
|
+
originalInput: currentBranch,
|
|
1920
|
+
autoDetected: true
|
|
1921
|
+
};
|
|
1922
|
+
}
|
|
1923
|
+
/**
|
|
1924
|
+
* Validate the parsed input based on its type
|
|
1925
|
+
*/
|
|
1926
|
+
async validateInput(parsed, options, repo) {
|
|
1927
|
+
switch (parsed.type) {
|
|
1928
|
+
case "pr": {
|
|
1929
|
+
if (!parsed.number) {
|
|
1930
|
+
throw new Error("Invalid PR number");
|
|
1931
|
+
}
|
|
1932
|
+
if (!this.issueTracker.supportsPullRequests || !this.issueTracker.fetchPR) {
|
|
1933
|
+
throw new Error("Issue tracker does not support pull requests");
|
|
1934
|
+
}
|
|
1935
|
+
const pr = await this.issueTracker.fetchPR(parsed.number);
|
|
1936
|
+
logger.debug(`Validated PR #${parsed.number} (state: ${pr.state})`);
|
|
1937
|
+
return await this.findWorktreeForIdentifier(parsed);
|
|
1938
|
+
}
|
|
1939
|
+
case "issue": {
|
|
1940
|
+
if (!parsed.number) {
|
|
1941
|
+
throw new Error("Invalid issue number");
|
|
1942
|
+
}
|
|
1943
|
+
const issue = await this.issueTracker.fetchIssue(parsed.number, repo);
|
|
1944
|
+
if (issue.state === "closed" && !options.force) {
|
|
1945
|
+
throw new Error(
|
|
1946
|
+
`Issue #${parsed.number} is closed. Use --force to finish anyway.`
|
|
1947
|
+
);
|
|
1948
|
+
}
|
|
1949
|
+
logger.debug(`Validated issue #${parsed.number} (state: ${issue.state})`);
|
|
1950
|
+
return await this.findWorktreeForIdentifier(parsed);
|
|
1951
|
+
}
|
|
1952
|
+
case "branch": {
|
|
1953
|
+
if (!parsed.branchName) {
|
|
1954
|
+
throw new Error("Invalid branch name");
|
|
1955
|
+
}
|
|
1956
|
+
if (!this.isValidBranchName(parsed.branchName)) {
|
|
1957
|
+
throw new Error(
|
|
1958
|
+
"Invalid branch name. Use only letters, numbers, hyphens, underscores, and slashes"
|
|
1959
|
+
);
|
|
1960
|
+
}
|
|
1961
|
+
logger.debug(`Validated branch name: ${parsed.branchName}`);
|
|
1962
|
+
return await this.findWorktreeForIdentifier(parsed);
|
|
1963
|
+
}
|
|
1964
|
+
default: {
|
|
1965
|
+
const unknownType = parsed;
|
|
1966
|
+
throw new Error(`Unknown input type: ${unknownType.type}`);
|
|
1967
|
+
}
|
|
1968
|
+
}
|
|
1969
|
+
}
|
|
1970
|
+
/**
|
|
1971
|
+
* Find worktree for the given identifier using specific methods based on type
|
|
1972
|
+
* (uses precise pattern matching instead of broad substring matching)
|
|
1973
|
+
* Throws error if not found
|
|
1974
|
+
*/
|
|
1975
|
+
async findWorktreeForIdentifier(parsed) {
|
|
1976
|
+
let worktree = null;
|
|
1977
|
+
switch (parsed.type) {
|
|
1978
|
+
case "pr": {
|
|
1979
|
+
if (!parsed.number) {
|
|
1980
|
+
throw new Error("Invalid PR number");
|
|
1981
|
+
}
|
|
1982
|
+
const prNumber = typeof parsed.number === "number" ? parsed.number : Number(parsed.number);
|
|
1983
|
+
if (isNaN(prNumber) || !isFinite(prNumber)) {
|
|
1984
|
+
throw new Error(`Invalid PR number: ${parsed.number}. PR numbers must be numeric.`);
|
|
1985
|
+
}
|
|
1986
|
+
worktree = await this.gitWorktreeManager.findWorktreeForPR(
|
|
1987
|
+
prNumber,
|
|
1988
|
+
""
|
|
1989
|
+
);
|
|
1990
|
+
break;
|
|
1991
|
+
}
|
|
1992
|
+
case "issue": {
|
|
1993
|
+
if (!parsed.number) {
|
|
1994
|
+
throw new Error("Invalid issue number");
|
|
1995
|
+
}
|
|
1996
|
+
worktree = await this.gitWorktreeManager.findWorktreeForIssue(
|
|
1997
|
+
parsed.number
|
|
1998
|
+
);
|
|
1999
|
+
break;
|
|
2000
|
+
}
|
|
2001
|
+
case "branch": {
|
|
2002
|
+
if (!parsed.branchName) {
|
|
2003
|
+
throw new Error("Invalid branch name");
|
|
2004
|
+
}
|
|
2005
|
+
worktree = await this.gitWorktreeManager.findWorktreeForBranch(
|
|
2006
|
+
parsed.branchName
|
|
2007
|
+
);
|
|
2008
|
+
break;
|
|
2009
|
+
}
|
|
2010
|
+
default: {
|
|
2011
|
+
const unknownType = parsed;
|
|
2012
|
+
throw new Error(`Unknown input type: ${unknownType.type}`);
|
|
2013
|
+
}
|
|
2014
|
+
}
|
|
2015
|
+
if (!worktree) {
|
|
2016
|
+
throw new Error(
|
|
2017
|
+
`No worktree found for ${this.formatParsedInput(parsed)}. Use 'il list' to see available worktrees.`
|
|
2018
|
+
);
|
|
2019
|
+
}
|
|
2020
|
+
logger.debug(`Found worktree: ${worktree.path}`);
|
|
2021
|
+
return [worktree];
|
|
2022
|
+
}
|
|
2023
|
+
/**
|
|
2024
|
+
* Validate branch name format
|
|
2025
|
+
*/
|
|
2026
|
+
isValidBranchName(branch) {
|
|
2027
|
+
return /^[a-zA-Z0-9/_-]+$/.test(branch);
|
|
2028
|
+
}
|
|
2029
|
+
/**
|
|
2030
|
+
* Format parsed input for display
|
|
2031
|
+
*/
|
|
2032
|
+
formatParsedInput(parsed) {
|
|
2033
|
+
const autoLabel = parsed.autoDetected ? " (auto-detected)" : "";
|
|
2034
|
+
switch (parsed.type) {
|
|
2035
|
+
case "pr":
|
|
2036
|
+
return `PR #${parsed.number}${autoLabel}`;
|
|
2037
|
+
case "issue":
|
|
2038
|
+
return `Issue #${parsed.number}${autoLabel}`;
|
|
2039
|
+
case "branch":
|
|
2040
|
+
return `Branch '${parsed.branchName}'${autoLabel}`;
|
|
2041
|
+
default:
|
|
2042
|
+
return "Unknown input";
|
|
2043
|
+
}
|
|
2044
|
+
}
|
|
2045
|
+
/**
|
|
2046
|
+
* Execute workflow for issues and branches (merge into main)
|
|
2047
|
+
* This is the traditional workflow: validate → commit → rebase → merge → cleanup
|
|
2048
|
+
*/
|
|
2049
|
+
async executeIssueWorkflow(parsed, options, worktree) {
|
|
2050
|
+
var _a, _b;
|
|
2051
|
+
if (!options.dryRun) {
|
|
2052
|
+
logger.info("Running pre-merge validations...");
|
|
2053
|
+
await this.validationRunner.runValidations(worktree.path, {
|
|
2054
|
+
dryRun: options.dryRun ?? false
|
|
2055
|
+
});
|
|
2056
|
+
logger.success("All validations passed");
|
|
2057
|
+
} else {
|
|
2058
|
+
logger.info("[DRY RUN] Would run pre-merge validations");
|
|
2059
|
+
}
|
|
2060
|
+
const gitStatus = await this.commitManager.detectUncommittedChanges(worktree.path);
|
|
2061
|
+
if (gitStatus.hasUncommittedChanges) {
|
|
2062
|
+
if (options.dryRun) {
|
|
2063
|
+
logger.info("[DRY RUN] Would auto-commit uncommitted changes (validation passed)");
|
|
2064
|
+
} else {
|
|
2065
|
+
logger.info("Validation passed, auto-committing uncommitted changes...");
|
|
2066
|
+
const settings2 = await this.settingsManager.loadSettings(worktree.path);
|
|
2067
|
+
const skipVerify = ((_b = (_a = settings2.workflows) == null ? void 0 : _a.issue) == null ? void 0 : _b.noVerify) ?? false;
|
|
2068
|
+
const commitOptions = {
|
|
2069
|
+
dryRun: options.dryRun ?? false,
|
|
2070
|
+
skipVerify
|
|
2071
|
+
};
|
|
2072
|
+
if (parsed.type === "issue" && parsed.number) {
|
|
2073
|
+
commitOptions.issueNumber = parsed.number;
|
|
2074
|
+
}
|
|
2075
|
+
await this.commitManager.commitChanges(worktree.path, commitOptions);
|
|
2076
|
+
logger.success("Changes committed successfully");
|
|
2077
|
+
}
|
|
2078
|
+
} else {
|
|
2079
|
+
logger.debug("No uncommitted changes found");
|
|
2080
|
+
}
|
|
2081
|
+
const settings = await this.settingsManager.loadSettings(worktree.path);
|
|
2082
|
+
const mergeBehavior = settings.mergeBehavior ?? { mode: "local" };
|
|
2083
|
+
if (mergeBehavior.mode === "github-pr") {
|
|
2084
|
+
await this.executeGitHubPRWorkflow(parsed, options, worktree, settings);
|
|
2085
|
+
return;
|
|
2086
|
+
}
|
|
2087
|
+
logger.info("Rebasing branch on main...");
|
|
2088
|
+
const mergeOptions = {
|
|
2089
|
+
dryRun: options.dryRun ?? false,
|
|
2090
|
+
force: options.force ?? false
|
|
2091
|
+
};
|
|
2092
|
+
await this.mergeManager.rebaseOnMain(worktree.path, mergeOptions);
|
|
2093
|
+
logger.success("Branch rebased successfully");
|
|
2094
|
+
logger.info("Performing fast-forward merge...");
|
|
2095
|
+
await this.mergeManager.performFastForwardMerge(worktree.branch, worktree.path, mergeOptions);
|
|
2096
|
+
logger.success("Fast-forward merge completed successfully");
|
|
2097
|
+
if (options.dryRun) {
|
|
2098
|
+
logger.info("[DRY RUN] Would install dependencies in main worktree");
|
|
2099
|
+
} else {
|
|
2100
|
+
logger.info("Installing dependencies in main worktree...");
|
|
2101
|
+
const mainWorktreePath = await findMainWorktreePathWithSettings(worktree.path, this.settingsManager);
|
|
2102
|
+
await installDependencies(mainWorktreePath, true, true);
|
|
2103
|
+
}
|
|
2104
|
+
if (!options.skipBuild) {
|
|
2105
|
+
await this.runPostMergeBuild(worktree.path, options);
|
|
2106
|
+
} else {
|
|
2107
|
+
logger.debug("Skipping build verification (--skip-build flag provided)");
|
|
2108
|
+
}
|
|
2109
|
+
await this.performPostMergeCleanup(parsed, options, worktree);
|
|
2110
|
+
}
|
|
2111
|
+
/**
|
|
2112
|
+
* Execute workflow for Pull Requests
|
|
2113
|
+
* Behavior depends on PR state:
|
|
2114
|
+
* - OPEN: Commit changes, push to remote, keep worktree active
|
|
2115
|
+
* - CLOSED/MERGED: Skip to cleanup
|
|
2116
|
+
*/
|
|
2117
|
+
async executePRWorkflow(parsed, options, worktree, pr) {
|
|
2118
|
+
var _a, _b;
|
|
2119
|
+
if (pr.state === "closed" || pr.state === "merged") {
|
|
2120
|
+
logger.info(`PR #${parsed.number} is ${pr.state.toUpperCase()} - skipping to cleanup`);
|
|
2121
|
+
const gitStatus = await this.commitManager.detectUncommittedChanges(worktree.path);
|
|
2122
|
+
if (gitStatus.hasUncommittedChanges && !options.force) {
|
|
2123
|
+
logger.warn("PR has uncommitted changes");
|
|
2124
|
+
throw new Error(
|
|
2125
|
+
"Cannot cleanup PR with uncommitted changes. Commit or stash changes, then run again with --force to cleanup anyway."
|
|
2126
|
+
);
|
|
2127
|
+
}
|
|
2128
|
+
await this.performPRCleanup(parsed, options, worktree);
|
|
2129
|
+
logger.success(`PR #${parsed.number} cleanup completed`);
|
|
2130
|
+
} else {
|
|
2131
|
+
logger.info(`PR #${parsed.number} is OPEN - will push changes and keep worktree active`);
|
|
2132
|
+
const gitStatus = await this.commitManager.detectUncommittedChanges(worktree.path);
|
|
2133
|
+
if (gitStatus.hasUncommittedChanges) {
|
|
2134
|
+
if (options.dryRun) {
|
|
2135
|
+
logger.info("[DRY RUN] Would commit uncommitted changes");
|
|
2136
|
+
} else {
|
|
2137
|
+
logger.info("Committing uncommitted changes...");
|
|
2138
|
+
const settings = await this.settingsManager.loadSettings(worktree.path);
|
|
2139
|
+
const skipVerify = ((_b = (_a = settings.workflows) == null ? void 0 : _a.pr) == null ? void 0 : _b.noVerify) ?? false;
|
|
2140
|
+
await this.commitManager.commitChanges(worktree.path, {
|
|
2141
|
+
dryRun: false,
|
|
2142
|
+
skipVerify
|
|
2143
|
+
// Do NOT pass issueNumber for PRs - no "Fixes #" trailer needed
|
|
2144
|
+
});
|
|
2145
|
+
logger.success("Changes committed");
|
|
2146
|
+
}
|
|
2147
|
+
} else {
|
|
2148
|
+
logger.debug("No uncommitted changes found");
|
|
2149
|
+
}
|
|
2150
|
+
if (options.dryRun) {
|
|
2151
|
+
logger.info(`[DRY RUN] Would push changes to origin/${pr.branch}`);
|
|
2152
|
+
} else {
|
|
2153
|
+
logger.info("Pushing changes to remote...");
|
|
2154
|
+
await pushBranchToRemote(pr.branch, worktree.path, {
|
|
2155
|
+
dryRun: false
|
|
2156
|
+
});
|
|
2157
|
+
logger.success(`Changes pushed to PR #${parsed.number}`);
|
|
2158
|
+
}
|
|
2159
|
+
logger.success(`PR #${parsed.number} updated successfully`);
|
|
2160
|
+
logger.info("Worktree remains active for continued work");
|
|
2161
|
+
logger.info(`To cleanup when done: il cleanup ${parsed.number}`);
|
|
2162
|
+
}
|
|
2163
|
+
}
|
|
2164
|
+
/**
|
|
2165
|
+
* Execute workflow for GitHub PR creation (github-pr merge mode)
|
|
2166
|
+
* Validates → Commits → Pushes → Creates PR → Prompts for cleanup
|
|
2167
|
+
*/
|
|
2168
|
+
async executeGitHubPRWorkflow(parsed, options, worktree, settings) {
|
|
2169
|
+
if (options.dryRun) {
|
|
2170
|
+
logger.info("[DRY RUN] Would push branch to origin");
|
|
2171
|
+
} else {
|
|
2172
|
+
logger.info("Pushing branch to origin...");
|
|
2173
|
+
await pushBranchToRemote(worktree.branch, worktree.path, { dryRun: false });
|
|
2174
|
+
logger.success("Branch pushed successfully");
|
|
2175
|
+
}
|
|
2176
|
+
const prManager = new PRManager(settings);
|
|
2177
|
+
let prTitle = `Work from ${worktree.branch}`;
|
|
2178
|
+
if (parsed.type === "issue" && parsed.number) {
|
|
2179
|
+
try {
|
|
2180
|
+
const issue = await this.issueTracker.fetchIssue(parsed.number);
|
|
2181
|
+
prTitle = issue.title;
|
|
2182
|
+
} catch (error) {
|
|
2183
|
+
logger.debug("Could not fetch issue title, using branch name", { error });
|
|
2184
|
+
}
|
|
2185
|
+
}
|
|
2186
|
+
if (options.dryRun) {
|
|
2187
|
+
logger.info("[DRY RUN] Would create GitHub PR");
|
|
2188
|
+
logger.info(` Title: ${prTitle}`);
|
|
2189
|
+
logger.info(` Base: ${settings.mainBranch ?? "main"}`);
|
|
2190
|
+
} else {
|
|
2191
|
+
const baseBranch = settings.mainBranch ?? "main";
|
|
2192
|
+
const openInBrowser = options.noBrowser !== true;
|
|
2193
|
+
const result = await prManager.createOrOpenPR(
|
|
2194
|
+
worktree.branch,
|
|
2195
|
+
prTitle,
|
|
2196
|
+
parsed.type === "issue" ? parsed.number : void 0,
|
|
2197
|
+
baseBranch,
|
|
2198
|
+
worktree.path,
|
|
2199
|
+
openInBrowser
|
|
2200
|
+
);
|
|
2201
|
+
if (result.wasExisting) {
|
|
2202
|
+
logger.success(`Existing pull request: ${result.url}`);
|
|
2203
|
+
} else {
|
|
2204
|
+
logger.success(`Pull request created: ${result.url}`);
|
|
2205
|
+
}
|
|
2206
|
+
await this.handlePRCleanupPrompt(parsed, options, worktree);
|
|
2207
|
+
}
|
|
2208
|
+
}
|
|
2209
|
+
/**
|
|
2210
|
+
* Handle cleanup prompt after PR creation
|
|
2211
|
+
* Respects --cleanup and --no-cleanup flags, otherwise prompts user
|
|
2212
|
+
*/
|
|
2213
|
+
async handlePRCleanupPrompt(parsed, options, worktree) {
|
|
2214
|
+
if (options.cleanup === true) {
|
|
2215
|
+
logger.info("Cleaning up worktree (--cleanup flag)...");
|
|
2216
|
+
await this.performWorktreeCleanup(parsed, options, worktree);
|
|
2217
|
+
} else if (options.cleanup === false) {
|
|
2218
|
+
logger.info("Worktree kept active for continued work (--no-cleanup flag)");
|
|
2219
|
+
logger.info(`To cleanup later: il cleanup ${parsed.originalInput}`);
|
|
2220
|
+
} else {
|
|
2221
|
+
logger.info("");
|
|
2222
|
+
logger.info("PR created successfully. Would you like to clean up the worktree?");
|
|
2223
|
+
logger.info(` Worktree: ${worktree.path}`);
|
|
2224
|
+
logger.info(` Branch: ${worktree.branch}`);
|
|
2225
|
+
logger.info("");
|
|
2226
|
+
const shouldCleanup = await promptConfirmation(
|
|
2227
|
+
"Clean up worktree now?",
|
|
2228
|
+
false
|
|
2229
|
+
// Default to keeping worktree (safer option)
|
|
2230
|
+
);
|
|
2231
|
+
if (shouldCleanup) {
|
|
2232
|
+
await this.performWorktreeCleanup(parsed, options, worktree);
|
|
2233
|
+
} else {
|
|
2234
|
+
logger.info("Worktree kept active. Run `il cleanup` when ready.");
|
|
2235
|
+
}
|
|
2236
|
+
}
|
|
2237
|
+
}
|
|
2238
|
+
/**
|
|
2239
|
+
* Perform worktree cleanup (used by GitHub PR workflow)
|
|
2240
|
+
* Similar to performPostMergeCleanup but for PR workflow
|
|
2241
|
+
*/
|
|
2242
|
+
async performWorktreeCleanup(parsed, options, worktree) {
|
|
2243
|
+
const cleanupInput = {
|
|
2244
|
+
type: parsed.type,
|
|
2245
|
+
originalInput: parsed.originalInput,
|
|
2246
|
+
...parsed.number !== void 0 && { number: parsed.number },
|
|
2247
|
+
...parsed.branchName !== void 0 && { branchName: parsed.branchName }
|
|
2248
|
+
};
|
|
2249
|
+
const cleanupOptions = {
|
|
2250
|
+
dryRun: options.dryRun ?? false,
|
|
2251
|
+
deleteBranch: false,
|
|
2252
|
+
// Don't delete branch - PR still needs it
|
|
2253
|
+
keepDatabase: false,
|
|
2254
|
+
// Clean up database
|
|
2255
|
+
force: options.force ?? false
|
|
2256
|
+
};
|
|
2257
|
+
try {
|
|
2258
|
+
logger.info("Starting worktree cleanup...");
|
|
2259
|
+
await this.ensureResourceCleanup();
|
|
2260
|
+
if (!this.resourceCleanup) {
|
|
2261
|
+
throw new Error("Failed to initialize ResourceCleanup");
|
|
2262
|
+
}
|
|
2263
|
+
const result = await this.resourceCleanup.cleanupWorktree(cleanupInput, cleanupOptions);
|
|
2264
|
+
this.reportCleanupResults(result);
|
|
2265
|
+
if (!result.success) {
|
|
2266
|
+
logger.warn("Some cleanup operations failed - manual cleanup may be required");
|
|
2267
|
+
this.showManualCleanupInstructions(worktree);
|
|
2268
|
+
} else {
|
|
2269
|
+
logger.success("Worktree cleanup completed successfully");
|
|
2270
|
+
}
|
|
2271
|
+
if (this.isRunningFromWithinWorktree(worktree.path)) {
|
|
2272
|
+
this.showTerminalCloseWarning(worktree);
|
|
2273
|
+
}
|
|
2274
|
+
} catch (error) {
|
|
2275
|
+
const errorMessage = error instanceof Error ? error.message : "Unknown error";
|
|
2276
|
+
logger.warn(`Cleanup failed: ${errorMessage}`);
|
|
2277
|
+
logger.warn("Manual cleanup may be required");
|
|
2278
|
+
this.showManualCleanupInstructions(worktree);
|
|
2279
|
+
}
|
|
2280
|
+
}
|
|
2281
|
+
/**
|
|
2282
|
+
* Perform cleanup for closed/merged PRs
|
|
2283
|
+
* Similar to performPostMergeCleanup but with different messaging
|
|
2284
|
+
*/
|
|
2285
|
+
async performPRCleanup(parsed, options, worktree) {
|
|
2286
|
+
const cleanupInput = {
|
|
2287
|
+
type: parsed.type,
|
|
2288
|
+
originalInput: parsed.originalInput,
|
|
2289
|
+
...parsed.number !== void 0 && { number: parsed.number },
|
|
2290
|
+
...parsed.branchName !== void 0 && { branchName: parsed.branchName }
|
|
2291
|
+
};
|
|
2292
|
+
const cleanupOptions = {
|
|
2293
|
+
dryRun: options.dryRun ?? false,
|
|
2294
|
+
deleteBranch: true,
|
|
2295
|
+
// Delete branch for closed/merged PRs
|
|
2296
|
+
keepDatabase: false,
|
|
2297
|
+
force: options.force ?? false
|
|
2298
|
+
};
|
|
2299
|
+
try {
|
|
2300
|
+
await this.ensureResourceCleanup();
|
|
2301
|
+
if (!this.resourceCleanup) {
|
|
2302
|
+
throw new Error("Failed to initialize ResourceCleanup");
|
|
2303
|
+
}
|
|
2304
|
+
const result = await this.resourceCleanup.cleanupWorktree(cleanupInput, cleanupOptions);
|
|
2305
|
+
this.reportCleanupResults(result);
|
|
2306
|
+
if (!result.success) {
|
|
2307
|
+
logger.warn("Some cleanup operations failed - manual cleanup may be required");
|
|
2308
|
+
this.showManualCleanupInstructions(worktree);
|
|
2309
|
+
} else {
|
|
2310
|
+
if (this.isRunningFromWithinWorktree(worktree.path)) {
|
|
2311
|
+
this.showTerminalCloseWarning(worktree);
|
|
2312
|
+
}
|
|
2313
|
+
}
|
|
2314
|
+
} catch (error) {
|
|
2315
|
+
const errorMessage = error instanceof Error ? error.message : "Unknown error";
|
|
2316
|
+
logger.warn(`Cleanup failed: ${errorMessage}`);
|
|
2317
|
+
this.showManualCleanupInstructions(worktree);
|
|
2318
|
+
throw error;
|
|
2319
|
+
}
|
|
2320
|
+
}
|
|
2321
|
+
/**
|
|
2322
|
+
* Run post-merge build verification for CLI projects
|
|
2323
|
+
* Runs in main worktree to verify merged code builds successfully
|
|
2324
|
+
*/
|
|
2325
|
+
async runPostMergeBuild(worktreePath, options) {
|
|
2326
|
+
const mainWorktreePath = await findMainWorktreePathWithSettings(worktreePath, this.settingsManager);
|
|
2327
|
+
if (options.dryRun) {
|
|
2328
|
+
logger.info("[DRY RUN] Would run post-merge build");
|
|
2329
|
+
return;
|
|
2330
|
+
}
|
|
2331
|
+
logger.info("Running post-merge build...");
|
|
2332
|
+
const result = await this.buildRunner.runBuild(mainWorktreePath, {
|
|
2333
|
+
dryRun: options.dryRun ?? false
|
|
2334
|
+
});
|
|
2335
|
+
if (result.skipped) {
|
|
2336
|
+
logger.debug(`Build skipped: ${result.reason}`);
|
|
2337
|
+
} else {
|
|
2338
|
+
logger.success("Post-merge build completed successfully");
|
|
2339
|
+
}
|
|
2340
|
+
}
|
|
2341
|
+
/**
|
|
2342
|
+
* Perform post-merge cleanup operations
|
|
2343
|
+
* Converts ParsedFinishInput to ParsedInput and calls ResourceCleanup
|
|
2344
|
+
* Handles failures gracefully without throwing
|
|
2345
|
+
*/
|
|
2346
|
+
async performPostMergeCleanup(parsed, options, worktree) {
|
|
2347
|
+
await this.ensureResourceCleanup();
|
|
2348
|
+
if (!this.loomManager) {
|
|
2349
|
+
throw new Error("Failed to initialize LoomManager");
|
|
2350
|
+
}
|
|
2351
|
+
await this.checkForChildLooms(parsed);
|
|
2352
|
+
const cleanupInput = {
|
|
2353
|
+
type: parsed.type,
|
|
2354
|
+
originalInput: parsed.originalInput,
|
|
2355
|
+
...parsed.number !== void 0 && { number: parsed.number },
|
|
2356
|
+
...parsed.branchName !== void 0 && { branchName: parsed.branchName }
|
|
2357
|
+
};
|
|
2358
|
+
const cleanupOptions = {
|
|
2359
|
+
dryRun: options.dryRun ?? false,
|
|
2360
|
+
deleteBranch: true,
|
|
2361
|
+
// Delete branch after successful merge
|
|
2362
|
+
keepDatabase: false,
|
|
2363
|
+
// Clean up database after merge
|
|
2364
|
+
force: options.force ?? false
|
|
2365
|
+
};
|
|
2366
|
+
try {
|
|
2367
|
+
logger.info("Starting post-merge cleanup...");
|
|
2368
|
+
if (!this.resourceCleanup) {
|
|
2369
|
+
throw new Error("Failed to initialize ResourceCleanup");
|
|
2370
|
+
}
|
|
2371
|
+
const result = await this.resourceCleanup.cleanupWorktree(cleanupInput, cleanupOptions);
|
|
2372
|
+
this.reportCleanupResults(result);
|
|
2373
|
+
if (!result.success) {
|
|
2374
|
+
logger.warn("Some cleanup operations failed - manual cleanup may be required");
|
|
2375
|
+
this.showManualCleanupInstructions(worktree);
|
|
2376
|
+
} else {
|
|
2377
|
+
logger.success("Post-merge cleanup completed successfully");
|
|
2378
|
+
}
|
|
2379
|
+
if (this.isRunningFromWithinWorktree(worktree.path)) {
|
|
2380
|
+
this.showTerminalCloseWarning(worktree);
|
|
2381
|
+
}
|
|
2382
|
+
} catch (error) {
|
|
2383
|
+
const errorMessage = error instanceof Error ? error.message : "Unknown error";
|
|
2384
|
+
logger.warn(`Cleanup failed: ${errorMessage}`);
|
|
2385
|
+
logger.warn("Merge completed successfully, but manual cleanup is required");
|
|
2386
|
+
this.showManualCleanupInstructions(worktree);
|
|
2387
|
+
}
|
|
2388
|
+
}
|
|
2389
|
+
/**
|
|
2390
|
+
* Report cleanup operation results to user
|
|
2391
|
+
*/
|
|
2392
|
+
reportCleanupResults(result) {
|
|
2393
|
+
if (result.operations.length === 0) {
|
|
2394
|
+
return;
|
|
2395
|
+
}
|
|
2396
|
+
logger.info("Cleanup operations:");
|
|
2397
|
+
for (const op of result.operations) {
|
|
2398
|
+
const status = op.success ? "\u2713" : "\u2717";
|
|
2399
|
+
const message = op.error ? `${op.message}: ${op.error}` : op.message;
|
|
2400
|
+
if (op.success) {
|
|
2401
|
+
logger.info(` ${status} ${message}`);
|
|
2402
|
+
} else {
|
|
2403
|
+
logger.warn(` ${status} ${message}`);
|
|
2404
|
+
}
|
|
2405
|
+
}
|
|
2406
|
+
}
|
|
2407
|
+
/**
|
|
2408
|
+
* Show manual cleanup instructions when cleanup fails
|
|
2409
|
+
*/
|
|
2410
|
+
showManualCleanupInstructions(worktree) {
|
|
2411
|
+
logger.info("\nManual cleanup commands:");
|
|
2412
|
+
logger.info(` 1. Remove worktree: git worktree remove ${worktree.path}`);
|
|
2413
|
+
logger.info(` 2. Delete branch: git branch -d ${worktree.branch}`);
|
|
2414
|
+
logger.info(` 3. Check dev servers: lsof -i :PORT (and kill if needed)`);
|
|
2415
|
+
}
|
|
2416
|
+
/**
|
|
2417
|
+
* Check if current working directory is within the target worktree
|
|
2418
|
+
*/
|
|
2419
|
+
isRunningFromWithinWorktree(worktreePath) {
|
|
2420
|
+
const normalizedCwd = path3.normalize(process.cwd());
|
|
2421
|
+
const normalizedWorktree = path3.normalize(worktreePath);
|
|
2422
|
+
return normalizedCwd.startsWith(normalizedWorktree);
|
|
2423
|
+
}
|
|
2424
|
+
/**
|
|
2425
|
+
* Display warning to close terminal/IDE when running from within finished loom
|
|
2426
|
+
*/
|
|
2427
|
+
showTerminalCloseWarning(worktree) {
|
|
2428
|
+
logger.info("");
|
|
2429
|
+
logger.info("You are currently in the directory of the loom that was just finished.");
|
|
2430
|
+
logger.info("Please close this terminal and any IDE/terminal windows using this directory.");
|
|
2431
|
+
logger.info(`Directory: ${worktree.path}`);
|
|
2432
|
+
}
|
|
2433
|
+
};
|
|
2434
|
+
|
|
16
2435
|
// src/utils/package-info.ts
|
|
17
2436
|
import { readFileSync } from "fs";
|
|
18
2437
|
import { fileURLToPath } from "url";
|
|
@@ -42,6 +2461,10 @@ function getPackageInfo(scriptPath) {
|
|
|
42
2461
|
import { fileURLToPath as fileURLToPath2 } from "url";
|
|
43
2462
|
var __filename = fileURLToPath2(import.meta.url);
|
|
44
2463
|
var packageJson = getPackageInfo(__filename);
|
|
2464
|
+
function parseIssueIdentifier(value) {
|
|
2465
|
+
const parsed = parseInt(value, 10);
|
|
2466
|
+
return !isNaN(parsed) && String(parsed) === value ? parsed : value;
|
|
2467
|
+
}
|
|
45
2468
|
program.name("iloom").description(packageJson.description).version(packageJson.version).option("--debug", "Enable debug output (default: based on ILOOM_DEBUG env var)").option("--completion", "Output shell completion script for current shell").option("--set <key=value>", "Override any setting using dot notation (repeatable, e.g., --set workflows.issue.startIde=false)").allowUnknownOption().hook("preAction", async (thisCommand) => {
|
|
46
2469
|
const options = thisCommand.opts();
|
|
47
2470
|
const envDebug = process.env.ILOOM_DEBUG === "true";
|
|
@@ -60,7 +2483,7 @@ program.name("iloom").description(packageJson.description).version(packageJson.v
|
|
|
60
2483
|
} catch {
|
|
61
2484
|
}
|
|
62
2485
|
try {
|
|
63
|
-
const { SettingsMigrationManager } = await import("./SettingsMigrationManager-
|
|
2486
|
+
const { SettingsMigrationManager } = await import("./SettingsMigrationManager-AGIIIPDQ.js");
|
|
64
2487
|
const migrationManager = new SettingsMigrationManager();
|
|
65
2488
|
await migrationManager.migrateSettingsIfNeeded();
|
|
66
2489
|
} catch (error) {
|
|
@@ -76,11 +2499,10 @@ async function validateSettingsForCommand(command) {
|
|
|
76
2499
|
return;
|
|
77
2500
|
}
|
|
78
2501
|
try {
|
|
79
|
-
const { SettingsManager } = await import("./SettingsManager-SKLUVE3K.js");
|
|
80
2502
|
const settingsManager = new SettingsManager();
|
|
81
2503
|
const settings = await settingsManager.loadSettings();
|
|
82
|
-
const { hasMultipleRemotes } = await import("./remote-
|
|
83
|
-
const multipleRemotes = await
|
|
2504
|
+
const { hasMultipleRemotes: hasMultipleRemotes2 } = await import("./remote-VUNCQZ6J.js");
|
|
2505
|
+
const multipleRemotes = await hasMultipleRemotes2();
|
|
84
2506
|
if (multipleRemotes && !((_b = (_a = settings.issueManagement) == null ? void 0 : _a.github) == null ? void 0 : _b.remote)) {
|
|
85
2507
|
await autoLaunchInitForMultipleRemotes();
|
|
86
2508
|
return;
|
|
@@ -98,22 +2520,22 @@ async function autoLaunchInitForMultipleRemotes() {
|
|
|
98
2520
|
logger.info("iloom will now launch an interactive configuration session with Claude");
|
|
99
2521
|
logger.info("to help you select which remote to use for GitHub operations.");
|
|
100
2522
|
logger.info("");
|
|
101
|
-
const { waitForKeypress } = await import("./prompt-
|
|
102
|
-
await
|
|
2523
|
+
const { waitForKeypress: waitForKeypress2 } = await import("./prompt-7INJ7YRU.js");
|
|
2524
|
+
await waitForKeypress2("Press any key to start configuration...");
|
|
103
2525
|
logger.info("");
|
|
104
2526
|
try {
|
|
105
|
-
const { InitCommand } = await import("./init-
|
|
2527
|
+
const { InitCommand } = await import("./init-G3T64SC4.js");
|
|
106
2528
|
const initCommand = new InitCommand();
|
|
107
2529
|
const customInitialMessage = "Help me configure which git remote iloom should use for GitHub operations. I have multiple remotes and need to select the correct one.";
|
|
108
2530
|
await initCommand.execute(customInitialMessage);
|
|
109
2531
|
logger.info("");
|
|
110
2532
|
logger.info("Configuration complete! Continuing with your original command...");
|
|
111
2533
|
logger.info("");
|
|
112
|
-
const { SettingsManager } = await import("./SettingsManager-
|
|
113
|
-
const settingsManager = new
|
|
2534
|
+
const { SettingsManager: SettingsManager2 } = await import("./SettingsManager-WHHFGSL7.js");
|
|
2535
|
+
const settingsManager = new SettingsManager2();
|
|
114
2536
|
const settings = await settingsManager.loadSettings();
|
|
115
|
-
const { hasMultipleRemotes } = await import("./remote-
|
|
116
|
-
const multipleRemotes = await
|
|
2537
|
+
const { hasMultipleRemotes: hasMultipleRemotes2 } = await import("./remote-VUNCQZ6J.js");
|
|
2538
|
+
const multipleRemotes = await hasMultipleRemotes2();
|
|
117
2539
|
if (multipleRemotes && !((_b = (_a = settings.issueManagement) == null ? void 0 : _a.github) == null ? void 0 : _b.remote)) {
|
|
118
2540
|
logger.error("Configuration incomplete: GitHub remote is still not configured.");
|
|
119
2541
|
logger.info('Please run "iloom init" again and configure the GitHub remote setting.');
|
|
@@ -128,21 +2550,23 @@ async function autoLaunchInitForMultipleRemotes() {
|
|
|
128
2550
|
}
|
|
129
2551
|
var shellCompletion = new ShellCompletion();
|
|
130
2552
|
shellCompletion.init();
|
|
131
|
-
program.command("start").alias("create").alias("up").description("Create isolated workspace for an issue/PR").argument("[identifier]", "Issue number, PR number, or branch name (optional - will prompt if not provided)").option("--claude", "Enable Claude integration (default: true)").option("--no-claude", "Disable Claude integration").option("--code", "Enable VSCode (default: true)").option("--no-code", "Disable VSCode").option("--dev-server", "Enable dev server in terminal (default: true)").option("--no-dev-server", "Disable dev server").option("--terminal", "Enable terminal without dev server (default: false)").option("--no-terminal", "Disable terminal").addOption(
|
|
2553
|
+
program.command("start").alias("create").alias("up").description("Create isolated workspace for an issue/PR").argument("[identifier]", "Issue number, PR number, or branch name (optional - will prompt if not provided)").option("--claude", "Enable Claude integration (default: true)").option("--no-claude", "Disable Claude integration").option("--code", "Enable VSCode (default: true)").option("--no-code", "Disable VSCode").option("--dev-server", "Enable dev server in terminal (default: true)").option("--no-dev-server", "Disable dev server").option("--terminal", "Enable terminal without dev server (default: false)").option("--no-terminal", "Disable terminal").option("--child-loom", "Force create as child loom (skip prompt)").option("--no-child-loom", "Force create as independent loom (skip prompt)").option("--body <text>", "Body text for issue (skips AI enhancement)").addOption(
|
|
132
2554
|
new Option("--one-shot <mode>", "One-shot automation mode").choices(["default", "noReview", "bypassPermissions"]).default("default")
|
|
133
2555
|
).action(async (identifier, options) => {
|
|
134
2556
|
try {
|
|
135
2557
|
let finalIdentifier = identifier;
|
|
136
2558
|
if (!finalIdentifier) {
|
|
137
|
-
const { promptInput } = await import("./prompt-
|
|
2559
|
+
const { promptInput } = await import("./prompt-7INJ7YRU.js");
|
|
138
2560
|
finalIdentifier = await promptInput("Enter issue number, PR number (pr/123), or branch name");
|
|
139
2561
|
if (!(finalIdentifier == null ? void 0 : finalIdentifier.trim())) {
|
|
140
2562
|
logger.error("Identifier is required");
|
|
141
2563
|
process.exit(1);
|
|
142
2564
|
}
|
|
143
2565
|
}
|
|
144
|
-
const
|
|
145
|
-
const
|
|
2566
|
+
const settingsManager = new SettingsManager();
|
|
2567
|
+
const settings = await settingsManager.loadSettings();
|
|
2568
|
+
const issueTracker = IssueTrackerFactory.create(settings);
|
|
2569
|
+
const command = new StartCommand(issueTracker, void 0, void 0, settingsManager);
|
|
146
2570
|
await command.execute({ identifier: finalIdentifier, options });
|
|
147
2571
|
} catch (error) {
|
|
148
2572
|
logger.error(`Failed to start workspace: ${error instanceof Error ? error.message : "Unknown error"}`);
|
|
@@ -151,8 +2575,11 @@ program.command("start").alias("create").alias("up").description("Create isolate
|
|
|
151
2575
|
});
|
|
152
2576
|
program.command("add-issue").alias("a").description("Create and enhance GitHub issue without starting workspace").argument("<description>", "Natural language description of the issue (>50 chars, >2 spaces)").action(async (description) => {
|
|
153
2577
|
try {
|
|
154
|
-
const
|
|
155
|
-
const
|
|
2578
|
+
const settingsManager = new SettingsManager();
|
|
2579
|
+
const settings = await settingsManager.loadSettings();
|
|
2580
|
+
const issueTracker = IssueTrackerFactory.create(settings);
|
|
2581
|
+
const enhancementService = new IssueEnhancementService(issueTracker, new AgentManager(), settingsManager);
|
|
2582
|
+
const command = new AddIssueCommand(enhancementService, settingsManager);
|
|
156
2583
|
const issueNumber = await command.execute({
|
|
157
2584
|
description,
|
|
158
2585
|
options: {}
|
|
@@ -164,13 +2591,17 @@ program.command("add-issue").alias("a").description("Create and enhance GitHub i
|
|
|
164
2591
|
process.exit(1);
|
|
165
2592
|
}
|
|
166
2593
|
});
|
|
167
|
-
program.command("feedback").alias("f").description("Submit feedback/bug report to iloom-cli repository").argument("<description>", "Natural language description of feedback (>50 chars, >2 spaces)").action(async (description) => {
|
|
2594
|
+
program.command("feedback").alias("f").description("Submit feedback/bug report to iloom-cli repository").argument("<description>", "Natural language description of feedback (>50 chars, >2 spaces)").option("--body <text>", "Body text for feedback (added after diagnostics)").action(async (description, options) => {
|
|
168
2595
|
try {
|
|
169
|
-
const { FeedbackCommand } = await import("./feedback-
|
|
2596
|
+
const { FeedbackCommand } = await import("./feedback-N4ECWIPF.js");
|
|
170
2597
|
const command = new FeedbackCommand();
|
|
2598
|
+
const feedbackOptions = {};
|
|
2599
|
+
if (options.body !== void 0) {
|
|
2600
|
+
feedbackOptions.body = options.body;
|
|
2601
|
+
}
|
|
171
2602
|
const issueNumber = await command.execute({
|
|
172
2603
|
description,
|
|
173
|
-
options:
|
|
2604
|
+
options: feedbackOptions
|
|
174
2605
|
});
|
|
175
2606
|
logger.success(`Feedback submitted as issue #${issueNumber} in iloom-cli repository`);
|
|
176
2607
|
process.exit(0);
|
|
@@ -179,10 +2610,12 @@ program.command("feedback").alias("f").description("Submit feedback/bug report t
|
|
|
179
2610
|
process.exit(1);
|
|
180
2611
|
}
|
|
181
2612
|
});
|
|
182
|
-
program.command("enhance").description("Apply enhancement agent to existing GitHub issue").argument("<issue-number>", "GitHub issue
|
|
2613
|
+
program.command("enhance").description("Apply enhancement agent to existing GitHub issue").argument("<issue-number>", "GitHub issue identifier to enhance", parseIssueIdentifier).option("--no-browser", "Skip browser opening prompt").option("--author <username>", "GitHub username to tag in questions (for CI usage)").action(async (issueNumber, options) => {
|
|
183
2614
|
try {
|
|
184
|
-
const
|
|
185
|
-
const
|
|
2615
|
+
const settingsManager = new SettingsManager();
|
|
2616
|
+
const settings = await settingsManager.loadSettings();
|
|
2617
|
+
const issueTracker = IssueTrackerFactory.create(settings);
|
|
2618
|
+
const command = new EnhanceCommand(issueTracker);
|
|
186
2619
|
await command.execute({
|
|
187
2620
|
issueNumber,
|
|
188
2621
|
options: {
|
|
@@ -197,10 +2630,12 @@ program.command("enhance").description("Apply enhancement agent to existing GitH
|
|
|
197
2630
|
process.exit(1);
|
|
198
2631
|
}
|
|
199
2632
|
});
|
|
200
|
-
program.command("finish").alias("dn").description("Merge work and cleanup workspace").argument("[identifier]", "Issue number, PR number, or branch name (auto-detected if omitted)").option("-f, --force", "Skip confirmation prompts").option("-n, --dry-run", "Preview actions without executing").option("--pr <number>", "Treat input as PR number", parseFloat).option("--skip-build", "Skip post-merge build verification").action(async (identifier, options) => {
|
|
2633
|
+
program.command("finish").alias("dn").description("Merge work and cleanup workspace").argument("[identifier]", "Issue number, PR number, or branch name (auto-detected if omitted)").option("-f, --force", "Skip confirmation prompts").option("-n, --dry-run", "Preview actions without executing").option("--pr <number>", "Treat input as PR number", parseFloat).option("--skip-build", "Skip post-merge build verification").option("--no-browser", "Skip opening PR in browser (github-pr mode only)").option("--cleanup", "Clean up worktree after PR creation (github-pr mode only)").option("--no-cleanup", "Keep worktree after PR creation (github-pr mode only)").action(async (identifier, options) => {
|
|
201
2634
|
try {
|
|
202
|
-
const
|
|
203
|
-
const
|
|
2635
|
+
const settingsManager = new SettingsManager();
|
|
2636
|
+
const settings = await settingsManager.loadSettings();
|
|
2637
|
+
const issueTracker = IssueTrackerFactory.create(settings);
|
|
2638
|
+
const command = new FinishCommand(issueTracker);
|
|
204
2639
|
await command.execute({ identifier, options });
|
|
205
2640
|
} catch (error) {
|
|
206
2641
|
logger.error(`Failed to finish workspace: ${error instanceof Error ? error.message : "Unknown error"}`);
|
|
@@ -209,7 +2644,7 @@ program.command("finish").alias("dn").description("Merge work and cleanup worksp
|
|
|
209
2644
|
});
|
|
210
2645
|
program.command("rebase").description("Rebase current branch on main with Claude-assisted conflict resolution").option("-f, --force", "Skip confirmation prompts").option("-n, --dry-run", "Preview actions without executing").action(async (options) => {
|
|
211
2646
|
try {
|
|
212
|
-
const { RebaseCommand } = await import("./rebase-
|
|
2647
|
+
const { RebaseCommand } = await import("./rebase-Q7GMM7EI.js");
|
|
213
2648
|
const command = new RebaseCommand();
|
|
214
2649
|
await command.execute(options);
|
|
215
2650
|
} catch (error) {
|
|
@@ -221,7 +2656,7 @@ program.command("spin").alias("ignite").description("Launch Claude with auto-det
|
|
|
221
2656
|
new Option("--one-shot <mode>", "One-shot automation mode").choices(["default", "noReview", "bypassPermissions"]).default("default")
|
|
222
2657
|
).action(async (options) => {
|
|
223
2658
|
try {
|
|
224
|
-
const { IgniteCommand } = await import("./ignite-
|
|
2659
|
+
const { IgniteCommand } = await import("./ignite-VM64QO3J.js");
|
|
225
2660
|
const command = new IgniteCommand();
|
|
226
2661
|
await command.execute(options.oneShot ?? "default");
|
|
227
2662
|
} catch (error) {
|
|
@@ -232,7 +2667,7 @@ program.command("spin").alias("ignite").description("Launch Claude with auto-det
|
|
|
232
2667
|
program.command("open").description("Open workspace in browser or run CLI tool").argument("[identifier]", "Issue number, PR number, or branch name (auto-detected if omitted)").allowUnknownOption().action(async (identifier, _options, command) => {
|
|
233
2668
|
try {
|
|
234
2669
|
const args = (command == null ? void 0 : command.args) ? command.args.slice(identifier ? 1 : 0) : [];
|
|
235
|
-
const { OpenCommand } = await import("./open-
|
|
2670
|
+
const { OpenCommand } = await import("./open-KXDXEKRZ.js");
|
|
236
2671
|
const cmd = new OpenCommand();
|
|
237
2672
|
const input = identifier ? { identifier, args } : { args };
|
|
238
2673
|
await cmd.execute(input);
|
|
@@ -244,7 +2679,7 @@ program.command("open").description("Open workspace in browser or run CLI tool")
|
|
|
244
2679
|
program.command("run").description("Run CLI tool or open workspace in browser").argument("[identifier]", "Issue number, PR number, or branch name (auto-detected if omitted)").allowUnknownOption().action(async (identifier, _options, command) => {
|
|
245
2680
|
try {
|
|
246
2681
|
const args = (command == null ? void 0 : command.args) ? command.args.slice(identifier ? 1 : 0) : [];
|
|
247
|
-
const { RunCommand } = await import("./run-
|
|
2682
|
+
const { RunCommand } = await import("./run-PAWJJCSX.js");
|
|
248
2683
|
const cmd = new RunCommand();
|
|
249
2684
|
const input = identifier ? { identifier, args } : { args };
|
|
250
2685
|
await cmd.execute(input);
|
|
@@ -255,7 +2690,7 @@ program.command("run").description("Run CLI tool or open workspace in browser").
|
|
|
255
2690
|
});
|
|
256
2691
|
program.command("cleanup").description("Remove workspaces").argument("[identifier]", "Branch name or issue number to cleanup (auto-detected)").option("-l, --list", "List all worktrees").option("-a, --all", "Remove all worktrees (interactive confirmation)").option("-i, --issue <number>", "Cleanup by issue number", parseInt).option("-f, --force", "Skip confirmations and force removal").option("--dry-run", "Show what would be done without doing it").action(async (identifier, options) => {
|
|
257
2692
|
try {
|
|
258
|
-
const { CleanupCommand } = await import("./cleanup-
|
|
2693
|
+
const { CleanupCommand } = await import("./cleanup-7RWLBSLE.js");
|
|
259
2694
|
const command = new CleanupCommand();
|
|
260
2695
|
const input = {
|
|
261
2696
|
options: options ?? {}
|
|
@@ -289,15 +2724,25 @@ program.command("list").description("Show active workspaces").option("--json", "
|
|
|
289
2724
|
logger.info(` Commit: ${formatted.commit}`);
|
|
290
2725
|
}
|
|
291
2726
|
} catch (error) {
|
|
2727
|
+
if (error instanceof Error && error.message.includes("not a git repository")) {
|
|
2728
|
+
if (options.json) {
|
|
2729
|
+
console.log("[]");
|
|
2730
|
+
} else {
|
|
2731
|
+
logger.info("No worktrees found");
|
|
2732
|
+
}
|
|
2733
|
+
return;
|
|
2734
|
+
}
|
|
292
2735
|
logger.error(`Failed to list worktrees: ${error instanceof Error ? error.message : "Unknown error"}`);
|
|
293
2736
|
process.exit(1);
|
|
294
2737
|
}
|
|
295
2738
|
});
|
|
296
|
-
program.command("init").alias("config").description("Initialize iloom configuration and setup shell autocomplete").action(async () => {
|
|
2739
|
+
program.command("init").alias("config").description("Initialize iloom configuration and setup shell autocomplete").argument("[prompt]", 'Custom initial message to send to Claude (defaults to "Help me configure iloom settings.")').action(async (prompt) => {
|
|
297
2740
|
try {
|
|
298
|
-
const { InitCommand } = await import("./init-
|
|
2741
|
+
const { InitCommand } = await import("./init-G3T64SC4.js");
|
|
299
2742
|
const command = new InitCommand();
|
|
300
|
-
|
|
2743
|
+
const trimmedPrompt = prompt == null ? void 0 : prompt.trim();
|
|
2744
|
+
const customPrompt = trimmedPrompt && trimmedPrompt.length > 0 ? trimmedPrompt : void 0;
|
|
2745
|
+
await command.execute(customPrompt);
|
|
301
2746
|
} catch (error) {
|
|
302
2747
|
logger.error(`Failed to initialize: ${error instanceof Error ? error.message : "Unknown error"}`);
|
|
303
2748
|
process.exit(1);
|
|
@@ -305,7 +2750,7 @@ program.command("init").alias("config").description("Initialize iloom configurat
|
|
|
305
2750
|
});
|
|
306
2751
|
program.command("contribute").description("Set up local development environment for contributing to iloom").action(async () => {
|
|
307
2752
|
try {
|
|
308
|
-
const { ContributeCommand } = await import("./contribute-
|
|
2753
|
+
const { ContributeCommand } = await import("./contribute-BS2L4FZR.js");
|
|
309
2754
|
const command = new ContributeCommand();
|
|
310
2755
|
await command.execute();
|
|
311
2756
|
} catch (error) {
|
|
@@ -325,29 +2770,32 @@ program.command("update").description("Update iloom-cli to the latest version").
|
|
|
325
2770
|
});
|
|
326
2771
|
program.command("test-github").description("Test GitHub integration (Issue #3)").argument("<identifier>", "Issue number or PR number").option("--no-claude", "Skip Claude for branch name generation").action(async (identifier, options) => {
|
|
327
2772
|
try {
|
|
328
|
-
const { GitHubService } = await import("./GitHubService-
|
|
2773
|
+
const { GitHubService } = await import("./GitHubService-EBOETDIW.js");
|
|
2774
|
+
const { DefaultBranchNamingService: DefaultBranchNamingService2 } = await import("./BranchNamingService-3OQPRSWT.js");
|
|
329
2775
|
logger.info("Testing GitHub Integration\n");
|
|
330
|
-
const service = new GitHubService(
|
|
2776
|
+
const service = new GitHubService();
|
|
2777
|
+
const branchNaming = new DefaultBranchNamingService2({ useClaude: options.claude !== false });
|
|
331
2778
|
logger.info("Detecting input type...");
|
|
332
2779
|
const detection = await service.detectInputType(identifier);
|
|
333
2780
|
logger.info(` Type: ${detection.type}`);
|
|
334
|
-
logger.info(`
|
|
2781
|
+
logger.info(` Identifier: ${detection.identifier}`);
|
|
335
2782
|
if (detection.type === "unknown") {
|
|
336
2783
|
logger.error("Could not detect if input is an issue or PR");
|
|
337
2784
|
process.exit(1);
|
|
338
2785
|
}
|
|
339
2786
|
logger.info("Fetching from GitHub...");
|
|
340
2787
|
if (detection.type === "issue") {
|
|
341
|
-
if (!detection.
|
|
2788
|
+
if (!detection.identifier) {
|
|
342
2789
|
throw new Error("Issue number not detected");
|
|
343
2790
|
}
|
|
344
|
-
const
|
|
2791
|
+
const issueNumber = parseInt(detection.identifier, 10);
|
|
2792
|
+
const issue = await service.fetchIssue(issueNumber);
|
|
345
2793
|
logger.success(` Issue #${issue.number}: ${issue.title}`);
|
|
346
2794
|
logger.info(` State: ${issue.state}`);
|
|
347
2795
|
logger.info(` Labels: ${issue.labels.join(", ") || "none"}`);
|
|
348
2796
|
logger.info(` URL: ${issue.url}`);
|
|
349
2797
|
logger.info("Generating branch name...");
|
|
350
|
-
const branchName = await
|
|
2798
|
+
const branchName = await branchNaming.generateBranchName({
|
|
351
2799
|
issueNumber: issue.number,
|
|
352
2800
|
title: issue.title
|
|
353
2801
|
});
|
|
@@ -356,10 +2804,11 @@ program.command("test-github").description("Test GitHub integration (Issue #3)")
|
|
|
356
2804
|
const context = service.extractContext(issue);
|
|
357
2805
|
logger.info(` ${context.split("\n").join("\n ")}`);
|
|
358
2806
|
} else {
|
|
359
|
-
if (!detection.
|
|
2807
|
+
if (!detection.identifier) {
|
|
360
2808
|
throw new Error("PR number not detected");
|
|
361
2809
|
}
|
|
362
|
-
const
|
|
2810
|
+
const prNumber = parseInt(detection.identifier, 10);
|
|
2811
|
+
const pr = await service.fetchPR(prNumber);
|
|
363
2812
|
logger.success(` PR #${pr.number}: ${pr.title}`);
|
|
364
2813
|
logger.info(` State: ${pr.state}`);
|
|
365
2814
|
logger.info(` Branch: ${pr.branch}`);
|
|
@@ -380,14 +2829,14 @@ program.command("test-github").description("Test GitHub integration (Issue #3)")
|
|
|
380
2829
|
});
|
|
381
2830
|
program.command("test-claude").description("Test Claude integration (Issue #10)").option("--detect", "Test Claude CLI detection").option("--version", "Get Claude CLI version").option("--branch <title>", "Test branch name generation with given title").option("--issue <number>", "Issue number for branch generation", "123").option("--launch <prompt>", "Launch Claude with a prompt (headless)").option("--interactive", "Launch Claude interactively (requires --launch)").option("--template <name>", "Test template loading").action(async (options) => {
|
|
382
2831
|
try {
|
|
383
|
-
const { detectClaudeCli, getClaudeVersion, generateBranchName, launchClaude } = await import("./claude-
|
|
384
|
-
const { PromptTemplateManager } = await import("./PromptTemplateManager-
|
|
385
|
-
const { ClaudeService } = await import("./ClaudeService-
|
|
386
|
-
const { ClaudeContextManager } = await import("./ClaudeContextManager-
|
|
2832
|
+
const { detectClaudeCli: detectClaudeCli2, getClaudeVersion, generateBranchName, launchClaude: launchClaude2 } = await import("./claude-GOP6PFC7.js");
|
|
2833
|
+
const { PromptTemplateManager } = await import("./PromptTemplateManager-A52RUAMS.js");
|
|
2834
|
+
const { ClaudeService } = await import("./ClaudeService-HG4VQ7AW.js");
|
|
2835
|
+
const { ClaudeContextManager: ClaudeContextManager2 } = await import("./ClaudeContextManager-MUQSDY2E.js");
|
|
387
2836
|
logger.info("Testing Claude Integration\n");
|
|
388
2837
|
if (options.detect) {
|
|
389
2838
|
logger.info("Detecting Claude CLI...");
|
|
390
|
-
const isAvailable = await
|
|
2839
|
+
const isAvailable = await detectClaudeCli2();
|
|
391
2840
|
if (isAvailable) {
|
|
392
2841
|
logger.success(" Claude CLI is available");
|
|
393
2842
|
} else {
|
|
@@ -416,11 +2865,11 @@ program.command("test-claude").description("Test Claude integration (Issue #10)"
|
|
|
416
2865
|
logger.info(` Mode: ${options.interactive ? "Interactive" : "Headless"}`);
|
|
417
2866
|
if (options.interactive) {
|
|
418
2867
|
logger.info(" Launching Claude in new terminal...");
|
|
419
|
-
await
|
|
2868
|
+
await launchClaude2(options.launch, { headless: false });
|
|
420
2869
|
logger.info(" (Claude should open in a separate process)");
|
|
421
2870
|
} else {
|
|
422
2871
|
logger.info(" Waiting for response...");
|
|
423
|
-
const result = await
|
|
2872
|
+
const result = await launchClaude2(options.launch, { headless: true });
|
|
424
2873
|
if (result) {
|
|
425
2874
|
logger.success(" Response:");
|
|
426
2875
|
logger.info(` ${result.split("\n").join("\n ")}`);
|
|
@@ -443,7 +2892,7 @@ program.command("test-claude").description("Test Claude integration (Issue #10)"
|
|
|
443
2892
|
if (!options.detect && !options.version && !options.branch && !options.launch && !options.template) {
|
|
444
2893
|
logger.info("Running full Claude integration test suite...\n");
|
|
445
2894
|
logger.info("1. Testing Claude CLI detection...");
|
|
446
|
-
const isAvailable = await
|
|
2895
|
+
const isAvailable = await detectClaudeCli2();
|
|
447
2896
|
if (isAvailable) {
|
|
448
2897
|
logger.success(" Claude CLI is available");
|
|
449
2898
|
} else {
|
|
@@ -468,7 +2917,7 @@ program.command("test-claude").description("Test Claude integration (Issue #10)"
|
|
|
468
2917
|
new ClaudeService();
|
|
469
2918
|
logger.success(" Service initialized");
|
|
470
2919
|
logger.info("\n5. Testing ClaudeContextManager...");
|
|
471
|
-
const contextManager = new
|
|
2920
|
+
const contextManager = new ClaudeContextManager2();
|
|
472
2921
|
await contextManager.prepareContext({
|
|
473
2922
|
type: "issue",
|
|
474
2923
|
identifier: 123,
|
|
@@ -494,7 +2943,7 @@ program.command("test-claude").description("Test Claude integration (Issue #10)"
|
|
|
494
2943
|
logger.info("\n7. Testing Claude launch (headless)...");
|
|
495
2944
|
logger.info(' Sending test prompt: "Say hello"');
|
|
496
2945
|
try {
|
|
497
|
-
const result = await
|
|
2946
|
+
const result = await launchClaude2("Say hello", { headless: true });
|
|
498
2947
|
if (result) {
|
|
499
2948
|
logger.success(" Claude responded successfully");
|
|
500
2949
|
logger.info(` Response preview: ${result.substring(0, 100)}...`);
|
|
@@ -518,7 +2967,7 @@ program.command("test-claude").description("Test Claude integration (Issue #10)"
|
|
|
518
2967
|
});
|
|
519
2968
|
program.command("test-webserver").description("Test if a web server is running on a workspace port").argument("<issue-number>", "Issue number (port will be calculated as 3000 + issue number)", parseInt).option("--kill", "Kill the web server if detected").action(async (issueNumber, options) => {
|
|
520
2969
|
try {
|
|
521
|
-
const { TestWebserverCommand } = await import("./test-webserver-
|
|
2970
|
+
const { TestWebserverCommand } = await import("./test-webserver-DAHONWCS.js");
|
|
522
2971
|
const command = new TestWebserverCommand();
|
|
523
2972
|
await command.execute({ issueNumber, options });
|
|
524
2973
|
} catch (error) {
|
|
@@ -531,7 +2980,7 @@ program.command("test-webserver").description("Test if a web server is running o
|
|
|
531
2980
|
});
|
|
532
2981
|
program.command("test-git").description("Test Git integration - findMainWorktreePath() function (reads .iloom/settings.json)").action(async () => {
|
|
533
2982
|
try {
|
|
534
|
-
const { TestGitCommand } = await import("./test-git-
|
|
2983
|
+
const { TestGitCommand } = await import("./test-git-3WDLNQCA.js");
|
|
535
2984
|
const command = new TestGitCommand();
|
|
536
2985
|
await command.execute();
|
|
537
2986
|
} catch (error) {
|
|
@@ -544,7 +2993,7 @@ program.command("test-git").description("Test Git integration - findMainWorktree
|
|
|
544
2993
|
});
|
|
545
2994
|
program.command("test-tabs").description("Test iTerm2 dual tab functionality - opens two tabs with test commands").action(async () => {
|
|
546
2995
|
try {
|
|
547
|
-
const { TestTabsCommand } = await import("./test-tabs-
|
|
2996
|
+
const { TestTabsCommand } = await import("./test-tabs-RXDBZ6J7.js");
|
|
548
2997
|
const command = new TestTabsCommand();
|
|
549
2998
|
await command.execute();
|
|
550
2999
|
} catch (error) {
|
|
@@ -557,7 +3006,7 @@ program.command("test-tabs").description("Test iTerm2 dual tab functionality - o
|
|
|
557
3006
|
});
|
|
558
3007
|
program.command("test-prefix").description("Test worktree prefix configuration - preview worktree paths (reads .iloom/settings.json)").action(async () => {
|
|
559
3008
|
try {
|
|
560
|
-
const { TestPrefixCommand } = await import("./test-prefix-
|
|
3009
|
+
const { TestPrefixCommand } = await import("./test-prefix-EVGAWAJW.js");
|
|
561
3010
|
const command = new TestPrefixCommand();
|
|
562
3011
|
await command.execute();
|
|
563
3012
|
} catch (error) {
|
|
@@ -571,18 +3020,18 @@ program.command("test-prefix").description("Test worktree prefix configuration -
|
|
|
571
3020
|
program.command("test-neon").description("Test Neon integration and debug configuration").action(async () => {
|
|
572
3021
|
var _a;
|
|
573
3022
|
try {
|
|
574
|
-
const { SettingsManager } = await import("./SettingsManager-
|
|
575
|
-
const { createNeonProviderFromSettings } = await import("./neon-helpers-
|
|
3023
|
+
const { SettingsManager: SettingsManager2 } = await import("./SettingsManager-WHHFGSL7.js");
|
|
3024
|
+
const { createNeonProviderFromSettings: createNeonProviderFromSettings2 } = await import("./neon-helpers-WPUACUVC.js");
|
|
576
3025
|
logger.info("Testing Neon Integration\n");
|
|
577
3026
|
logger.info("1. Settings Configuration:");
|
|
578
|
-
const settingsManager = new
|
|
3027
|
+
const settingsManager = new SettingsManager2();
|
|
579
3028
|
const settings = await settingsManager.loadSettings();
|
|
580
3029
|
const neonConfig = (_a = settings.databaseProviders) == null ? void 0 : _a.neon;
|
|
581
3030
|
logger.info(` projectId: ${(neonConfig == null ? void 0 : neonConfig.projectId) ?? "(not configured)"}`);
|
|
582
3031
|
logger.info(` parentBranch: ${(neonConfig == null ? void 0 : neonConfig.parentBranch) ?? "(not configured)"}`);
|
|
583
3032
|
logger.info("\n2. Creating NeonProvider...");
|
|
584
3033
|
try {
|
|
585
|
-
const neonProvider =
|
|
3034
|
+
const neonProvider = createNeonProviderFromSettings2(settings);
|
|
586
3035
|
logger.success(" NeonProvider created successfully");
|
|
587
3036
|
logger.info("\n3. Testing Neon CLI availability...");
|
|
588
3037
|
const isAvailable = await neonProvider.isCliAvailable();
|