@iloom/cli 0.2.0 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (169) hide show
  1. package/README.md +274 -30
  2. package/dist/BranchNamingService-3OQPRSWT.js +13 -0
  3. package/dist/ClaudeContextManager-MUQSDY2E.js +13 -0
  4. package/dist/ClaudeService-HG4VQ7AW.js +12 -0
  5. package/dist/GitHubService-EBOETDIW.js +11 -0
  6. package/dist/{LoomLauncher-CTSWJL35.js → LoomLauncher-FLEMBCSQ.js} +63 -32
  7. package/dist/LoomLauncher-FLEMBCSQ.js.map +1 -0
  8. package/dist/ProjectCapabilityDetector-34LU7JJ4.js +9 -0
  9. package/dist/{PromptTemplateManager-WII75TKH.js → PromptTemplateManager-A52RUAMS.js} +2 -2
  10. package/dist/README.md +274 -30
  11. package/dist/{SettingsManager-XOYCLH3D.js → SettingsManager-WHHFGSL7.js} +12 -4
  12. package/dist/SettingsMigrationManager-AGIIIPDQ.js +10 -0
  13. package/dist/agents/iloom-issue-analyze-and-plan.md +125 -35
  14. package/dist/agents/iloom-issue-analyzer.md +284 -32
  15. package/dist/agents/iloom-issue-complexity-evaluator.md +40 -21
  16. package/dist/agents/iloom-issue-enhancer.md +69 -48
  17. package/dist/agents/iloom-issue-implementer.md +36 -25
  18. package/dist/agents/iloom-issue-planner.md +35 -24
  19. package/dist/agents/iloom-issue-reviewer.md +62 -9
  20. package/dist/chunk-3KATJIKO.js +55 -0
  21. package/dist/chunk-3KATJIKO.js.map +1 -0
  22. package/dist/{chunk-SWCRXDZC.js → chunk-3RUPPQRG.js} +1 -18
  23. package/dist/chunk-3RUPPQRG.js.map +1 -0
  24. package/dist/{chunk-RF2YI2XJ.js → chunk-47KSHUCR.js} +3 -3
  25. package/dist/chunk-47KSHUCR.js.map +1 -0
  26. package/dist/{chunk-VETG35MF.js → chunk-4HHRTA7Q.js} +3 -3
  27. package/dist/{chunk-VETG35MF.js.map → chunk-4HHRTA7Q.js.map} +1 -1
  28. package/dist/chunk-5EF7Z346.js +1987 -0
  29. package/dist/chunk-5EF7Z346.js.map +1 -0
  30. package/dist/{chunk-4IV6W4U5.js → chunk-AWOFAD5O.js} +12 -12
  31. package/dist/chunk-AWOFAD5O.js.map +1 -0
  32. package/dist/{chunk-2PLUQT6J.js → chunk-C5QCTEQK.js} +2 -2
  33. package/dist/{chunk-CWR2SANQ.js → chunk-EBISESAP.js} +1 -1
  34. package/dist/{chunk-LHP6ROUM.js → chunk-FIAT22G7.js} +4 -16
  35. package/dist/chunk-FIAT22G7.js.map +1 -0
  36. package/dist/{chunk-TS6DL67T.js → chunk-G2IEYOLQ.js} +11 -38
  37. package/dist/chunk-G2IEYOLQ.js.map +1 -0
  38. package/dist/{chunk-ZMNQBJUI.js → chunk-IP7SMKIF.js} +61 -22
  39. package/dist/chunk-IP7SMKIF.js.map +1 -0
  40. package/dist/{chunk-JNKJ7NJV.js → chunk-JKXJ7BGL.js} +6 -2
  41. package/dist/{chunk-JNKJ7NJV.js.map → chunk-JKXJ7BGL.js.map} +1 -1
  42. package/dist/{chunk-LAPY6NAE.js → chunk-JQFO7QQN.js} +68 -12
  43. package/dist/{chunk-LAPY6NAE.js.map → chunk-JQFO7QQN.js.map} +1 -1
  44. package/dist/{SettingsMigrationManager-MTQIMI54.js → chunk-KLBYVHPK.js} +3 -2
  45. package/dist/{chunk-HBVFXN7R.js → chunk-MAVL6PJF.js} +26 -3
  46. package/dist/chunk-MAVL6PJF.js.map +1 -0
  47. package/dist/{chunk-USVVV3FP.js → chunk-MKWYLDFK.js} +5 -5
  48. package/dist/chunk-ML3NRPNB.js +396 -0
  49. package/dist/chunk-ML3NRPNB.js.map +1 -0
  50. package/dist/{chunk-DJUGYNQE.js → chunk-PA6Q6AWM.js} +16 -3
  51. package/dist/chunk-PA6Q6AWM.js.map +1 -0
  52. package/dist/chunk-RO26VS3W.js +444 -0
  53. package/dist/chunk-RO26VS3W.js.map +1 -0
  54. package/dist/{chunk-6LEQW46Y.js → chunk-VAYCCUXW.js} +72 -2
  55. package/dist/{chunk-6LEQW46Y.js.map → chunk-VAYCCUXW.js.map} +1 -1
  56. package/dist/{chunk-SPYPLHMK.js → chunk-VU3QMIP2.js} +34 -2
  57. package/dist/chunk-VU3QMIP2.js.map +1 -0
  58. package/dist/{chunk-PVAVNJKS.js → chunk-WEN5C5DM.js} +10 -1
  59. package/dist/chunk-WEN5C5DM.js.map +1 -0
  60. package/dist/{chunk-MFU53H6J.js → chunk-XXV3UFZL.js} +3 -3
  61. package/dist/{chunk-MFU53H6J.js.map → chunk-XXV3UFZL.js.map} +1 -1
  62. package/dist/{chunk-GZP4UGGM.js → chunk-ZM3CFL5L.js} +2 -2
  63. package/dist/{chunk-BLCTGFZN.js → chunk-ZT3YZB4K.js} +3 -4
  64. package/dist/chunk-ZT3YZB4K.js.map +1 -0
  65. package/dist/{claude-ZIWDG4XG.js → claude-GOP6PFC7.js} +2 -2
  66. package/dist/{cleanup-FEIVZSIV.js → cleanup-7RWLBSLE.js} +86 -25
  67. package/dist/cleanup-7RWLBSLE.js.map +1 -0
  68. package/dist/cli.js +2511 -62
  69. package/dist/cli.js.map +1 -1
  70. package/dist/{contribute-EMZKCAC6.js → contribute-BS2L4FZR.js} +6 -6
  71. package/dist/{feedback-LFNMQBAZ.js → feedback-N4ECWIPF.js} +15 -14
  72. package/dist/{feedback-LFNMQBAZ.js.map → feedback-N4ECWIPF.js.map} +1 -1
  73. package/dist/{git-WC6HZLOT.js → git-TDXKRTXM.js} +4 -2
  74. package/dist/{ignite-MQWVJEAB.js → ignite-VM64QO3J.js} +32 -27
  75. package/dist/ignite-VM64QO3J.js.map +1 -0
  76. package/dist/index.d.ts +359 -45
  77. package/dist/index.js +1266 -502
  78. package/dist/index.js.map +1 -1
  79. package/dist/{init-GJDYN2IK.js → init-G3T64SC4.js} +104 -40
  80. package/dist/init-G3T64SC4.js.map +1 -0
  81. package/dist/mcp/issue-management-server.js +934 -0
  82. package/dist/mcp/issue-management-server.js.map +1 -0
  83. package/dist/{neon-helpers-ZVIRPKCI.js → neon-helpers-WPUACUVC.js} +3 -3
  84. package/dist/neon-helpers-WPUACUVC.js.map +1 -0
  85. package/dist/{open-NXSN7XOC.js → open-KXDXEKRZ.js} +39 -36
  86. package/dist/open-KXDXEKRZ.js.map +1 -0
  87. package/dist/{prompt-ANTQWHUF.js → prompt-7INJ7YRU.js} +4 -2
  88. package/dist/prompt-7INJ7YRU.js.map +1 -0
  89. package/dist/prompts/init-prompt.txt +538 -95
  90. package/dist/prompts/issue-prompt.txt +27 -27
  91. package/dist/{rebase-DUNFOJVS.js → rebase-Q7GMM7EI.js} +6 -6
  92. package/dist/{remote-ZCXJVVNW.js → remote-VUNCQZ6J.js} +3 -2
  93. package/dist/remote-VUNCQZ6J.js.map +1 -0
  94. package/dist/{run-O7ZK7CKA.js → run-PAWJJCSX.js} +39 -36
  95. package/dist/run-PAWJJCSX.js.map +1 -0
  96. package/dist/schema/settings.schema.json +56 -0
  97. package/dist/{test-git-T76HOTIA.js → test-git-3WDLNQCA.js} +3 -3
  98. package/dist/{test-prefix-6HJUVQMH.js → test-prefix-EVGAWAJW.js} +3 -3
  99. package/dist/{test-webserver-M2I3EV4J.js → test-webserver-DAHONWCS.js} +4 -4
  100. package/dist/test-webserver-DAHONWCS.js.map +1 -0
  101. package/package.json +2 -1
  102. package/dist/ClaudeContextManager-LVCYRM6Q.js +0 -13
  103. package/dist/ClaudeService-WVTWB3DK.js +0 -12
  104. package/dist/GitHubService-7E2S5NNZ.js +0 -11
  105. package/dist/LoomLauncher-CTSWJL35.js.map +0 -1
  106. package/dist/add-issue-OBI325W7.js +0 -69
  107. package/dist/add-issue-OBI325W7.js.map +0 -1
  108. package/dist/chunk-4IV6W4U5.js.map +0 -1
  109. package/dist/chunk-BLCTGFZN.js.map +0 -1
  110. package/dist/chunk-CVLAZRNB.js +0 -54
  111. package/dist/chunk-CVLAZRNB.js.map +0 -1
  112. package/dist/chunk-DJUGYNQE.js.map +0 -1
  113. package/dist/chunk-H4E4THUZ.js +0 -55
  114. package/dist/chunk-H4E4THUZ.js.map +0 -1
  115. package/dist/chunk-H5LDRGVK.js +0 -642
  116. package/dist/chunk-H5LDRGVK.js.map +0 -1
  117. package/dist/chunk-HBVFXN7R.js.map +0 -1
  118. package/dist/chunk-LHP6ROUM.js.map +0 -1
  119. package/dist/chunk-PVAVNJKS.js.map +0 -1
  120. package/dist/chunk-RF2YI2XJ.js.map +0 -1
  121. package/dist/chunk-SPYPLHMK.js.map +0 -1
  122. package/dist/chunk-SWCRXDZC.js.map +0 -1
  123. package/dist/chunk-SYOSCMIT.js +0 -545
  124. package/dist/chunk-SYOSCMIT.js.map +0 -1
  125. package/dist/chunk-T3KEIB4D.js +0 -243
  126. package/dist/chunk-T3KEIB4D.js.map +0 -1
  127. package/dist/chunk-TS6DL67T.js.map +0 -1
  128. package/dist/chunk-ZMNQBJUI.js.map +0 -1
  129. package/dist/cleanup-FEIVZSIV.js.map +0 -1
  130. package/dist/enhance-MNA4ZGXW.js +0 -176
  131. package/dist/enhance-MNA4ZGXW.js.map +0 -1
  132. package/dist/finish-TX5CJICB.js +0 -1749
  133. package/dist/finish-TX5CJICB.js.map +0 -1
  134. package/dist/ignite-MQWVJEAB.js.map +0 -1
  135. package/dist/init-GJDYN2IK.js.map +0 -1
  136. package/dist/mcp/chunk-6SDFJ42P.js +0 -62
  137. package/dist/mcp/chunk-6SDFJ42P.js.map +0 -1
  138. package/dist/mcp/claude-NDFOCQQQ.js +0 -249
  139. package/dist/mcp/claude-NDFOCQQQ.js.map +0 -1
  140. package/dist/mcp/color-QS5BFCNN.js +0 -168
  141. package/dist/mcp/color-QS5BFCNN.js.map +0 -1
  142. package/dist/mcp/github-comment-server.js +0 -168
  143. package/dist/mcp/github-comment-server.js.map +0 -1
  144. package/dist/mcp/terminal-OMNRFWB3.js +0 -227
  145. package/dist/mcp/terminal-OMNRFWB3.js.map +0 -1
  146. package/dist/open-NXSN7XOC.js.map +0 -1
  147. package/dist/run-O7ZK7CKA.js.map +0 -1
  148. package/dist/start-73I5W7WW.js +0 -983
  149. package/dist/start-73I5W7WW.js.map +0 -1
  150. package/dist/test-webserver-M2I3EV4J.js.map +0 -1
  151. /package/dist/{ClaudeContextManager-LVCYRM6Q.js.map → BranchNamingService-3OQPRSWT.js.map} +0 -0
  152. /package/dist/{ClaudeService-WVTWB3DK.js.map → ClaudeContextManager-MUQSDY2E.js.map} +0 -0
  153. /package/dist/{GitHubService-7E2S5NNZ.js.map → ClaudeService-HG4VQ7AW.js.map} +0 -0
  154. /package/dist/{PromptTemplateManager-WII75TKH.js.map → GitHubService-EBOETDIW.js.map} +0 -0
  155. /package/dist/{SettingsManager-XOYCLH3D.js.map → ProjectCapabilityDetector-34LU7JJ4.js.map} +0 -0
  156. /package/dist/{claude-ZIWDG4XG.js.map → PromptTemplateManager-A52RUAMS.js.map} +0 -0
  157. /package/dist/{git-WC6HZLOT.js.map → SettingsManager-WHHFGSL7.js.map} +0 -0
  158. /package/dist/{neon-helpers-ZVIRPKCI.js.map → SettingsMigrationManager-AGIIIPDQ.js.map} +0 -0
  159. /package/dist/{chunk-2PLUQT6J.js.map → chunk-C5QCTEQK.js.map} +0 -0
  160. /package/dist/{chunk-CWR2SANQ.js.map → chunk-EBISESAP.js.map} +0 -0
  161. /package/dist/{SettingsMigrationManager-MTQIMI54.js.map → chunk-KLBYVHPK.js.map} +0 -0
  162. /package/dist/{chunk-USVVV3FP.js.map → chunk-MKWYLDFK.js.map} +0 -0
  163. /package/dist/{chunk-GZP4UGGM.js.map → chunk-ZM3CFL5L.js.map} +0 -0
  164. /package/dist/{prompt-ANTQWHUF.js.map → claude-GOP6PFC7.js.map} +0 -0
  165. /package/dist/{contribute-EMZKCAC6.js.map → contribute-BS2L4FZR.js.map} +0 -0
  166. /package/dist/{remote-ZCXJVVNW.js.map → git-TDXKRTXM.js.map} +0 -0
  167. /package/dist/{rebase-DUNFOJVS.js.map → rebase-Q7GMM7EI.js.map} +0 -0
  168. /package/dist/{test-git-T76HOTIA.js.map → test-git-3WDLNQCA.js.map} +0 -0
  169. /package/dist/{test-prefix-6HJUVQMH.js.map → test-prefix-EVGAWAJW.js.map} +0 -0
@@ -0,0 +1,1987 @@
1
+ #!/usr/bin/env node
2
+ import {
3
+ formatEnvLine,
4
+ loadEnvIntoProcess,
5
+ parseEnvFile,
6
+ validateEnvVariable
7
+ } from "./chunk-IP7SMKIF.js";
8
+ import {
9
+ calculatePortForBranch
10
+ } from "./chunk-VU3QMIP2.js";
11
+ import {
12
+ installDependencies,
13
+ runScript
14
+ } from "./chunk-ZT3YZB4K.js";
15
+ import {
16
+ hasScript,
17
+ readPackageJson
18
+ } from "./chunk-2ZPFJQ3B.js";
19
+ import {
20
+ SettingsManager
21
+ } from "./chunk-ML3NRPNB.js";
22
+ import {
23
+ branchExists,
24
+ ensureRepositoryHasCommits,
25
+ executeGitCommand,
26
+ extractIssueNumber,
27
+ findMainWorktreePathWithSettings,
28
+ hasUncommittedChanges
29
+ } from "./chunk-MAVL6PJF.js";
30
+ import {
31
+ calculateForegroundColor,
32
+ generateColorFromBranchName,
33
+ hexToRgb,
34
+ lightenColor,
35
+ rgbToHex
36
+ } from "./chunk-ZZZWQGTS.js";
37
+ import {
38
+ createLogger,
39
+ logger
40
+ } from "./chunk-GEHQXLEI.js";
41
+
42
+ // src/lib/LoomManager.ts
43
+ import path2 from "path";
44
+ import fs2 from "fs-extra";
45
+
46
+ // src/lib/VSCodeIntegration.ts
47
+ import fs from "fs-extra";
48
+ import path from "path";
49
+ import { parse, modify, applyEdits } from "jsonc-parser";
50
+ var VSCodeIntegration = class {
51
+ /**
52
+ * Set VSCode title bar color for a workspace
53
+ *
54
+ * @param workspacePath - Path to workspace directory
55
+ * @param hexColor - Hex color string (e.g., "#dcebf8")
56
+ */
57
+ async setTitleBarColor(workspacePath, hexColor) {
58
+ const vscodeDir = path.join(workspacePath, ".vscode");
59
+ const settingsPath = path.join(vscodeDir, "settings.json");
60
+ try {
61
+ await fs.ensureDir(vscodeDir);
62
+ const settings = await this.readSettings(settingsPath);
63
+ const updatedSettings = this.mergeColorSettings(settings, hexColor);
64
+ await this.writeSettings(settingsPath, updatedSettings);
65
+ logger.debug(`Set VSCode title bar color to ${hexColor} for ${workspacePath}`);
66
+ } catch (error) {
67
+ throw new Error(
68
+ `Failed to set VSCode title bar color: ${error instanceof Error ? error.message : "Unknown error"}`
69
+ );
70
+ }
71
+ }
72
+ /**
73
+ * Read VSCode settings from file
74
+ * Supports JSONC (JSON with Comments)
75
+ *
76
+ * @param settingsPath - Path to settings.json file
77
+ * @returns Parsed settings object
78
+ */
79
+ async readSettings(settingsPath) {
80
+ try {
81
+ if (!await fs.pathExists(settingsPath)) {
82
+ return {};
83
+ }
84
+ const content = await fs.readFile(settingsPath, "utf8");
85
+ const errors = [];
86
+ const settings = parse(content, errors, { allowTrailingComma: true });
87
+ if (errors.length > 0) {
88
+ const firstError = errors[0];
89
+ throw new Error(`Invalid JSON: ${firstError ? firstError.error : "Unknown parse error"}`);
90
+ }
91
+ return settings ?? {};
92
+ } catch (error) {
93
+ throw new Error(
94
+ `Failed to parse settings.json: ${error instanceof Error ? error.message : "Unknown error"}`
95
+ );
96
+ }
97
+ }
98
+ /**
99
+ * Write VSCode settings to file atomically
100
+ * Preserves comments if present (using JSONC parser)
101
+ *
102
+ * @param settingsPath - Path to settings.json file
103
+ * @param settings - Settings object to write
104
+ */
105
+ async writeSettings(settingsPath, settings) {
106
+ try {
107
+ let content;
108
+ if (await fs.pathExists(settingsPath)) {
109
+ const existingContent = await fs.readFile(settingsPath, "utf8");
110
+ if (existingContent.includes("//") || existingContent.includes("/*")) {
111
+ content = await this.modifyWithCommentsPreserved(existingContent, settings);
112
+ } else {
113
+ content = JSON.stringify(settings, null, 2) + "\n";
114
+ }
115
+ } else {
116
+ content = JSON.stringify(settings, null, 2) + "\n";
117
+ }
118
+ const tempPath = `${settingsPath}.tmp`;
119
+ await fs.writeFile(tempPath, content, "utf8");
120
+ await fs.rename(tempPath, settingsPath);
121
+ } catch (error) {
122
+ throw new Error(
123
+ `Failed to write settings.json: ${error instanceof Error ? error.message : "Unknown error"}`
124
+ );
125
+ }
126
+ }
127
+ /**
128
+ * Modify JSONC content while preserving comments
129
+ *
130
+ * @param existingContent - Original JSONC content
131
+ * @param newSettings - New settings to apply
132
+ * @returns Modified JSONC content with comments preserved
133
+ */
134
+ async modifyWithCommentsPreserved(existingContent, newSettings) {
135
+ let modifiedContent = existingContent;
136
+ for (const [key, value] of Object.entries(newSettings)) {
137
+ const edits = modify(modifiedContent, [key], value, {});
138
+ modifiedContent = applyEdits(modifiedContent, edits);
139
+ }
140
+ return modifiedContent;
141
+ }
142
+ /**
143
+ * Merge color settings into existing settings object
144
+ *
145
+ * @param existing - Existing settings object
146
+ * @param hexColor - Hex color to apply (subtle palette color)
147
+ * @returns Updated settings object with color merged
148
+ */
149
+ mergeColorSettings(existing, hexColor) {
150
+ const updated = { ...existing };
151
+ updated["workbench.colorCustomizations"] ??= {};
152
+ const colors = updated["workbench.colorCustomizations"];
153
+ const baseRgb = hexToRgb(hexColor);
154
+ const foreground = calculateForegroundColor(baseRgb);
155
+ const foregroundTransparent = foreground.replace("#", "#") + "99";
156
+ const lighterRgb = lightenColor(baseRgb, 0.05);
157
+ const lighterHex = rgbToHex(lighterRgb.r, lighterRgb.g, lighterRgb.b);
158
+ colors["titleBar.activeBackground"] = hexColor;
159
+ colors["titleBar.inactiveBackground"] = hexColor + "99";
160
+ colors["titleBar.activeForeground"] = foreground;
161
+ colors["titleBar.inactiveForeground"] = foregroundTransparent;
162
+ colors["statusBar.background"] = hexColor;
163
+ colors["statusBar.foreground"] = foreground;
164
+ colors["statusBarItem.hoverBackground"] = lighterHex;
165
+ colors["statusBarItem.remoteBackground"] = hexColor;
166
+ colors["statusBarItem.remoteForeground"] = foreground;
167
+ colors["sash.hoverBorder"] = hexColor;
168
+ colors["commandCenter.border"] = foregroundTransparent;
169
+ return updated;
170
+ }
171
+ };
172
+
173
+ // src/lib/LoomManager.ts
174
+ var LoomManager = class {
175
+ constructor(gitWorktree, issueTracker, branchNaming, environment, _claude, capabilityDetector, cliIsolation, settings, database) {
176
+ this.gitWorktree = gitWorktree;
177
+ this.issueTracker = issueTracker;
178
+ this.branchNaming = branchNaming;
179
+ this.environment = environment;
180
+ this.capabilityDetector = capabilityDetector;
181
+ this.cliIsolation = cliIsolation;
182
+ this.settings = settings;
183
+ this.database = database;
184
+ }
185
+ /**
186
+ * Get database branch name for a loom by reading its .env file
187
+ * Returns null if database is not configured or branch cannot be determined
188
+ *
189
+ * @param loomPath - Path to the loom worktree
190
+ */
191
+ async getDatabaseBranchForLoom(loomPath) {
192
+ var _a, _b;
193
+ if (!this.database) {
194
+ return null;
195
+ }
196
+ try {
197
+ const envFilePath = path2.join(loomPath, ".env");
198
+ const settings = await this.settings.loadSettings();
199
+ const databaseUrlVarName = ((_b = (_a = settings.capabilities) == null ? void 0 : _a.database) == null ? void 0 : _b.databaseUrlEnvVarName) ?? "DATABASE_URL";
200
+ const connectionString = await this.environment.getEnvVariable(envFilePath, databaseUrlVarName);
201
+ if (!connectionString) {
202
+ return null;
203
+ }
204
+ return await this.database.getBranchNameFromConnectionString(connectionString, loomPath);
205
+ } catch (error) {
206
+ logger.debug(`Could not get database branch for loom at ${loomPath}: ${error instanceof Error ? error.message : "Unknown error"}`);
207
+ return null;
208
+ }
209
+ }
210
+ /**
211
+ * Create a new loom (isolated workspace)
212
+ * Orchestrates worktree creation, environment setup, and Claude context generation
213
+ * NEW: Checks for existing worktrees and reuses them if found
214
+ */
215
+ async createIloom(input) {
216
+ var _a, _b, _c, _d, _e, _f, _g, _h, _i, _j, _k, _l, _m;
217
+ logger.info("Fetching issue data...");
218
+ const issueData = await this.fetchIssueData(input);
219
+ if (input.type === "issue" || input.type === "pr" || input.type === "branch") {
220
+ logger.info("Checking for existing worktree...");
221
+ const existing = await this.findExistingIloom(input, issueData);
222
+ if (existing) {
223
+ logger.success(`Found existing worktree, reusing: ${existing.path}`);
224
+ return await this.reuseIloom(existing, input, issueData);
225
+ }
226
+ logger.info("No existing worktree found, creating new one...");
227
+ }
228
+ logger.info("Preparing branch name...");
229
+ const branchName = await this.prepareBranchName(input, issueData);
230
+ logger.info("Creating git worktree...");
231
+ const worktreePath = await this.createWorktreeOnly(input, branchName);
232
+ this.loadMainEnvFile();
233
+ const { capabilities, binEntries } = await this.capabilityDetector.detectCapabilities(worktreePath);
234
+ await this.copyEnvironmentFiles(worktreePath);
235
+ await this.copyIloomSettings(worktreePath, (_a = input.parentLoom) == null ? void 0 : _a.branchName);
236
+ const settingsData = await this.settings.loadSettings();
237
+ const basePort = ((_c = (_b = settingsData.capabilities) == null ? void 0 : _b.web) == null ? void 0 : _c.basePort) ?? 3e3;
238
+ let port = basePort;
239
+ if (capabilities.includes("web")) {
240
+ port = await this.setupPortForWeb(worktreePath, input, basePort);
241
+ }
242
+ try {
243
+ await installDependencies(worktreePath, true, true);
244
+ } catch (error) {
245
+ logger.warn(`Failed to install dependencies: ${error instanceof Error ? error.message : "Unknown error"}`, error);
246
+ }
247
+ let databaseBranch = void 0;
248
+ if (this.database && !((_d = input.options) == null ? void 0 : _d.skipDatabase)) {
249
+ try {
250
+ const connectionString = await this.database.createBranchIfConfigured(
251
+ branchName,
252
+ path2.join(worktreePath, ".env"),
253
+ void 0,
254
+ // cwd
255
+ (_e = input.parentLoom) == null ? void 0 : _e.databaseBranch
256
+ // fromBranch - use parent's database branch for child looms
257
+ );
258
+ if (connectionString) {
259
+ await this.environment.setEnvVar(
260
+ path2.join(worktreePath, ".env"),
261
+ this.database.getConfiguredVariableName(),
262
+ connectionString
263
+ );
264
+ logger.success("Database branch configured");
265
+ databaseBranch = branchName;
266
+ }
267
+ } catch (error) {
268
+ logger.error(
269
+ `Failed to setup database branch: ${error instanceof Error ? error.message : "Unknown error"}`
270
+ );
271
+ throw error;
272
+ }
273
+ }
274
+ let cliSymlinks = void 0;
275
+ if (capabilities.includes("cli")) {
276
+ try {
277
+ cliSymlinks = await this.cliIsolation.setupCLIIsolation(
278
+ worktreePath,
279
+ input.identifier,
280
+ binEntries
281
+ );
282
+ } catch (error) {
283
+ logger.warn(
284
+ `Failed to setup CLI isolation: ${error instanceof Error ? error.message : "Unknown error"}`,
285
+ error
286
+ );
287
+ }
288
+ }
289
+ if (!((_f = input.options) == null ? void 0 : _f.skipColorSync)) {
290
+ try {
291
+ await this.applyColorSynchronization(worktreePath, branchName);
292
+ } catch (error) {
293
+ logger.warn(
294
+ `Failed to apply color synchronization: ${error instanceof Error ? error.message : "Unknown error"}`,
295
+ error
296
+ );
297
+ }
298
+ }
299
+ if (input.type === "issue") {
300
+ try {
301
+ logger.info("Moving issue to In Progress...");
302
+ if (this.issueTracker.moveIssueToInProgress) {
303
+ await this.issueTracker.moveIssueToInProgress(input.identifier);
304
+ }
305
+ } catch (error) {
306
+ logger.warn(
307
+ `Failed to move issue to In Progress: ${error instanceof Error ? error.message : "Unknown error"}`,
308
+ error
309
+ );
310
+ }
311
+ }
312
+ const enableClaude = ((_g = input.options) == null ? void 0 : _g.enableClaude) !== false;
313
+ const enableCode = ((_h = input.options) == null ? void 0 : _h.enableCode) !== false;
314
+ const enableDevServer = ((_i = input.options) == null ? void 0 : _i.enableDevServer) !== false;
315
+ const enableTerminal = ((_j = input.options) == null ? void 0 : _j.enableTerminal) ?? false;
316
+ const oneShot = ((_k = input.options) == null ? void 0 : _k.oneShot) ?? "default";
317
+ const setArguments = (_l = input.options) == null ? void 0 : _l.setArguments;
318
+ const executablePath = (_m = input.options) == null ? void 0 : _m.executablePath;
319
+ if (enableClaude || enableCode || enableDevServer || enableTerminal) {
320
+ const { LoomLauncher } = await import("./LoomLauncher-FLEMBCSQ.js");
321
+ const { ClaudeContextManager } = await import("./ClaudeContextManager-MUQSDY2E.js");
322
+ const claudeContext = new ClaudeContextManager(void 0, void 0, this.settings);
323
+ const launcher = new LoomLauncher(claudeContext, this.settings);
324
+ await launcher.launchLoom({
325
+ enableClaude,
326
+ enableCode,
327
+ enableDevServer,
328
+ enableTerminal,
329
+ worktreePath,
330
+ branchName,
331
+ port,
332
+ capabilities,
333
+ workflowType: input.type === "branch" ? "regular" : input.type,
334
+ identifier: input.identifier,
335
+ ...(issueData == null ? void 0 : issueData.title) && { title: issueData.title },
336
+ oneShot,
337
+ ...setArguments && { setArguments },
338
+ ...executablePath && { executablePath },
339
+ sourceEnvOnStart: settingsData.sourceEnvOnStart ?? false
340
+ });
341
+ }
342
+ const loom = {
343
+ id: this.generateLoomId(input),
344
+ path: worktreePath,
345
+ branch: branchName,
346
+ type: input.type,
347
+ identifier: input.identifier,
348
+ port,
349
+ createdAt: /* @__PURE__ */ new Date(),
350
+ lastAccessed: /* @__PURE__ */ new Date(),
351
+ ...databaseBranch !== void 0 && { databaseBranch },
352
+ ...capabilities.length > 0 && { capabilities },
353
+ ...Object.keys(binEntries).length > 0 && { binEntries },
354
+ ...cliSymlinks && cliSymlinks.length > 0 && { cliSymlinks },
355
+ ...issueData !== null && {
356
+ issueData: {
357
+ title: issueData.title,
358
+ body: issueData.body,
359
+ url: issueData.url,
360
+ state: issueData.state
361
+ }
362
+ }
363
+ };
364
+ logger.success(`Created loom: ${loom.id} at ${loom.path}`);
365
+ return loom;
366
+ }
367
+ /**
368
+ * Finish a loom (merge work and cleanup)
369
+ * Not yet implemented - see Issue #7
370
+ */
371
+ async finishIloom(_identifier) {
372
+ throw new Error("Not implemented - see Issue #7");
373
+ }
374
+ /**
375
+ * List all active looms
376
+ */
377
+ async listLooms() {
378
+ const worktrees = await this.gitWorktree.listWorktrees();
379
+ return await this.mapWorktreesToLooms(worktrees);
380
+ }
381
+ /**
382
+ * Find a specific loom by identifier
383
+ */
384
+ async findIloom(identifier) {
385
+ const looms = await this.listLooms();
386
+ return looms.find(
387
+ (h) => h.id === identifier || h.identifier.toString() === identifier || h.branch === identifier
388
+ ) ?? null;
389
+ }
390
+ /**
391
+ * Find child looms for a given parent loom
392
+ * Child looms are worktrees created with the parent loom as their base
393
+ *
394
+ * @param parentBranchName - The parent loom's branch name
395
+ * @returns Array of child loom worktrees
396
+ */
397
+ async findChildLooms(parentBranchName) {
398
+ try {
399
+ const worktrees = await this.gitWorktree.listWorktrees();
400
+ if (!worktrees) {
401
+ return [];
402
+ }
403
+ const sanitizedBranchName = parentBranchName.replace(/\//g, "-").replace(/[^a-zA-Z0-9-_]/g, "-");
404
+ const pattern = `${sanitizedBranchName}-looms/`;
405
+ return worktrees.filter((wt) => wt.path.includes(pattern));
406
+ } catch (error) {
407
+ logger.debug(`Failed to find child looms: ${error instanceof Error ? error.message : "Unknown error"}`);
408
+ return [];
409
+ }
410
+ }
411
+ /**
412
+ * Check for child looms and warn user if any exist
413
+ * This is useful before finishing or cleaning up a parent loom
414
+ *
415
+ * @param branchName - Optional branch name to check. If not provided, uses current branch.
416
+ * @returns true if child looms were found, false otherwise
417
+ */
418
+ async checkAndWarnChildLooms(branchName) {
419
+ let targetBranch = branchName;
420
+ if (!targetBranch) {
421
+ const { getCurrentBranch } = await import("./git-TDXKRTXM.js");
422
+ targetBranch = await getCurrentBranch();
423
+ }
424
+ if (!targetBranch) {
425
+ return false;
426
+ }
427
+ const childLooms = await this.findChildLooms(targetBranch);
428
+ if (childLooms.length > 0) {
429
+ logger.warn(`Found ${childLooms.length} child loom(s) that should be finished first:`);
430
+ for (const child of childLooms) {
431
+ logger.warn(` - ${child.path}`);
432
+ }
433
+ logger.warn("");
434
+ logger.warn("To finish child looms:");
435
+ for (const child of childLooms) {
436
+ const prMatch = child.branch.match(/_pr_(\d+)/);
437
+ const issueId = extractIssueNumber(child.branch);
438
+ const childIdentifier = prMatch ? prMatch[1] : issueId ?? child.branch;
439
+ logger.warn(` il finish ${childIdentifier}`);
440
+ }
441
+ logger.warn("");
442
+ return true;
443
+ }
444
+ return false;
445
+ }
446
+ /**
447
+ * Fetch issue/PR data based on input type
448
+ */
449
+ async fetchIssueData(input) {
450
+ if (input.type === "issue") {
451
+ return await this.issueTracker.fetchIssue(input.identifier);
452
+ } else if (input.type === "pr") {
453
+ if (!this.issueTracker.supportsPullRequests || !this.issueTracker.fetchPR) {
454
+ throw new Error("Issue tracker does not support pull requests");
455
+ }
456
+ return await this.issueTracker.fetchPR(input.identifier);
457
+ }
458
+ return null;
459
+ }
460
+ /**
461
+ * Prepare branch name based on input type and issue/PR data
462
+ */
463
+ async prepareBranchName(input, issueData) {
464
+ if (input.type === "branch") {
465
+ return input.identifier;
466
+ }
467
+ if (input.type === "pr" && issueData && "branch" in issueData) {
468
+ return issueData.branch;
469
+ }
470
+ if (input.type === "issue" && issueData) {
471
+ const branchName = await this.branchNaming.generateBranchName({
472
+ issueNumber: input.identifier,
473
+ title: issueData.title
474
+ });
475
+ return branchName;
476
+ }
477
+ if (input.type === "pr") {
478
+ return `pr-${input.identifier}`;
479
+ }
480
+ throw new Error(`Unable to determine branch name for input type: ${input.type}`);
481
+ }
482
+ /**
483
+ * Create worktree for the loom (without dependency installation)
484
+ */
485
+ async createWorktreeOnly(input, branchName) {
486
+ var _a;
487
+ logger.info("Ensuring repository has initial commit...");
488
+ await ensureRepositoryHasCommits(this.gitWorktree.workingDirectory);
489
+ const settingsData = await this.settings.loadSettings();
490
+ let worktreePrefix = settingsData.worktreePrefix;
491
+ if (input.parentLoom) {
492
+ const sanitizedBranchName = input.parentLoom.branchName.replace(/\//g, "-").replace(/[^a-zA-Z0-9-_]/g, "-");
493
+ worktreePrefix = `${sanitizedBranchName}-looms/`;
494
+ logger.info(`Creating child loom with prefix: ${worktreePrefix}`);
495
+ }
496
+ const pathOptions = input.type === "pr" ? { isPR: true, prNumber: input.identifier } : {};
497
+ if (worktreePrefix !== void 0) {
498
+ pathOptions.prefix = worktreePrefix;
499
+ }
500
+ const worktreePath = this.gitWorktree.generateWorktreePath(
501
+ branchName,
502
+ void 0,
503
+ pathOptions
504
+ );
505
+ if (input.type === "pr") {
506
+ logger.info("Fetching all remote branches...");
507
+ try {
508
+ await executeGitCommand(["fetch", "origin"], { cwd: this.gitWorktree.workingDirectory });
509
+ logger.success("Successfully fetched from remote");
510
+ } catch (error) {
511
+ throw new Error(
512
+ `Failed to fetch from remote: ${error instanceof Error ? error.message : "Unknown error"}. Make sure you have access to the repository.`
513
+ );
514
+ }
515
+ }
516
+ const branchExistedLocally = await branchExists(branchName);
517
+ if (input.type !== "pr" && branchExistedLocally) {
518
+ throw new Error(
519
+ `Cannot create worktree: branch '${branchName}' already exists. Use 'git branch -D ${branchName}' to delete it first if needed.`
520
+ );
521
+ }
522
+ const baseBranch = ((_a = input.parentLoom) == null ? void 0 : _a.branchName) ?? input.baseBranch;
523
+ await this.gitWorktree.createWorktree({
524
+ path: worktreePath,
525
+ branch: branchName,
526
+ createBranch: input.type !== "pr",
527
+ // PRs use existing branches
528
+ ...baseBranch && { baseBranch }
529
+ });
530
+ if (input.type === "pr" && !branchExistedLocally) {
531
+ logger.info("Resetting new PR branch to match remote exactly...");
532
+ try {
533
+ await executeGitCommand(["reset", "--hard", `origin/${branchName}`], { cwd: worktreePath });
534
+ await executeGitCommand(["branch", "--set-upstream-to", `origin/${branchName}`], { cwd: worktreePath });
535
+ logger.success("Successfully reset to match remote");
536
+ } catch (error) {
537
+ logger.warn(`Failed to reset to match remote: ${error instanceof Error ? error.message : "Unknown error"}`);
538
+ }
539
+ }
540
+ return worktreePath;
541
+ }
542
+ /**
543
+ * Copy user application environment files (.env) from main repo to worktree
544
+ * Always called regardless of project capabilities
545
+ */
546
+ async copyEnvironmentFiles(worktreePath) {
547
+ const envFilePath = path2.join(worktreePath, ".env");
548
+ try {
549
+ const mainEnvPath = path2.join(process.cwd(), ".env");
550
+ if (await fs2.pathExists(envFilePath)) {
551
+ logger.warn(".env file already exists in worktree, skipping copy");
552
+ } else {
553
+ await this.environment.copyIfExists(mainEnvPath, envFilePath);
554
+ }
555
+ } catch (error) {
556
+ logger.warn(`Warning: Failed to copy main .env file: ${error instanceof Error ? error.message : "Unknown error"}`);
557
+ }
558
+ }
559
+ /**
560
+ * Copy iloom configuration (settings.local.json) from main repo to worktree
561
+ * Always called regardless of project capabilities
562
+ * @param worktreePath Path to the worktree
563
+ * @param parentBranchName Optional parent branch name for child looms (sets mainBranch)
564
+ */
565
+ async copyIloomSettings(worktreePath, parentBranchName) {
566
+ const mainSettingsLocalPath = path2.join(process.cwd(), ".iloom", "settings.local.json");
567
+ try {
568
+ const worktreeIloomDir = path2.join(worktreePath, ".iloom");
569
+ await fs2.ensureDir(worktreeIloomDir);
570
+ const worktreeSettingsLocalPath = path2.join(worktreeIloomDir, "settings.local.json");
571
+ if (await fs2.pathExists(worktreeSettingsLocalPath)) {
572
+ logger.warn("settings.local.json already exists in worktree, skipping copy");
573
+ } else {
574
+ await this.environment.copyIfExists(mainSettingsLocalPath, worktreeSettingsLocalPath);
575
+ }
576
+ if (parentBranchName) {
577
+ let existingSettings = {};
578
+ try {
579
+ const content = await fs2.readFile(worktreeSettingsLocalPath, "utf8");
580
+ existingSettings = JSON.parse(content);
581
+ } catch {
582
+ }
583
+ const updatedSettings = {
584
+ ...existingSettings,
585
+ mainBranch: parentBranchName
586
+ };
587
+ await fs2.writeFile(worktreeSettingsLocalPath, JSON.stringify(updatedSettings, null, 2));
588
+ logger.info(`Set mainBranch to ${parentBranchName} for child loom`);
589
+ }
590
+ } catch (error) {
591
+ logger.warn(`Warning: Failed to copy settings.local.json: ${error instanceof Error ? error.message : "Unknown error"}`);
592
+ }
593
+ }
594
+ /**
595
+ * Setup PORT environment variable for web projects
596
+ * Only called when project has web capabilities
597
+ */
598
+ async setupPortForWeb(worktreePath, input, basePort) {
599
+ const envFilePath = path2.join(worktreePath, ".env");
600
+ const options = { basePort };
601
+ if (input.type === "issue") {
602
+ options.issueNumber = input.identifier;
603
+ } else if (input.type === "pr") {
604
+ options.prNumber = input.identifier;
605
+ } else if (input.type === "branch") {
606
+ options.branchName = input.identifier;
607
+ }
608
+ const port = this.environment.calculatePort(options);
609
+ await this.environment.setEnvVar(envFilePath, "PORT", String(port));
610
+ return port;
611
+ }
612
+ /**
613
+ * Load environment variables from main .env file into process.env
614
+ * Uses dotenv-flow to handle various .env file patterns
615
+ */
616
+ loadMainEnvFile() {
617
+ const result = loadEnvIntoProcess({ path: process.cwd() });
618
+ if (result.error) {
619
+ logger.warn(`Warning: Could not load .env files: ${result.error.message}`);
620
+ } else {
621
+ logger.info("Loaded environment variables using dotenv-flow");
622
+ if (result.parsed && Object.keys(result.parsed).length > 0) {
623
+ logger.debug(`Loaded ${Object.keys(result.parsed).length} environment variables`);
624
+ }
625
+ }
626
+ }
627
+ /**
628
+ * Generate a unique loom ID
629
+ */
630
+ generateLoomId(input) {
631
+ const prefix = input.type;
632
+ return `${prefix}-${input.identifier}`;
633
+ }
634
+ /**
635
+ * Calculate port for the loom
636
+ * Base port: configurable via settings.capabilities.web.basePort (default 3000) + issue/PR number (or deterministic hash for branches)
637
+ */
638
+ async calculatePort(input) {
639
+ var _a, _b;
640
+ const settingsData = await this.settings.loadSettings();
641
+ const basePort = ((_b = (_a = settingsData.capabilities) == null ? void 0 : _a.web) == null ? void 0 : _b.basePort) ?? 3e3;
642
+ if (input.type === "issue" && typeof input.identifier === "number") {
643
+ return this.environment.calculatePort({ basePort, issueNumber: input.identifier });
644
+ }
645
+ if (input.type === "pr" && typeof input.identifier === "number") {
646
+ return this.environment.calculatePort({ basePort, prNumber: input.identifier });
647
+ }
648
+ if (input.type === "branch" && typeof input.identifier === "string") {
649
+ return this.environment.calculatePort({ basePort, branchName: input.identifier });
650
+ }
651
+ throw new Error(`Unknown input type: ${input.type}`);
652
+ }
653
+ /**
654
+ * Apply color synchronization to both VSCode and terminal
655
+ * Colors are cosmetic - errors are logged but don't block workflow
656
+ */
657
+ async applyColorSynchronization(worktreePath, branchName) {
658
+ const colorData = generateColorFromBranchName(branchName);
659
+ const vscode = new VSCodeIntegration();
660
+ await vscode.setTitleBarColor(worktreePath, colorData.hex);
661
+ logger.info(`Applied VSCode title bar color: ${colorData.hex} for branch: ${branchName}`);
662
+ }
663
+ /**
664
+ * Map worktrees to loom objects
665
+ * This is a simplified conversion - in production we'd store loom metadata
666
+ */
667
+ async mapWorktreesToLooms(worktrees) {
668
+ return await Promise.all(worktrees.map(async (wt) => {
669
+ let type = "branch";
670
+ let identifier = wt.branch;
671
+ if (wt.branch.startsWith("issue-")) {
672
+ type = "issue";
673
+ identifier = parseInt(wt.branch.replace("issue-", ""), 10);
674
+ } else if (wt.branch.startsWith("pr-")) {
675
+ type = "pr";
676
+ identifier = parseInt(wt.branch.replace("pr-", ""), 10);
677
+ }
678
+ return {
679
+ id: `${type}-${identifier}`,
680
+ path: wt.path,
681
+ branch: wt.branch,
682
+ type,
683
+ identifier,
684
+ port: await this.calculatePort({ type, identifier, originalInput: "" }),
685
+ createdAt: /* @__PURE__ */ new Date(),
686
+ lastAccessed: /* @__PURE__ */ new Date()
687
+ };
688
+ }));
689
+ }
690
+ /**
691
+ * NEW: Find existing loom for the given input
692
+ * Checks for worktrees matching the issue/PR identifier
693
+ */
694
+ async findExistingIloom(input, issueData) {
695
+ if (input.type === "issue") {
696
+ return await this.gitWorktree.findWorktreeForIssue(input.identifier);
697
+ } else if (input.type === "pr" && issueData && "branch" in issueData) {
698
+ return await this.gitWorktree.findWorktreeForPR(
699
+ input.identifier,
700
+ issueData.branch
701
+ );
702
+ } else if (input.type === "branch") {
703
+ return await this.gitWorktree.findWorktreeForBranch(input.identifier);
704
+ }
705
+ return null;
706
+ }
707
+ /**
708
+ * NEW: Reuse an existing loom
709
+ * Includes environment setup and database branching for existing worktrees
710
+ * Ports: handle_existing_worktree() from bash script lines 168-215
711
+ */
712
+ async reuseIloom(worktree, input, issueData) {
713
+ var _a, _b, _c, _d, _e, _f, _g, _h, _i;
714
+ const worktreePath = worktree.path;
715
+ const branchName = worktree.branch;
716
+ this.loadMainEnvFile();
717
+ const { capabilities, binEntries } = await this.capabilityDetector.detectCapabilities(worktreePath);
718
+ await this.copyEnvironmentFiles(worktreePath);
719
+ await this.copyIloomSettings(worktreePath);
720
+ const settingsData = await this.settings.loadSettings();
721
+ const basePort = ((_b = (_a = settingsData.capabilities) == null ? void 0 : _a.web) == null ? void 0 : _b.basePort) ?? 3e3;
722
+ let port = basePort;
723
+ if (capabilities.includes("web")) {
724
+ port = await this.setupPortForWeb(worktreePath, input, basePort);
725
+ }
726
+ logger.info("Database branch assumed to be already configured for existing worktree");
727
+ const databaseBranch = void 0;
728
+ if (input.type === "issue") {
729
+ try {
730
+ logger.info("Moving issue to In Progress...");
731
+ if (this.issueTracker.moveIssueToInProgress) {
732
+ await this.issueTracker.moveIssueToInProgress(input.identifier);
733
+ }
734
+ } catch (error) {
735
+ logger.warn(
736
+ `Failed to move issue to In Progress: ${error instanceof Error ? error.message : "Unknown error"}`,
737
+ error
738
+ );
739
+ }
740
+ }
741
+ const enableClaude = ((_c = input.options) == null ? void 0 : _c.enableClaude) !== false;
742
+ const enableCode = ((_d = input.options) == null ? void 0 : _d.enableCode) !== false;
743
+ const enableDevServer = ((_e = input.options) == null ? void 0 : _e.enableDevServer) !== false;
744
+ const enableTerminal = ((_f = input.options) == null ? void 0 : _f.enableTerminal) ?? false;
745
+ const oneShot = ((_g = input.options) == null ? void 0 : _g.oneShot) ?? "default";
746
+ const setArguments = (_h = input.options) == null ? void 0 : _h.setArguments;
747
+ const executablePath = (_i = input.options) == null ? void 0 : _i.executablePath;
748
+ if (enableClaude || enableCode || enableDevServer || enableTerminal) {
749
+ logger.info("Launching workspace components...");
750
+ const { LoomLauncher } = await import("./LoomLauncher-FLEMBCSQ.js");
751
+ const { ClaudeContextManager } = await import("./ClaudeContextManager-MUQSDY2E.js");
752
+ const claudeContext = new ClaudeContextManager(void 0, void 0, this.settings);
753
+ const launcher = new LoomLauncher(claudeContext, this.settings);
754
+ await launcher.launchLoom({
755
+ enableClaude,
756
+ enableCode,
757
+ enableDevServer,
758
+ enableTerminal,
759
+ worktreePath,
760
+ branchName,
761
+ port,
762
+ capabilities,
763
+ workflowType: input.type === "branch" ? "regular" : input.type,
764
+ identifier: input.identifier,
765
+ ...(issueData == null ? void 0 : issueData.title) && { title: issueData.title },
766
+ oneShot,
767
+ ...setArguments && { setArguments },
768
+ ...executablePath && { executablePath },
769
+ sourceEnvOnStart: settingsData.sourceEnvOnStart ?? false
770
+ });
771
+ }
772
+ const loom = {
773
+ id: this.generateLoomId(input),
774
+ path: worktreePath,
775
+ branch: branchName,
776
+ type: input.type,
777
+ identifier: input.identifier,
778
+ port,
779
+ createdAt: /* @__PURE__ */ new Date(),
780
+ // We don't have actual creation date, use now
781
+ lastAccessed: /* @__PURE__ */ new Date(),
782
+ ...databaseBranch !== void 0 && { databaseBranch },
783
+ ...capabilities.length > 0 && { capabilities },
784
+ ...Object.keys(binEntries).length > 0 && { binEntries },
785
+ ...issueData !== null && {
786
+ issueData: {
787
+ title: issueData.title,
788
+ body: issueData.body,
789
+ url: issueData.url,
790
+ state: issueData.state
791
+ }
792
+ }
793
+ };
794
+ logger.success(`Reused existing loom: ${loom.id} at ${loom.path}`);
795
+ return loom;
796
+ }
797
+ };
798
+
799
+ // src/lib/EnvironmentManager.ts
800
+ import fs3 from "fs-extra";
801
+ var logger2 = createLogger({ prefix: "\u{1F4DD}" });
802
+ var EnvironmentManager = class {
803
+ constructor() {
804
+ this.backupSuffix = ".backup";
805
+ }
806
+ /**
807
+ * Set or update an environment variable in a .env file
808
+ * Ports functionality from bash/utils/env-utils.sh:setEnvVar()
809
+ * @returns The backup path if a backup was created
810
+ */
811
+ async setEnvVar(filePath, key, value, backup = false) {
812
+ const validation = validateEnvVariable(key, value);
813
+ if (!validation.valid) {
814
+ throw new Error(validation.error ?? "Invalid variable name");
815
+ }
816
+ const fileExists = await fs3.pathExists(filePath);
817
+ if (!fileExists) {
818
+ logger2.info(`Creating ${filePath} with ${key}...`);
819
+ const content = formatEnvLine(key, value);
820
+ await fs3.writeFile(filePath, content, "utf8");
821
+ logger2.success(`${filePath} created with ${key}`);
822
+ return;
823
+ }
824
+ const existingContent = await fs3.readFile(filePath, "utf8");
825
+ const envMap = parseEnvFile(existingContent);
826
+ let backupPath;
827
+ if (backup) {
828
+ backupPath = await this.createBackup(filePath);
829
+ }
830
+ envMap.set(key, value);
831
+ const lines = existingContent.split("\n");
832
+ const newLines = [];
833
+ let variableUpdated = false;
834
+ for (const line of lines) {
835
+ const trimmedLine = line.trim();
836
+ if (!trimmedLine || trimmedLine.startsWith("#")) {
837
+ newLines.push(line);
838
+ continue;
839
+ }
840
+ const cleanLine = trimmedLine.startsWith("export ") ? trimmedLine.substring(7) : trimmedLine;
841
+ const equalsIndex = cleanLine.indexOf("=");
842
+ if (equalsIndex !== -1) {
843
+ const lineKey = cleanLine.substring(0, equalsIndex).trim();
844
+ if (lineKey === key) {
845
+ newLines.push(formatEnvLine(key, value));
846
+ variableUpdated = true;
847
+ continue;
848
+ }
849
+ }
850
+ newLines.push(line);
851
+ }
852
+ if (!variableUpdated) {
853
+ logger2.info(`Adding ${key} to ${filePath}...`);
854
+ newLines.push(formatEnvLine(key, value));
855
+ logger2.success(`${key} added successfully`);
856
+ } else {
857
+ logger2.info(`Updating ${key} in ${filePath}...`);
858
+ logger2.success(`${key} updated successfully`);
859
+ }
860
+ const newContent = newLines.join("\n");
861
+ await fs3.writeFile(filePath, newContent, "utf8");
862
+ return backupPath;
863
+ }
864
+ /**
865
+ * Read and parse a .env file
866
+ */
867
+ async readEnvFile(filePath) {
868
+ try {
869
+ const content = await fs3.readFile(filePath, "utf8");
870
+ return parseEnvFile(content);
871
+ } catch (error) {
872
+ logger2.debug(
873
+ `Could not read env file ${filePath}: ${error instanceof Error ? error.message : String(error)}`
874
+ );
875
+ return /* @__PURE__ */ new Map();
876
+ }
877
+ }
878
+ /**
879
+ * Get a specific environment variable from a .env file
880
+ * Returns null if file doesn't exist or variable is not found
881
+ */
882
+ async getEnvVariable(filePath, variableName) {
883
+ const envVars = await this.readEnvFile(filePath);
884
+ return envVars.get(variableName) ?? null;
885
+ }
886
+ /**
887
+ * Generic file copy helper that only copies if source exists
888
+ * Does not throw if source file doesn't exist - just logs and returns
889
+ * @private
890
+ */
891
+ async copyIfExists(source, destination) {
892
+ const sourceExists = await fs3.pathExists(source);
893
+ if (!sourceExists) {
894
+ logger2.debug(`Source file ${source} does not exist, skipping copy`);
895
+ return;
896
+ }
897
+ await fs3.copy(source, destination, { overwrite: false });
898
+ logger2.success(`Copied ${source} to ${destination}`);
899
+ }
900
+ /**
901
+ * Calculate unique port for workspace
902
+ * Implements:
903
+ * - Issue/PR: 3000 + issue/PR number
904
+ * - Branch: 3000 + deterministic hash offset (1-999)
905
+ */
906
+ calculatePort(options) {
907
+ const basePort = options.basePort ?? 3e3;
908
+ if (options.issueNumber !== void 0) {
909
+ const numericIssue = typeof options.issueNumber === "number" ? options.issueNumber : parseInt(String(options.issueNumber), 10);
910
+ if (!isNaN(numericIssue) && String(numericIssue) === String(options.issueNumber)) {
911
+ const port = basePort + numericIssue;
912
+ if (port > 65535) {
913
+ throw new Error(
914
+ `Calculated port ${port} exceeds maximum (65535). Use a lower base port or issue number.`
915
+ );
916
+ }
917
+ return port;
918
+ }
919
+ return calculatePortForBranch(String(options.issueNumber), basePort);
920
+ }
921
+ if (options.prNumber !== void 0) {
922
+ const port = basePort + options.prNumber;
923
+ if (port > 65535) {
924
+ throw new Error(
925
+ `Calculated port ${port} exceeds maximum (65535). Use a lower base port or PR number.`
926
+ );
927
+ }
928
+ return port;
929
+ }
930
+ if (options.branchName !== void 0) {
931
+ return calculatePortForBranch(options.branchName, basePort);
932
+ }
933
+ return basePort;
934
+ }
935
+ /**
936
+ * Set port environment variable for workspace
937
+ */
938
+ async setPortForWorkspace(envFilePath, issueNumber, prNumber, branchName) {
939
+ const options = {};
940
+ if (issueNumber !== void 0) {
941
+ options.issueNumber = issueNumber;
942
+ }
943
+ if (prNumber !== void 0) {
944
+ options.prNumber = prNumber;
945
+ }
946
+ if (branchName !== void 0) {
947
+ options.branchName = branchName;
948
+ }
949
+ const port = this.calculatePort(options);
950
+ await this.setEnvVar(envFilePath, "PORT", String(port));
951
+ return port;
952
+ }
953
+ /**
954
+ * Validate environment configuration
955
+ */
956
+ async validateEnvFile(filePath) {
957
+ try {
958
+ const content = await fs3.readFile(filePath, "utf8");
959
+ const envMap = parseEnvFile(content);
960
+ const errors = [];
961
+ for (const [key, value] of envMap.entries()) {
962
+ const validation = validateEnvVariable(key, value);
963
+ if (!validation.valid) {
964
+ errors.push(`${key}: ${validation.error}`);
965
+ }
966
+ }
967
+ return {
968
+ valid: errors.length === 0,
969
+ errors
970
+ };
971
+ } catch (error) {
972
+ return {
973
+ valid: false,
974
+ errors: [
975
+ `Failed to read or parse file: ${error instanceof Error ? error.message : String(error)}`
976
+ ]
977
+ };
978
+ }
979
+ }
980
+ /**
981
+ * Create backup of existing file
982
+ */
983
+ async createBackup(filePath) {
984
+ const timestamp = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-");
985
+ const backupPath = `${filePath}${this.backupSuffix}-${timestamp}`;
986
+ await fs3.copy(filePath, backupPath);
987
+ logger2.debug(`Created backup at ${backupPath}`);
988
+ return backupPath;
989
+ }
990
+ };
991
+
992
+ // src/lib/CLIIsolationManager.ts
993
+ import fs4 from "fs-extra";
994
+ import path3 from "path";
995
+ import os from "os";
996
+ var CLIIsolationManager = class {
997
+ constructor() {
998
+ this.iloomBinDir = path3.join(os.homedir(), ".iloom", "bin");
999
+ }
1000
+ /**
1001
+ * Setup CLI isolation for a worktree
1002
+ * - Build the project
1003
+ * - Create versioned symlinks
1004
+ * - Check PATH configuration
1005
+ * @param worktreePath Path to the worktree
1006
+ * @param identifier Issue/PR number or branch identifier
1007
+ * @param binEntries Bin entries from package.json
1008
+ * @returns Array of created symlink names
1009
+ */
1010
+ async setupCLIIsolation(worktreePath, identifier, binEntries) {
1011
+ await this.buildProject(worktreePath);
1012
+ await this.verifyBinTargets(worktreePath, binEntries);
1013
+ await fs4.ensureDir(this.iloomBinDir);
1014
+ const symlinkNames = await this.createVersionedSymlinks(
1015
+ worktreePath,
1016
+ identifier,
1017
+ binEntries
1018
+ );
1019
+ await this.ensureIloomBinInPath();
1020
+ return symlinkNames;
1021
+ }
1022
+ /**
1023
+ * Build the project using package.json build script
1024
+ * @param worktreePath Path to the worktree
1025
+ */
1026
+ async buildProject(worktreePath) {
1027
+ const pkgJson = await readPackageJson(worktreePath);
1028
+ if (!hasScript(pkgJson, "build")) {
1029
+ logger.warn("No build script found in package.json - skipping build");
1030
+ return;
1031
+ }
1032
+ logger.info("Building CLI tool...");
1033
+ await runScript("build", worktreePath, [], { quiet: true });
1034
+ logger.success("Build completed");
1035
+ }
1036
+ /**
1037
+ * Verify bin targets exist and are executable
1038
+ * @param worktreePath Path to the worktree
1039
+ * @param binEntries Bin entries from package.json
1040
+ */
1041
+ async verifyBinTargets(worktreePath, binEntries) {
1042
+ for (const binPath of Object.values(binEntries)) {
1043
+ const targetPath = path3.resolve(worktreePath, binPath);
1044
+ const exists = await fs4.pathExists(targetPath);
1045
+ if (!exists) {
1046
+ throw new Error(`Bin target does not exist: ${targetPath}`);
1047
+ }
1048
+ try {
1049
+ await fs4.access(targetPath, fs4.constants.X_OK);
1050
+ } catch {
1051
+ }
1052
+ }
1053
+ }
1054
+ /**
1055
+ * Create versioned symlinks in ~/.iloom/bin
1056
+ * @param worktreePath Path to the worktree
1057
+ * @param identifier Issue/PR number or branch identifier
1058
+ * @param binEntries Bin entries from package.json
1059
+ * @returns Array of created symlink names
1060
+ */
1061
+ async createVersionedSymlinks(worktreePath, identifier, binEntries) {
1062
+ const symlinkNames = [];
1063
+ for (const [binName, binPath] of Object.entries(binEntries)) {
1064
+ const versionedName = `${binName}-${identifier}`;
1065
+ const targetPath = path3.resolve(worktreePath, binPath);
1066
+ const symlinkPath = path3.join(this.iloomBinDir, versionedName);
1067
+ await fs4.symlink(targetPath, symlinkPath);
1068
+ logger.success(`CLI available: ${versionedName}`);
1069
+ symlinkNames.push(versionedName);
1070
+ }
1071
+ return symlinkNames;
1072
+ }
1073
+ /**
1074
+ * Check if ~/.iloom/bin is in PATH and provide setup instructions
1075
+ */
1076
+ async ensureIloomBinInPath() {
1077
+ const currentPath = process.env.PATH ?? "";
1078
+ if (currentPath.includes(".iloom/bin")) {
1079
+ return;
1080
+ }
1081
+ const shell = this.detectShell();
1082
+ const rcFile = this.getShellRcFile(shell);
1083
+ logger.warn("\n\u26A0\uFE0F One-time PATH setup required:");
1084
+ logger.warn(` Add to ${rcFile}:`);
1085
+ logger.warn(` export PATH="$HOME/.iloom/bin:$PATH"`);
1086
+ logger.warn(` Then run: source ${rcFile}
1087
+ `);
1088
+ }
1089
+ /**
1090
+ * Detect current shell
1091
+ * @returns Shell name (zsh, bash, fish, etc.)
1092
+ */
1093
+ detectShell() {
1094
+ const shell = process.env.SHELL ?? "";
1095
+ return shell.split("/").pop() ?? "bash";
1096
+ }
1097
+ /**
1098
+ * Get RC file path for shell
1099
+ * @param shell Shell name
1100
+ * @returns RC file path
1101
+ */
1102
+ getShellRcFile(shell) {
1103
+ const rcFiles = {
1104
+ zsh: "~/.zshrc",
1105
+ bash: "~/.bashrc",
1106
+ fish: "~/.config/fish/config.fish"
1107
+ };
1108
+ return rcFiles[shell] ?? "~/.bashrc";
1109
+ }
1110
+ /**
1111
+ * Cleanup versioned CLI executables for a specific identifier
1112
+ * Removes all symlinks matching the pattern: {binName}-{identifier}
1113
+ *
1114
+ * @param identifier - Issue/PR number or branch identifier
1115
+ * @returns Array of removed symlink names
1116
+ */
1117
+ async cleanupVersionedExecutables(identifier) {
1118
+ const removed = [];
1119
+ try {
1120
+ const files = await fs4.readdir(this.iloomBinDir);
1121
+ for (const file of files) {
1122
+ if (this.matchesIdentifier(file, identifier)) {
1123
+ const symlinkPath = path3.join(this.iloomBinDir, file);
1124
+ try {
1125
+ await fs4.unlink(symlinkPath);
1126
+ removed.push(file);
1127
+ } catch (error) {
1128
+ const isEnoent = error && typeof error === "object" && "code" in error && error.code === "ENOENT";
1129
+ if (isEnoent) {
1130
+ removed.push(file);
1131
+ continue;
1132
+ }
1133
+ logger.warn(
1134
+ `Failed to remove symlink ${file}: ${error instanceof Error ? error.message : "Unknown error"}`
1135
+ );
1136
+ }
1137
+ }
1138
+ }
1139
+ } catch (error) {
1140
+ const isEnoent = error && typeof error === "object" && "code" in error && error.code === "ENOENT";
1141
+ if (isEnoent) {
1142
+ logger.warn("No CLI executables directory found - nothing to cleanup");
1143
+ return [];
1144
+ }
1145
+ throw error;
1146
+ }
1147
+ if (removed.length > 0) {
1148
+ logger.success(`Removed CLI executables: ${removed.join(", ")}`);
1149
+ }
1150
+ return removed;
1151
+ }
1152
+ /**
1153
+ * Find orphaned symlinks in ~/.iloom/bin
1154
+ * Returns symlinks that point to non-existent targets
1155
+ *
1156
+ * @returns Array of orphaned symlink information
1157
+ */
1158
+ async findOrphanedSymlinks() {
1159
+ const orphaned = [];
1160
+ try {
1161
+ const files = await fs4.readdir(this.iloomBinDir);
1162
+ for (const file of files) {
1163
+ const symlinkPath = path3.join(this.iloomBinDir, file);
1164
+ try {
1165
+ const stats = await fs4.lstat(symlinkPath);
1166
+ if (stats.isSymbolicLink()) {
1167
+ const target = await fs4.readlink(symlinkPath);
1168
+ try {
1169
+ await fs4.access(target);
1170
+ } catch {
1171
+ orphaned.push({
1172
+ name: file,
1173
+ path: symlinkPath,
1174
+ brokenTarget: target
1175
+ });
1176
+ }
1177
+ }
1178
+ } catch (error) {
1179
+ logger.warn(
1180
+ `Failed to check symlink ${file}: ${error instanceof Error ? error.message : "Unknown error"}`
1181
+ );
1182
+ }
1183
+ }
1184
+ } catch (error) {
1185
+ const isEnoent = error && typeof error === "object" && "code" in error && error.code === "ENOENT";
1186
+ if (isEnoent) {
1187
+ return [];
1188
+ }
1189
+ throw error;
1190
+ }
1191
+ return orphaned;
1192
+ }
1193
+ /**
1194
+ * Cleanup all orphaned symlinks
1195
+ * Removes symlinks that point to non-existent targets
1196
+ *
1197
+ * @returns Number of symlinks removed
1198
+ */
1199
+ async cleanupOrphanedSymlinks() {
1200
+ const orphaned = await this.findOrphanedSymlinks();
1201
+ let removedCount = 0;
1202
+ for (const symlink of orphaned) {
1203
+ try {
1204
+ await fs4.unlink(symlink.path);
1205
+ removedCount++;
1206
+ logger.success(`Removed orphaned symlink: ${symlink.name}`);
1207
+ } catch (error) {
1208
+ logger.warn(
1209
+ `Failed to remove orphaned symlink ${symlink.name}: ${error instanceof Error ? error.message : "Unknown error"}`
1210
+ );
1211
+ }
1212
+ }
1213
+ return removedCount;
1214
+ }
1215
+ /**
1216
+ * Check if a filename matches the versioned pattern for an identifier
1217
+ * Pattern: {binName}-{identifier}
1218
+ *
1219
+ * @param fileName - Name of the file to check
1220
+ * @param identifier - Issue/PR number or branch identifier
1221
+ * @returns True if the filename matches the pattern
1222
+ */
1223
+ matchesIdentifier(fileName, identifier) {
1224
+ const suffix = `-${identifier}`;
1225
+ return fileName.endsWith(suffix);
1226
+ }
1227
+ };
1228
+
1229
+ // src/lib/DatabaseManager.ts
1230
+ var logger3 = createLogger({ prefix: "\u{1F5C2}\uFE0F" });
1231
+ var DatabaseManager = class {
1232
+ constructor(provider, environment, databaseUrlEnvVarName = "DATABASE_URL") {
1233
+ this.provider = provider;
1234
+ this.environment = environment;
1235
+ this.databaseUrlEnvVarName = databaseUrlEnvVarName;
1236
+ if (databaseUrlEnvVarName !== "DATABASE_URL") {
1237
+ logger3.debug(`\u{1F527} DatabaseManager configured with custom variable: ${databaseUrlEnvVarName}`);
1238
+ } else {
1239
+ logger3.debug("\u{1F527} DatabaseManager using default variable: DATABASE_URL");
1240
+ }
1241
+ }
1242
+ /**
1243
+ * Get the configured database URL environment variable name
1244
+ */
1245
+ getConfiguredVariableName() {
1246
+ return this.databaseUrlEnvVarName;
1247
+ }
1248
+ /**
1249
+ * Check if database branching should be used
1250
+ * Requires BOTH conditions:
1251
+ * 1. Database provider is properly configured (checked via provider.isConfigured())
1252
+ * 2. .env file contains the configured database URL variable
1253
+ */
1254
+ async shouldUseDatabaseBranching(envFilePath) {
1255
+ if (!this.provider.isConfigured()) {
1256
+ logger3.debug("Skipping database branching: Database provider not configured");
1257
+ return false;
1258
+ }
1259
+ const hasDatabaseUrl = await this.hasDatabaseUrlInEnv(envFilePath);
1260
+ if (!hasDatabaseUrl) {
1261
+ logger3.debug(
1262
+ "Skipping database branching: configured database URL variable not found in .env file"
1263
+ );
1264
+ return false;
1265
+ }
1266
+ return true;
1267
+ }
1268
+ /**
1269
+ * Create database branch only if configured
1270
+ * Returns connection string if branch was created, null if skipped
1271
+ *
1272
+ * @param branchName - Name of the branch to create
1273
+ * @param envFilePath - Path to .env file for configuration checks
1274
+ * @param cwd - Optional working directory to run commands from
1275
+ * @param fromBranch - Optional parent branch to create from (for child looms)
1276
+ */
1277
+ async createBranchIfConfigured(branchName, envFilePath, cwd, fromBranch) {
1278
+ if (!await this.shouldUseDatabaseBranching(envFilePath)) {
1279
+ return null;
1280
+ }
1281
+ if (!await this.provider.isCliAvailable()) {
1282
+ logger3.warn("Skipping database branch creation: Neon CLI not available");
1283
+ logger3.warn("Install with: npm install -g neonctl");
1284
+ return null;
1285
+ }
1286
+ try {
1287
+ const isAuth = await this.provider.isAuthenticated(cwd);
1288
+ if (!isAuth) {
1289
+ logger3.warn("Skipping database branch creation: Not authenticated with Neon CLI");
1290
+ logger3.warn("Run: neon auth");
1291
+ return null;
1292
+ }
1293
+ } catch (error) {
1294
+ const errorMessage = error instanceof Error ? error.message : String(error);
1295
+ logger3.error(`Database authentication check failed: ${errorMessage}`);
1296
+ throw error;
1297
+ }
1298
+ try {
1299
+ const connectionString = await this.provider.createBranch(branchName, fromBranch, cwd);
1300
+ logger3.success(`Database branch ready: ${this.provider.sanitizeBranchName(branchName)}`);
1301
+ return connectionString;
1302
+ } catch (error) {
1303
+ logger3.error(
1304
+ `Failed to create database branch: ${error instanceof Error ? error.message : String(error)}`
1305
+ );
1306
+ throw error;
1307
+ }
1308
+ }
1309
+ /**
1310
+ * Delete database branch only if configured
1311
+ * Returns result object indicating what happened
1312
+ *
1313
+ * @param branchName - Name of the branch to delete
1314
+ * @param shouldCleanup - Boolean indicating if database cleanup should be performed (pre-fetched config)
1315
+ * @param isPreview - Whether this is a preview database branch
1316
+ * @param cwd - Optional working directory to run commands from (prevents issues with deleted directories)
1317
+ */
1318
+ async deleteBranchIfConfigured(branchName, shouldCleanup, isPreview = false, cwd) {
1319
+ if (shouldCleanup === false) {
1320
+ return {
1321
+ success: true,
1322
+ deleted: false,
1323
+ notFound: true,
1324
+ // Treat "not configured" as "nothing to delete"
1325
+ branchName
1326
+ };
1327
+ }
1328
+ if (!this.provider.isConfigured()) {
1329
+ logger3.debug("Skipping database branch deletion: Database provider not configured");
1330
+ return {
1331
+ success: true,
1332
+ deleted: false,
1333
+ notFound: true,
1334
+ branchName
1335
+ };
1336
+ }
1337
+ if (!await this.provider.isCliAvailable()) {
1338
+ logger3.info("Skipping database branch deletion: CLI tool not available");
1339
+ return {
1340
+ success: false,
1341
+ deleted: false,
1342
+ notFound: true,
1343
+ error: "CLI tool not available",
1344
+ branchName
1345
+ };
1346
+ }
1347
+ try {
1348
+ const isAuth = await this.provider.isAuthenticated(cwd);
1349
+ if (!isAuth) {
1350
+ logger3.warn("Skipping database branch deletion: Not authenticated with DB Provider");
1351
+ return {
1352
+ success: false,
1353
+ deleted: false,
1354
+ notFound: false,
1355
+ error: "Not authenticated with DB Provider",
1356
+ branchName
1357
+ };
1358
+ }
1359
+ } catch (error) {
1360
+ const errorMessage = error instanceof Error ? error.message : String(error);
1361
+ logger3.error(`Database authentication check failed: ${errorMessage}`);
1362
+ return {
1363
+ success: false,
1364
+ deleted: false,
1365
+ notFound: false,
1366
+ error: `Authentication check failed: ${errorMessage}`,
1367
+ branchName
1368
+ };
1369
+ }
1370
+ try {
1371
+ const result = await this.provider.deleteBranch(branchName, isPreview, cwd);
1372
+ return result;
1373
+ } catch (error) {
1374
+ logger3.warn(
1375
+ `Unexpected error in database deletion: ${error instanceof Error ? error.message : String(error)}`
1376
+ );
1377
+ return {
1378
+ success: false,
1379
+ deleted: false,
1380
+ notFound: false,
1381
+ error: error instanceof Error ? error.message : String(error),
1382
+ branchName
1383
+ };
1384
+ }
1385
+ }
1386
+ /**
1387
+ * Get database branch name from connection string (reverse lookup)
1388
+ * Returns branch name if provider supports reverse lookup, null otherwise
1389
+ *
1390
+ * @param connectionString - Database connection string
1391
+ * @param cwd - Optional working directory to run commands from
1392
+ */
1393
+ async getBranchNameFromConnectionString(connectionString, cwd) {
1394
+ if (!this.provider.isConfigured()) {
1395
+ logger3.debug("Provider not configured, skipping reverse lookup");
1396
+ return null;
1397
+ }
1398
+ if ("getBranchNameFromConnectionString" in this.provider && typeof this.provider.getBranchNameFromConnectionString === "function") {
1399
+ return this.provider.getBranchNameFromConnectionString(connectionString, cwd);
1400
+ }
1401
+ logger3.debug("Provider does not support reverse lookup");
1402
+ return null;
1403
+ }
1404
+ /**
1405
+ * Check if .env has the configured database URL variable
1406
+ * CRITICAL: If user explicitly configured a custom variable name (not default),
1407
+ * throw an error if it's missing from .env
1408
+ */
1409
+ async hasDatabaseUrlInEnv(envFilePath) {
1410
+ try {
1411
+ const envMap = await this.environment.readEnvFile(envFilePath);
1412
+ if (this.databaseUrlEnvVarName !== "DATABASE_URL") {
1413
+ logger3.debug(`Looking for custom database URL variable: ${this.databaseUrlEnvVarName}`);
1414
+ } else {
1415
+ logger3.debug("Looking for default database URL variable: DATABASE_URL");
1416
+ }
1417
+ if (envMap.has(this.databaseUrlEnvVarName)) {
1418
+ if (this.databaseUrlEnvVarName !== "DATABASE_URL") {
1419
+ logger3.debug(`\u2705 Found custom database URL variable: ${this.databaseUrlEnvVarName}`);
1420
+ } else {
1421
+ logger3.debug(`\u2705 Found default database URL variable: DATABASE_URL`);
1422
+ }
1423
+ return true;
1424
+ }
1425
+ if (this.databaseUrlEnvVarName !== "DATABASE_URL") {
1426
+ logger3.debug(`\u274C Custom database URL variable '${this.databaseUrlEnvVarName}' not found in .env file`);
1427
+ throw new Error(
1428
+ `Configured database URL environment variable '${this.databaseUrlEnvVarName}' not found in .env file. Please add it to your .env file or update your iloom configuration.`
1429
+ );
1430
+ }
1431
+ const hasDefaultVar = envMap.has("DATABASE_URL");
1432
+ if (hasDefaultVar) {
1433
+ logger3.debug("\u2705 Found fallback DATABASE_URL variable");
1434
+ } else {
1435
+ logger3.debug("\u274C No DATABASE_URL variable found in .env file");
1436
+ }
1437
+ return hasDefaultVar;
1438
+ } catch (error) {
1439
+ if (error instanceof Error && error.message.includes("not found in .env")) {
1440
+ throw error;
1441
+ }
1442
+ return false;
1443
+ }
1444
+ }
1445
+ };
1446
+
1447
+ // src/lib/ResourceCleanup.ts
1448
+ import path4 from "path";
1449
+ var ResourceCleanup = class {
1450
+ constructor(gitWorktree, processManager, database, cliIsolation, settingsManager) {
1451
+ this.gitWorktree = gitWorktree;
1452
+ this.processManager = processManager;
1453
+ this.database = database;
1454
+ this.cliIsolation = cliIsolation;
1455
+ this.settingsManager = settingsManager ?? new SettingsManager();
1456
+ }
1457
+ /**
1458
+ * Cleanup a worktree and associated resources
1459
+ * Main orchestration method
1460
+ *
1461
+ * @param parsed - ParsedInput from IdentifierParser with type information
1462
+ * @param options - Cleanup options
1463
+ */
1464
+ async cleanupWorktree(parsed, options = {}) {
1465
+ var _a;
1466
+ const operations = [];
1467
+ const errors = [];
1468
+ const displayIdentifier = parsed.branchName ?? ((_a = parsed.number) == null ? void 0 : _a.toString()) ?? parsed.originalInput;
1469
+ logger.info(`Starting cleanup for: ${displayIdentifier}`);
1470
+ const number = parsed.number;
1471
+ if (number !== void 0) {
1472
+ const port = this.processManager.calculatePort(number);
1473
+ if (options.dryRun) {
1474
+ operations.push({
1475
+ type: "dev-server",
1476
+ success: true,
1477
+ message: `[DRY RUN] Would check for dev server on port ${port}`
1478
+ });
1479
+ } else {
1480
+ try {
1481
+ const terminated = await this.terminateDevServer(port);
1482
+ operations.push({
1483
+ type: "dev-server",
1484
+ success: true,
1485
+ message: terminated ? `Dev server on port ${port} terminated` : `No dev server running on port ${port}`
1486
+ });
1487
+ } catch (error) {
1488
+ const err = error instanceof Error ? error : new Error("Unknown error");
1489
+ errors.push(err);
1490
+ operations.push({
1491
+ type: "dev-server",
1492
+ success: false,
1493
+ message: `Failed to terminate dev server`,
1494
+ error: err.message
1495
+ });
1496
+ }
1497
+ }
1498
+ }
1499
+ let worktree = null;
1500
+ try {
1501
+ if (parsed.type === "pr" && parsed.number !== void 0) {
1502
+ const prNumber = typeof parsed.number === "number" ? parsed.number : Number(parsed.number);
1503
+ if (isNaN(prNumber) || !isFinite(prNumber)) {
1504
+ throw new Error(`Invalid PR number: ${parsed.number}. PR numbers must be numeric.`);
1505
+ }
1506
+ worktree = await this.gitWorktree.findWorktreeForPR(prNumber, "");
1507
+ } else if (parsed.type === "issue" && parsed.number !== void 0) {
1508
+ worktree = await this.gitWorktree.findWorktreeForIssue(parsed.number);
1509
+ } else if (parsed.type === "branch" && parsed.branchName) {
1510
+ worktree = await this.gitWorktree.findWorktreeForBranch(parsed.branchName);
1511
+ }
1512
+ if (!worktree) {
1513
+ throw new Error(`No worktree found for identifier: ${displayIdentifier}`);
1514
+ }
1515
+ logger.debug(`Found worktree: path="${worktree.path}", branch="${worktree.branch}"`);
1516
+ } catch (error) {
1517
+ const err = error instanceof Error ? error : new Error("Unknown error");
1518
+ errors.push(err);
1519
+ return {
1520
+ identifier: displayIdentifier,
1521
+ success: false,
1522
+ operations,
1523
+ errors,
1524
+ rollbackRequired: false
1525
+ };
1526
+ }
1527
+ if (!options.force) {
1528
+ const safety = await this.validateWorktreeSafety(worktree, parsed.originalInput);
1529
+ if (!safety.isSafe) {
1530
+ const blockerMessage = safety.blockers.join("\n\n");
1531
+ throw new Error(`Cannot cleanup:
1532
+
1533
+ ${blockerMessage}`);
1534
+ }
1535
+ if (safety.warnings.length > 0) {
1536
+ safety.warnings.forEach((warning) => {
1537
+ logger.warn(warning);
1538
+ });
1539
+ }
1540
+ }
1541
+ let databaseConfig = null;
1542
+ if (!options.keepDatabase && worktree) {
1543
+ const envFilePath = path4.join(worktree.path, ".env");
1544
+ try {
1545
+ const shouldCleanup = this.database ? await this.database.shouldUseDatabaseBranching(envFilePath) : false;
1546
+ databaseConfig = { shouldCleanup, envFilePath };
1547
+ } catch (error) {
1548
+ logger.warn(
1549
+ `Failed to read database config from ${envFilePath}, skipping database cleanup: ${error instanceof Error ? error.message : String(error)}`
1550
+ );
1551
+ databaseConfig = { shouldCleanup: false, envFilePath };
1552
+ }
1553
+ }
1554
+ let mainWorktreePath = null;
1555
+ if (!options.dryRun) {
1556
+ try {
1557
+ mainWorktreePath = await findMainWorktreePathWithSettings(worktree.path, this.settingsManager);
1558
+ } catch (error) {
1559
+ logger.warn(
1560
+ `Failed to find main worktree path: ${error instanceof Error ? error.message : String(error)}`
1561
+ );
1562
+ }
1563
+ }
1564
+ if (options.dryRun) {
1565
+ operations.push({
1566
+ type: "worktree",
1567
+ success: true,
1568
+ message: `[DRY RUN] Would remove worktree: ${worktree.path}`
1569
+ });
1570
+ } else {
1571
+ try {
1572
+ const worktreeOptions = {
1573
+ removeDirectory: true,
1574
+ removeBranch: false
1575
+ // Handle branch separately
1576
+ };
1577
+ if (options.force !== void 0) {
1578
+ worktreeOptions.force = options.force;
1579
+ }
1580
+ await this.gitWorktree.removeWorktree(worktree.path, worktreeOptions);
1581
+ operations.push({
1582
+ type: "worktree",
1583
+ success: true,
1584
+ message: `Worktree removed: ${worktree.path}`
1585
+ });
1586
+ } catch (error) {
1587
+ const err = error instanceof Error ? error : new Error("Unknown error");
1588
+ errors.push(err);
1589
+ operations.push({
1590
+ type: "worktree",
1591
+ success: false,
1592
+ message: `Failed to remove worktree`,
1593
+ error: err.message
1594
+ });
1595
+ }
1596
+ }
1597
+ if (options.deleteBranch && worktree) {
1598
+ if (options.dryRun) {
1599
+ operations.push({
1600
+ type: "branch",
1601
+ success: true,
1602
+ message: `[DRY RUN] Would delete branch: ${worktree.branch}`
1603
+ });
1604
+ } else {
1605
+ try {
1606
+ const branchOptions = { dryRun: false };
1607
+ if (options.force !== void 0) {
1608
+ branchOptions.force = options.force;
1609
+ }
1610
+ await this.deleteBranch(worktree.branch, branchOptions, mainWorktreePath ?? void 0);
1611
+ operations.push({
1612
+ type: "branch",
1613
+ success: true,
1614
+ message: `Branch deleted: ${worktree.branch}`
1615
+ });
1616
+ } catch (error) {
1617
+ const err = error instanceof Error ? error : new Error("Unknown error");
1618
+ errors.push(err);
1619
+ operations.push({
1620
+ type: "branch",
1621
+ success: false,
1622
+ message: `Failed to delete branch`,
1623
+ error: err.message
1624
+ });
1625
+ }
1626
+ }
1627
+ }
1628
+ const cliIdentifier = parsed.number ?? parsed.branchName;
1629
+ if (this.cliIsolation && cliIdentifier !== void 0) {
1630
+ if (options.dryRun) {
1631
+ operations.push({
1632
+ type: "cli-symlinks",
1633
+ success: true,
1634
+ message: `[DRY RUN] Would cleanup CLI symlinks for: ${cliIdentifier}`
1635
+ });
1636
+ } else {
1637
+ try {
1638
+ const removed = await this.cliIsolation.cleanupVersionedExecutables(cliIdentifier);
1639
+ operations.push({
1640
+ type: "cli-symlinks",
1641
+ success: true,
1642
+ message: removed.length > 0 ? `CLI symlinks removed: ${removed.length}` : "No CLI symlinks to cleanup"
1643
+ });
1644
+ } catch (error) {
1645
+ const err = error instanceof Error ? error : new Error("Unknown error");
1646
+ errors.push(err);
1647
+ logger.warn(
1648
+ `CLI symlink cleanup failed: ${err.message}`
1649
+ );
1650
+ operations.push({
1651
+ type: "cli-symlinks",
1652
+ success: false,
1653
+ message: "CLI symlink cleanup failed (non-fatal)"
1654
+ });
1655
+ }
1656
+ }
1657
+ }
1658
+ if (databaseConfig && worktree) {
1659
+ if (options.dryRun) {
1660
+ operations.push({
1661
+ type: "database",
1662
+ success: true,
1663
+ message: `[DRY RUN] Would cleanup database branch for: ${worktree.branch}`
1664
+ });
1665
+ } else {
1666
+ try {
1667
+ if (databaseConfig.shouldCleanup && this.database) {
1668
+ try {
1669
+ const deletionResult = await this.database.deleteBranchIfConfigured(
1670
+ worktree.branch,
1671
+ databaseConfig.shouldCleanup,
1672
+ false,
1673
+ // isPreview
1674
+ mainWorktreePath ?? void 0
1675
+ );
1676
+ if (deletionResult.deleted) {
1677
+ logger.info(`Database branch deleted: ${worktree.branch}`);
1678
+ operations.push({
1679
+ type: "database",
1680
+ success: true,
1681
+ message: `Database branch deleted`,
1682
+ deleted: true
1683
+ });
1684
+ } else if (deletionResult.notFound) {
1685
+ logger.debug(`No database branch found for: ${worktree.branch}`);
1686
+ operations.push({
1687
+ type: "database",
1688
+ success: true,
1689
+ message: `No database branch found (skipped)`,
1690
+ deleted: false
1691
+ });
1692
+ } else if (deletionResult.userDeclined) {
1693
+ logger.info("Preview database deletion declined by user");
1694
+ operations.push({
1695
+ type: "database",
1696
+ success: true,
1697
+ message: `Database cleanup skipped (user declined)`,
1698
+ deleted: false
1699
+ });
1700
+ } else if (!deletionResult.success) {
1701
+ const errorMsg = deletionResult.error ?? "Unknown error";
1702
+ errors.push(new Error(errorMsg));
1703
+ logger.warn(`Database cleanup failed: ${errorMsg}`);
1704
+ operations.push({
1705
+ type: "database",
1706
+ success: false,
1707
+ // Non-fatal, but report error
1708
+ message: `Database cleanup failed`,
1709
+ error: errorMsg,
1710
+ deleted: false
1711
+ });
1712
+ } else {
1713
+ errors.push(new Error("Database cleanup in an unknown state"));
1714
+ logger.warn("Database deletion returned unexpected result state");
1715
+ operations.push({
1716
+ type: "database",
1717
+ success: false,
1718
+ message: `Database cleanup in an unknown state`,
1719
+ deleted: false
1720
+ });
1721
+ }
1722
+ } catch (error) {
1723
+ errors.push(error instanceof Error ? error : new Error(String(error)));
1724
+ logger.warn(
1725
+ `Unexpected database cleanup exception: ${error instanceof Error ? error.message : String(error)}`
1726
+ );
1727
+ operations.push({
1728
+ type: "database",
1729
+ success: false,
1730
+ message: `Database cleanup failed`,
1731
+ error: error instanceof Error ? error.message : String(error),
1732
+ deleted: false
1733
+ });
1734
+ }
1735
+ } else {
1736
+ operations.push({
1737
+ type: "database",
1738
+ success: true,
1739
+ message: `Database cleanup skipped (not available)`,
1740
+ deleted: false
1741
+ });
1742
+ }
1743
+ } catch (error) {
1744
+ const err = error instanceof Error ? error : new Error("Unknown error");
1745
+ errors.push(err);
1746
+ operations.push({
1747
+ type: "database",
1748
+ success: false,
1749
+ message: `Database cleanup failed`,
1750
+ error: err.message,
1751
+ deleted: false
1752
+ });
1753
+ }
1754
+ }
1755
+ }
1756
+ const success = errors.length === 0;
1757
+ return {
1758
+ identifier: displayIdentifier,
1759
+ branchName: worktree == null ? void 0 : worktree.branch,
1760
+ success,
1761
+ operations,
1762
+ errors,
1763
+ rollbackRequired: false
1764
+ // Cleanup operations are generally not reversible
1765
+ };
1766
+ }
1767
+ /**
1768
+ * Terminate dev server on specified port
1769
+ */
1770
+ async terminateDevServer(port) {
1771
+ logger.debug(`Checking for dev server on port ${port}`);
1772
+ const processInfo = await this.processManager.detectDevServer(port);
1773
+ if (!processInfo) {
1774
+ logger.debug(`No process found on port ${port}`);
1775
+ return false;
1776
+ }
1777
+ if (!processInfo.isDevServer) {
1778
+ logger.warn(
1779
+ `Process on port ${port} (${processInfo.name}) doesn't appear to be a dev server, skipping`
1780
+ );
1781
+ return false;
1782
+ }
1783
+ logger.info(`Terminating dev server: ${processInfo.name} (PID: ${processInfo.pid})`);
1784
+ await this.processManager.terminateProcess(processInfo.pid);
1785
+ const isFree = await this.processManager.verifyPortFree(port);
1786
+ if (!isFree) {
1787
+ throw new Error(`Dev server may still be running on port ${port}`);
1788
+ }
1789
+ return true;
1790
+ }
1791
+ /**
1792
+ * Delete a Git branch with safety checks
1793
+ *
1794
+ * @param branchName - Name of the branch to delete
1795
+ * @param options - Delete options (force, dryRun)
1796
+ * @param cwd - Working directory to execute git command from (defaults to finding main worktree)
1797
+ */
1798
+ async deleteBranch(branchName, options = {}, cwd) {
1799
+ const protectedBranches = await this.settingsManager.getProtectedBranches(cwd);
1800
+ if (protectedBranches.includes(branchName)) {
1801
+ throw new Error(`Cannot delete protected branch: ${branchName}`);
1802
+ }
1803
+ if (options.dryRun) {
1804
+ logger.info(`[DRY RUN] Would delete branch: ${branchName}`);
1805
+ return true;
1806
+ }
1807
+ try {
1808
+ let workingDir = cwd ?? await findMainWorktreePathWithSettings(void 0, this.settingsManager);
1809
+ const deleteFlag = options.force ? "-D" : "-d";
1810
+ await executeGitCommand(["branch", deleteFlag, branchName], {
1811
+ cwd: workingDir
1812
+ });
1813
+ logger.info(`Branch deleted: ${branchName}`);
1814
+ return true;
1815
+ } catch (error) {
1816
+ if (options.force) {
1817
+ throw error;
1818
+ }
1819
+ const errorMessage = error instanceof Error ? error.message : String(error);
1820
+ if (errorMessage.includes("not fully merged")) {
1821
+ throw new Error(
1822
+ `Cannot delete unmerged branch '${branchName}'. Use --force to delete anyway.`
1823
+ );
1824
+ }
1825
+ throw error;
1826
+ }
1827
+ }
1828
+ /**
1829
+ * Cleanup database branch
1830
+ * Gracefully handles missing DatabaseManager
1831
+ *
1832
+ * @deprecated This method is deprecated and should not be used for post-deletion cleanup.
1833
+ * Use the pre-fetch mechanism in cleanupWorktree() instead.
1834
+ * This method will fail if called after worktree deletion because
1835
+ * it attempts to read the .env file which has been deleted.
1836
+ *
1837
+ * @param branchName - Name of the branch to delete
1838
+ * @param worktreePath - Path to worktree (must still exist with .env file)
1839
+ */
1840
+ async cleanupDatabase(branchName, worktreePath) {
1841
+ if (!this.database) {
1842
+ logger.debug("Database manager not available, skipping database cleanup");
1843
+ return false;
1844
+ }
1845
+ try {
1846
+ const envFilePath = path4.join(worktreePath, ".env");
1847
+ const shouldCleanup = await this.database.shouldUseDatabaseBranching(envFilePath);
1848
+ let cwd;
1849
+ try {
1850
+ cwd = await findMainWorktreePathWithSettings(worktreePath, this.settingsManager);
1851
+ } catch (error) {
1852
+ logger.debug(
1853
+ `Could not find main worktree path, using current directory: ${error instanceof Error ? error.message : String(error)}`
1854
+ );
1855
+ }
1856
+ const result = await this.database.deleteBranchIfConfigured(
1857
+ branchName,
1858
+ shouldCleanup,
1859
+ false,
1860
+ // isPreview
1861
+ cwd
1862
+ );
1863
+ if (result.deleted) {
1864
+ logger.info(`Database branch deleted: ${branchName}`);
1865
+ return true;
1866
+ } else if (result.notFound) {
1867
+ logger.debug(`No database branch found for: ${branchName}`);
1868
+ return false;
1869
+ } else if (result.userDeclined) {
1870
+ logger.info("Preview database deletion declined by user");
1871
+ return false;
1872
+ } else if (!result.success) {
1873
+ logger.warn(`Database cleanup failed: ${result.error ?? "Unknown error"}`);
1874
+ return false;
1875
+ } else {
1876
+ logger.debug("Database deletion returned unexpected result");
1877
+ return false;
1878
+ }
1879
+ } catch (error) {
1880
+ logger.warn(
1881
+ `Unexpected database cleanup error: ${error instanceof Error ? error.message : String(error)}`
1882
+ );
1883
+ return false;
1884
+ }
1885
+ }
1886
+ /**
1887
+ * Cleanup multiple worktrees
1888
+ */
1889
+ async cleanupMultipleWorktrees(identifiers, options = {}) {
1890
+ const results = [];
1891
+ for (const identifier of identifiers) {
1892
+ const parsed = this.parseIdentifier(identifier);
1893
+ const result = await this.cleanupWorktree(parsed, options);
1894
+ results.push(result);
1895
+ }
1896
+ return results;
1897
+ }
1898
+ /**
1899
+ * Validate worktree safety given a worktree object
1900
+ * Private method used internally when worktree is already known
1901
+ */
1902
+ async validateWorktreeSafety(worktree, identifier) {
1903
+ const warnings = [];
1904
+ const blockers = [];
1905
+ const isMain = await this.gitWorktree.isMainWorktree(worktree, this.settingsManager);
1906
+ if (isMain) {
1907
+ blockers.push(`Cannot cleanup main worktree: "${worktree.branch}" @ "${worktree.path}"`);
1908
+ }
1909
+ const hasChanges = await hasUncommittedChanges(worktree.path);
1910
+ if (hasChanges) {
1911
+ const blockerMessage = `Worktree has uncommitted changes.
1912
+
1913
+ Please resolve before cleanup - you have some options:
1914
+ \u2022 Commit changes: cd ${worktree.path} && git commit -am "message"
1915
+ \u2022 Stash changes: cd ${worktree.path} && git stash
1916
+ \u2022 Force cleanup: il cleanup ${identifier} --force (WARNING: will discard changes)`;
1917
+ blockers.push(blockerMessage);
1918
+ }
1919
+ return {
1920
+ isSafe: blockers.length === 0,
1921
+ warnings,
1922
+ blockers
1923
+ };
1924
+ }
1925
+ /**
1926
+ * Validate cleanup safety
1927
+ */
1928
+ async validateCleanupSafety(identifier) {
1929
+ const warnings = [];
1930
+ const blockers = [];
1931
+ const worktrees = await this.gitWorktree.findWorktreesByIdentifier(identifier);
1932
+ if (worktrees.length === 0) {
1933
+ blockers.push(`No worktree found for: ${identifier}`);
1934
+ return { isSafe: false, warnings, blockers };
1935
+ }
1936
+ const worktree = worktrees[0];
1937
+ if (!worktree) {
1938
+ blockers.push(`No worktree found for: ${identifier}`);
1939
+ return { isSafe: false, warnings, blockers };
1940
+ }
1941
+ return await this.validateWorktreeSafety(worktree, identifier);
1942
+ }
1943
+ /**
1944
+ * Parse identifier to determine type and extract number
1945
+ * Helper method for port calculation
1946
+ */
1947
+ parseIdentifier(identifier) {
1948
+ const issueId = extractIssueNumber(identifier);
1949
+ if (issueId !== null) {
1950
+ return {
1951
+ type: "issue",
1952
+ number: issueId,
1953
+ originalInput: identifier
1954
+ };
1955
+ }
1956
+ const prMatch = identifier.match(/(?:pr|PR)[/-](\d+)/);
1957
+ if (prMatch == null ? void 0 : prMatch[1]) {
1958
+ return {
1959
+ type: "pr",
1960
+ number: parseInt(prMatch[1], 10),
1961
+ originalInput: identifier
1962
+ };
1963
+ }
1964
+ const numericMatch = identifier.match(/^#?(\d+)$/);
1965
+ if (numericMatch == null ? void 0 : numericMatch[1]) {
1966
+ return {
1967
+ type: "issue",
1968
+ number: parseInt(numericMatch[1], 10),
1969
+ originalInput: identifier
1970
+ };
1971
+ }
1972
+ return {
1973
+ type: "branch",
1974
+ branchName: identifier,
1975
+ originalInput: identifier
1976
+ };
1977
+ }
1978
+ };
1979
+
1980
+ export {
1981
+ LoomManager,
1982
+ EnvironmentManager,
1983
+ CLIIsolationManager,
1984
+ DatabaseManager,
1985
+ ResourceCleanup
1986
+ };
1987
+ //# sourceMappingURL=chunk-5EF7Z346.js.map