@iloom/cli 0.1.14
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +33 -0
- package/README.md +711 -0
- package/dist/ClaudeContextManager-XOSXQ67R.js +13 -0
- package/dist/ClaudeContextManager-XOSXQ67R.js.map +1 -0
- package/dist/ClaudeService-YSZ6EXWP.js +12 -0
- package/dist/ClaudeService-YSZ6EXWP.js.map +1 -0
- package/dist/GitHubService-F7Z3XJOS.js +11 -0
- package/dist/GitHubService-F7Z3XJOS.js.map +1 -0
- package/dist/LoomLauncher-MODG2SEM.js +263 -0
- package/dist/LoomLauncher-MODG2SEM.js.map +1 -0
- package/dist/NeonProvider-PAGPUH7F.js +12 -0
- package/dist/NeonProvider-PAGPUH7F.js.map +1 -0
- package/dist/PromptTemplateManager-7FINLRDE.js +9 -0
- package/dist/PromptTemplateManager-7FINLRDE.js.map +1 -0
- package/dist/SettingsManager-VAZF26S2.js +19 -0
- package/dist/SettingsManager-VAZF26S2.js.map +1 -0
- package/dist/SettingsMigrationManager-MTQIMI54.js +146 -0
- package/dist/SettingsMigrationManager-MTQIMI54.js.map +1 -0
- package/dist/add-issue-22JBNOML.js +54 -0
- package/dist/add-issue-22JBNOML.js.map +1 -0
- package/dist/agents/iloom-issue-analyze-and-plan.md +580 -0
- package/dist/agents/iloom-issue-analyzer.md +290 -0
- package/dist/agents/iloom-issue-complexity-evaluator.md +224 -0
- package/dist/agents/iloom-issue-enhancer.md +266 -0
- package/dist/agents/iloom-issue-implementer.md +262 -0
- package/dist/agents/iloom-issue-planner.md +358 -0
- package/dist/agents/iloom-issue-reviewer.md +63 -0
- package/dist/chunk-2ZPFJQ3B.js +63 -0
- package/dist/chunk-2ZPFJQ3B.js.map +1 -0
- package/dist/chunk-37DYYFVK.js +29 -0
- package/dist/chunk-37DYYFVK.js.map +1 -0
- package/dist/chunk-BLCTGFZN.js +121 -0
- package/dist/chunk-BLCTGFZN.js.map +1 -0
- package/dist/chunk-CP2NU2JC.js +545 -0
- package/dist/chunk-CP2NU2JC.js.map +1 -0
- package/dist/chunk-CWR2SANQ.js +39 -0
- package/dist/chunk-CWR2SANQ.js.map +1 -0
- package/dist/chunk-F3XBU2R7.js +110 -0
- package/dist/chunk-F3XBU2R7.js.map +1 -0
- package/dist/chunk-GEHQXLEI.js +130 -0
- package/dist/chunk-GEHQXLEI.js.map +1 -0
- package/dist/chunk-GYCR2LOU.js +143 -0
- package/dist/chunk-GYCR2LOU.js.map +1 -0
- package/dist/chunk-GZP4UGGM.js +48 -0
- package/dist/chunk-GZP4UGGM.js.map +1 -0
- package/dist/chunk-H4E4THUZ.js +55 -0
- package/dist/chunk-H4E4THUZ.js.map +1 -0
- package/dist/chunk-HPJJSYNS.js +644 -0
- package/dist/chunk-HPJJSYNS.js.map +1 -0
- package/dist/chunk-JBH2ZYYZ.js +220 -0
- package/dist/chunk-JBH2ZYYZ.js.map +1 -0
- package/dist/chunk-JNKJ7NJV.js +78 -0
- package/dist/chunk-JNKJ7NJV.js.map +1 -0
- package/dist/chunk-JQ7VOSTC.js +437 -0
- package/dist/chunk-JQ7VOSTC.js.map +1 -0
- package/dist/chunk-KQDEK2ZW.js +199 -0
- package/dist/chunk-KQDEK2ZW.js.map +1 -0
- package/dist/chunk-O2QWO64Z.js +179 -0
- package/dist/chunk-O2QWO64Z.js.map +1 -0
- package/dist/chunk-OC4H6HJD.js +248 -0
- package/dist/chunk-OC4H6HJD.js.map +1 -0
- package/dist/chunk-PR7FKQBG.js +120 -0
- package/dist/chunk-PR7FKQBG.js.map +1 -0
- package/dist/chunk-PXZBAC2M.js +250 -0
- package/dist/chunk-PXZBAC2M.js.map +1 -0
- package/dist/chunk-QEPVTTHD.js +383 -0
- package/dist/chunk-QEPVTTHD.js.map +1 -0
- package/dist/chunk-RSRO7564.js +203 -0
- package/dist/chunk-RSRO7564.js.map +1 -0
- package/dist/chunk-SJUQ2NDR.js +146 -0
- package/dist/chunk-SJUQ2NDR.js.map +1 -0
- package/dist/chunk-SPYPLHMK.js +177 -0
- package/dist/chunk-SPYPLHMK.js.map +1 -0
- package/dist/chunk-SSCQCCJ7.js +75 -0
- package/dist/chunk-SSCQCCJ7.js.map +1 -0
- package/dist/chunk-SSR5AVRJ.js +41 -0
- package/dist/chunk-SSR5AVRJ.js.map +1 -0
- package/dist/chunk-T7QPXANZ.js +315 -0
- package/dist/chunk-T7QPXANZ.js.map +1 -0
- package/dist/chunk-U3WU5OWO.js +203 -0
- package/dist/chunk-U3WU5OWO.js.map +1 -0
- package/dist/chunk-W3DQTW63.js +124 -0
- package/dist/chunk-W3DQTW63.js.map +1 -0
- package/dist/chunk-WKEWRSDB.js +151 -0
- package/dist/chunk-WKEWRSDB.js.map +1 -0
- package/dist/chunk-Y7SAGNUT.js +66 -0
- package/dist/chunk-Y7SAGNUT.js.map +1 -0
- package/dist/chunk-YETJNRQM.js +39 -0
- package/dist/chunk-YETJNRQM.js.map +1 -0
- package/dist/chunk-YYSKGAZT.js +384 -0
- package/dist/chunk-YYSKGAZT.js.map +1 -0
- package/dist/chunk-ZZZWQGTS.js +169 -0
- package/dist/chunk-ZZZWQGTS.js.map +1 -0
- package/dist/claude-7LUVDZZ4.js +17 -0
- package/dist/claude-7LUVDZZ4.js.map +1 -0
- package/dist/cleanup-3LUWPSM7.js +412 -0
- package/dist/cleanup-3LUWPSM7.js.map +1 -0
- package/dist/cli-overrides-XFZWY7CM.js +16 -0
- package/dist/cli-overrides-XFZWY7CM.js.map +1 -0
- package/dist/cli.js +603 -0
- package/dist/cli.js.map +1 -0
- package/dist/color-ZVALX37U.js +21 -0
- package/dist/color-ZVALX37U.js.map +1 -0
- package/dist/enhance-XJIQHVPD.js +166 -0
- package/dist/enhance-XJIQHVPD.js.map +1 -0
- package/dist/env-MDFL4ZXL.js +23 -0
- package/dist/env-MDFL4ZXL.js.map +1 -0
- package/dist/feedback-23CLXKFT.js +158 -0
- package/dist/feedback-23CLXKFT.js.map +1 -0
- package/dist/finish-CY4CIH6O.js +1608 -0
- package/dist/finish-CY4CIH6O.js.map +1 -0
- package/dist/git-LVRZ57GJ.js +43 -0
- package/dist/git-LVRZ57GJ.js.map +1 -0
- package/dist/ignite-WXEF2ID5.js +359 -0
- package/dist/ignite-WXEF2ID5.js.map +1 -0
- package/dist/index.d.ts +1341 -0
- package/dist/index.js +3058 -0
- package/dist/index.js.map +1 -0
- package/dist/init-RHACUR4E.js +123 -0
- package/dist/init-RHACUR4E.js.map +1 -0
- package/dist/installation-detector-VARGFFRZ.js +11 -0
- package/dist/installation-detector-VARGFFRZ.js.map +1 -0
- package/dist/logger-MKYH4UDV.js +12 -0
- package/dist/logger-MKYH4UDV.js.map +1 -0
- package/dist/mcp/chunk-6SDFJ42P.js +62 -0
- package/dist/mcp/chunk-6SDFJ42P.js.map +1 -0
- package/dist/mcp/claude-YHHHLSXH.js +249 -0
- package/dist/mcp/claude-YHHHLSXH.js.map +1 -0
- package/dist/mcp/color-QS5BFCNN.js +168 -0
- package/dist/mcp/color-QS5BFCNN.js.map +1 -0
- package/dist/mcp/github-comment-server.js +165 -0
- package/dist/mcp/github-comment-server.js.map +1 -0
- package/dist/mcp/terminal-SDCMDVD7.js +202 -0
- package/dist/mcp/terminal-SDCMDVD7.js.map +1 -0
- package/dist/open-X6BTENPV.js +278 -0
- package/dist/open-X6BTENPV.js.map +1 -0
- package/dist/prompt-ANTQWHUF.js +13 -0
- package/dist/prompt-ANTQWHUF.js.map +1 -0
- package/dist/prompts/issue-prompt.txt +230 -0
- package/dist/prompts/pr-prompt.txt +35 -0
- package/dist/prompts/regular-prompt.txt +14 -0
- package/dist/run-2JCPQAX3.js +278 -0
- package/dist/run-2JCPQAX3.js.map +1 -0
- package/dist/schema/settings.schema.json +221 -0
- package/dist/start-LWVRBJ6S.js +982 -0
- package/dist/start-LWVRBJ6S.js.map +1 -0
- package/dist/terminal-3D6TUAKJ.js +16 -0
- package/dist/terminal-3D6TUAKJ.js.map +1 -0
- package/dist/test-git-XPF4SZXJ.js +52 -0
- package/dist/test-git-XPF4SZXJ.js.map +1 -0
- package/dist/test-prefix-XGFXFAYN.js +68 -0
- package/dist/test-prefix-XGFXFAYN.js.map +1 -0
- package/dist/test-tabs-JRKY3QMM.js +69 -0
- package/dist/test-tabs-JRKY3QMM.js.map +1 -0
- package/dist/test-webserver-M2I3EV4J.js +62 -0
- package/dist/test-webserver-M2I3EV4J.js.map +1 -0
- package/dist/update-3ZT2XX2G.js +79 -0
- package/dist/update-3ZT2XX2G.js.map +1 -0
- package/dist/update-notifier-QSSEB5KC.js +11 -0
- package/dist/update-notifier-QSSEB5KC.js.map +1 -0
- package/package.json +113 -0
|
@@ -0,0 +1,644 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import {
|
|
3
|
+
calculatePortForBranch
|
|
4
|
+
} from "./chunk-37DYYFVK.js";
|
|
5
|
+
import {
|
|
6
|
+
runScript
|
|
7
|
+
} from "./chunk-BLCTGFZN.js";
|
|
8
|
+
import {
|
|
9
|
+
hasScript,
|
|
10
|
+
readPackageJson
|
|
11
|
+
} from "./chunk-2ZPFJQ3B.js";
|
|
12
|
+
import {
|
|
13
|
+
formatEnvLine,
|
|
14
|
+
parseEnvFile,
|
|
15
|
+
validateEnvVariable
|
|
16
|
+
} from "./chunk-SJUQ2NDR.js";
|
|
17
|
+
import {
|
|
18
|
+
createLogger,
|
|
19
|
+
logger
|
|
20
|
+
} from "./chunk-GEHQXLEI.js";
|
|
21
|
+
|
|
22
|
+
// src/lib/EnvironmentManager.ts
|
|
23
|
+
import fs from "fs-extra";
|
|
24
|
+
var logger2 = createLogger({ prefix: "\u{1F4DD}" });
|
|
25
|
+
var EnvironmentManager = class {
|
|
26
|
+
constructor() {
|
|
27
|
+
this.backupSuffix = ".backup";
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* Set or update an environment variable in a .env file
|
|
31
|
+
* Ports functionality from bash/utils/env-utils.sh:setEnvVar()
|
|
32
|
+
* @returns The backup path if a backup was created
|
|
33
|
+
*/
|
|
34
|
+
async setEnvVar(filePath, key, value, backup = false) {
|
|
35
|
+
const validation = validateEnvVariable(key, value);
|
|
36
|
+
if (!validation.valid) {
|
|
37
|
+
throw new Error(validation.error ?? "Invalid variable name");
|
|
38
|
+
}
|
|
39
|
+
const fileExists = await fs.pathExists(filePath);
|
|
40
|
+
if (!fileExists) {
|
|
41
|
+
logger2.info(`Creating ${filePath} with ${key}...`);
|
|
42
|
+
const content = formatEnvLine(key, value);
|
|
43
|
+
await fs.writeFile(filePath, content, "utf8");
|
|
44
|
+
logger2.success(`${filePath} created with ${key}`);
|
|
45
|
+
return;
|
|
46
|
+
}
|
|
47
|
+
const existingContent = await fs.readFile(filePath, "utf8");
|
|
48
|
+
const envMap = parseEnvFile(existingContent);
|
|
49
|
+
let backupPath;
|
|
50
|
+
if (backup) {
|
|
51
|
+
backupPath = await this.createBackup(filePath);
|
|
52
|
+
}
|
|
53
|
+
envMap.set(key, value);
|
|
54
|
+
const lines = existingContent.split("\n");
|
|
55
|
+
const newLines = [];
|
|
56
|
+
let variableUpdated = false;
|
|
57
|
+
for (const line of lines) {
|
|
58
|
+
const trimmedLine = line.trim();
|
|
59
|
+
if (!trimmedLine || trimmedLine.startsWith("#")) {
|
|
60
|
+
newLines.push(line);
|
|
61
|
+
continue;
|
|
62
|
+
}
|
|
63
|
+
const cleanLine = trimmedLine.startsWith("export ") ? trimmedLine.substring(7) : trimmedLine;
|
|
64
|
+
const equalsIndex = cleanLine.indexOf("=");
|
|
65
|
+
if (equalsIndex !== -1) {
|
|
66
|
+
const lineKey = cleanLine.substring(0, equalsIndex).trim();
|
|
67
|
+
if (lineKey === key) {
|
|
68
|
+
newLines.push(formatEnvLine(key, value));
|
|
69
|
+
variableUpdated = true;
|
|
70
|
+
continue;
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
newLines.push(line);
|
|
74
|
+
}
|
|
75
|
+
if (!variableUpdated) {
|
|
76
|
+
logger2.info(`Adding ${key} to ${filePath}...`);
|
|
77
|
+
newLines.push(formatEnvLine(key, value));
|
|
78
|
+
logger2.success(`${key} added successfully`);
|
|
79
|
+
} else {
|
|
80
|
+
logger2.info(`Updating ${key} in ${filePath}...`);
|
|
81
|
+
logger2.success(`${key} updated successfully`);
|
|
82
|
+
}
|
|
83
|
+
const newContent = newLines.join("\n");
|
|
84
|
+
await fs.writeFile(filePath, newContent, "utf8");
|
|
85
|
+
return backupPath;
|
|
86
|
+
}
|
|
87
|
+
/**
|
|
88
|
+
* Read and parse a .env file
|
|
89
|
+
*/
|
|
90
|
+
async readEnvFile(filePath) {
|
|
91
|
+
try {
|
|
92
|
+
const content = await fs.readFile(filePath, "utf8");
|
|
93
|
+
return parseEnvFile(content);
|
|
94
|
+
} catch (error) {
|
|
95
|
+
logger2.debug(
|
|
96
|
+
`Could not read env file ${filePath}: ${error instanceof Error ? error.message : String(error)}`
|
|
97
|
+
);
|
|
98
|
+
return /* @__PURE__ */ new Map();
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
/**
|
|
102
|
+
* Generic file copy helper that only copies if source exists
|
|
103
|
+
* Does not throw if source file doesn't exist - just logs and returns
|
|
104
|
+
* @private
|
|
105
|
+
*/
|
|
106
|
+
async copyIfExists(source, destination) {
|
|
107
|
+
const sourceExists = await fs.pathExists(source);
|
|
108
|
+
if (!sourceExists) {
|
|
109
|
+
logger2.debug(`Source file ${source} does not exist, skipping copy`);
|
|
110
|
+
return;
|
|
111
|
+
}
|
|
112
|
+
await fs.copy(source, destination, { overwrite: false });
|
|
113
|
+
logger2.success(`Copied ${source} to ${destination}`);
|
|
114
|
+
}
|
|
115
|
+
/**
|
|
116
|
+
* Calculate unique port for workspace
|
|
117
|
+
* Implements:
|
|
118
|
+
* - Issue/PR: 3000 + issue/PR number
|
|
119
|
+
* - Branch: 3000 + deterministic hash offset (1-999)
|
|
120
|
+
*/
|
|
121
|
+
calculatePort(options) {
|
|
122
|
+
const basePort = options.basePort ?? 3e3;
|
|
123
|
+
if (options.issueNumber !== void 0) {
|
|
124
|
+
const port = basePort + options.issueNumber;
|
|
125
|
+
if (port > 65535) {
|
|
126
|
+
throw new Error(
|
|
127
|
+
`Calculated port ${port} exceeds maximum (65535). Use a lower base port or issue number.`
|
|
128
|
+
);
|
|
129
|
+
}
|
|
130
|
+
return port;
|
|
131
|
+
}
|
|
132
|
+
if (options.prNumber !== void 0) {
|
|
133
|
+
const port = basePort + options.prNumber;
|
|
134
|
+
if (port > 65535) {
|
|
135
|
+
throw new Error(
|
|
136
|
+
`Calculated port ${port} exceeds maximum (65535). Use a lower base port or PR number.`
|
|
137
|
+
);
|
|
138
|
+
}
|
|
139
|
+
return port;
|
|
140
|
+
}
|
|
141
|
+
if (options.branchName !== void 0) {
|
|
142
|
+
return calculatePortForBranch(options.branchName, basePort);
|
|
143
|
+
}
|
|
144
|
+
return basePort;
|
|
145
|
+
}
|
|
146
|
+
/**
|
|
147
|
+
* Set port environment variable for workspace
|
|
148
|
+
*/
|
|
149
|
+
async setPortForWorkspace(envFilePath, issueNumber, prNumber, branchName) {
|
|
150
|
+
const options = {};
|
|
151
|
+
if (issueNumber !== void 0) {
|
|
152
|
+
options.issueNumber = issueNumber;
|
|
153
|
+
}
|
|
154
|
+
if (prNumber !== void 0) {
|
|
155
|
+
options.prNumber = prNumber;
|
|
156
|
+
}
|
|
157
|
+
if (branchName !== void 0) {
|
|
158
|
+
options.branchName = branchName;
|
|
159
|
+
}
|
|
160
|
+
const port = this.calculatePort(options);
|
|
161
|
+
await this.setEnvVar(envFilePath, "PORT", String(port));
|
|
162
|
+
return port;
|
|
163
|
+
}
|
|
164
|
+
/**
|
|
165
|
+
* Validate environment configuration
|
|
166
|
+
*/
|
|
167
|
+
async validateEnvFile(filePath) {
|
|
168
|
+
try {
|
|
169
|
+
const content = await fs.readFile(filePath, "utf8");
|
|
170
|
+
const envMap = parseEnvFile(content);
|
|
171
|
+
const errors = [];
|
|
172
|
+
for (const [key, value] of envMap.entries()) {
|
|
173
|
+
const validation = validateEnvVariable(key, value);
|
|
174
|
+
if (!validation.valid) {
|
|
175
|
+
errors.push(`${key}: ${validation.error}`);
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
return {
|
|
179
|
+
valid: errors.length === 0,
|
|
180
|
+
errors
|
|
181
|
+
};
|
|
182
|
+
} catch (error) {
|
|
183
|
+
return {
|
|
184
|
+
valid: false,
|
|
185
|
+
errors: [
|
|
186
|
+
`Failed to read or parse file: ${error instanceof Error ? error.message : String(error)}`
|
|
187
|
+
]
|
|
188
|
+
};
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
/**
|
|
192
|
+
* Create backup of existing file
|
|
193
|
+
*/
|
|
194
|
+
async createBackup(filePath) {
|
|
195
|
+
const timestamp = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-");
|
|
196
|
+
const backupPath = `${filePath}${this.backupSuffix}-${timestamp}`;
|
|
197
|
+
await fs.copy(filePath, backupPath);
|
|
198
|
+
logger2.debug(`Created backup at ${backupPath}`);
|
|
199
|
+
return backupPath;
|
|
200
|
+
}
|
|
201
|
+
};
|
|
202
|
+
|
|
203
|
+
// src/lib/CLIIsolationManager.ts
|
|
204
|
+
import fs2 from "fs-extra";
|
|
205
|
+
import path from "path";
|
|
206
|
+
import os from "os";
|
|
207
|
+
var CLIIsolationManager = class {
|
|
208
|
+
constructor() {
|
|
209
|
+
this.iloomBinDir = path.join(os.homedir(), ".iloom", "bin");
|
|
210
|
+
}
|
|
211
|
+
/**
|
|
212
|
+
* Setup CLI isolation for a worktree
|
|
213
|
+
* - Build the project
|
|
214
|
+
* - Create versioned symlinks
|
|
215
|
+
* - Check PATH configuration
|
|
216
|
+
* @param worktreePath Path to the worktree
|
|
217
|
+
* @param identifier Issue/PR number or branch identifier
|
|
218
|
+
* @param binEntries Bin entries from package.json
|
|
219
|
+
* @returns Array of created symlink names
|
|
220
|
+
*/
|
|
221
|
+
async setupCLIIsolation(worktreePath, identifier, binEntries) {
|
|
222
|
+
await this.buildProject(worktreePath);
|
|
223
|
+
await this.verifyBinTargets(worktreePath, binEntries);
|
|
224
|
+
await fs2.ensureDir(this.iloomBinDir);
|
|
225
|
+
const symlinkNames = await this.createVersionedSymlinks(
|
|
226
|
+
worktreePath,
|
|
227
|
+
identifier,
|
|
228
|
+
binEntries
|
|
229
|
+
);
|
|
230
|
+
await this.ensureIloomBinInPath();
|
|
231
|
+
return symlinkNames;
|
|
232
|
+
}
|
|
233
|
+
/**
|
|
234
|
+
* Build the project using package.json build script
|
|
235
|
+
* @param worktreePath Path to the worktree
|
|
236
|
+
*/
|
|
237
|
+
async buildProject(worktreePath) {
|
|
238
|
+
const pkgJson = await readPackageJson(worktreePath);
|
|
239
|
+
if (!hasScript(pkgJson, "build")) {
|
|
240
|
+
logger.warn("No build script found in package.json - skipping build");
|
|
241
|
+
return;
|
|
242
|
+
}
|
|
243
|
+
logger.info("Building CLI tool...");
|
|
244
|
+
await runScript("build", worktreePath);
|
|
245
|
+
logger.success("Build completed");
|
|
246
|
+
}
|
|
247
|
+
/**
|
|
248
|
+
* Verify bin targets exist and are executable
|
|
249
|
+
* @param worktreePath Path to the worktree
|
|
250
|
+
* @param binEntries Bin entries from package.json
|
|
251
|
+
*/
|
|
252
|
+
async verifyBinTargets(worktreePath, binEntries) {
|
|
253
|
+
for (const binPath of Object.values(binEntries)) {
|
|
254
|
+
const targetPath = path.resolve(worktreePath, binPath);
|
|
255
|
+
const exists = await fs2.pathExists(targetPath);
|
|
256
|
+
if (!exists) {
|
|
257
|
+
throw new Error(`Bin target does not exist: ${targetPath}`);
|
|
258
|
+
}
|
|
259
|
+
try {
|
|
260
|
+
await fs2.access(targetPath, fs2.constants.X_OK);
|
|
261
|
+
} catch {
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
/**
|
|
266
|
+
* Create versioned symlinks in ~/.iloom/bin
|
|
267
|
+
* @param worktreePath Path to the worktree
|
|
268
|
+
* @param identifier Issue/PR number or branch identifier
|
|
269
|
+
* @param binEntries Bin entries from package.json
|
|
270
|
+
* @returns Array of created symlink names
|
|
271
|
+
*/
|
|
272
|
+
async createVersionedSymlinks(worktreePath, identifier, binEntries) {
|
|
273
|
+
const symlinkNames = [];
|
|
274
|
+
for (const [binName, binPath] of Object.entries(binEntries)) {
|
|
275
|
+
const versionedName = `${binName}-${identifier}`;
|
|
276
|
+
const targetPath = path.resolve(worktreePath, binPath);
|
|
277
|
+
const symlinkPath = path.join(this.iloomBinDir, versionedName);
|
|
278
|
+
await fs2.symlink(targetPath, symlinkPath);
|
|
279
|
+
logger.success(`CLI available: ${versionedName}`);
|
|
280
|
+
symlinkNames.push(versionedName);
|
|
281
|
+
}
|
|
282
|
+
return symlinkNames;
|
|
283
|
+
}
|
|
284
|
+
/**
|
|
285
|
+
* Check if ~/.iloom/bin is in PATH and provide setup instructions
|
|
286
|
+
*/
|
|
287
|
+
async ensureIloomBinInPath() {
|
|
288
|
+
const currentPath = process.env.PATH ?? "";
|
|
289
|
+
if (currentPath.includes(".iloom/bin")) {
|
|
290
|
+
return;
|
|
291
|
+
}
|
|
292
|
+
const shell = this.detectShell();
|
|
293
|
+
const rcFile = this.getShellRcFile(shell);
|
|
294
|
+
logger.warn("\n\u26A0\uFE0F One-time PATH setup required:");
|
|
295
|
+
logger.warn(` Add to ${rcFile}:`);
|
|
296
|
+
logger.warn(` export PATH="$HOME/.iloom/bin:$PATH"`);
|
|
297
|
+
logger.warn(` Then run: source ${rcFile}
|
|
298
|
+
`);
|
|
299
|
+
}
|
|
300
|
+
/**
|
|
301
|
+
* Detect current shell
|
|
302
|
+
* @returns Shell name (zsh, bash, fish, etc.)
|
|
303
|
+
*/
|
|
304
|
+
detectShell() {
|
|
305
|
+
const shell = process.env.SHELL ?? "";
|
|
306
|
+
return shell.split("/").pop() ?? "bash";
|
|
307
|
+
}
|
|
308
|
+
/**
|
|
309
|
+
* Get RC file path for shell
|
|
310
|
+
* @param shell Shell name
|
|
311
|
+
* @returns RC file path
|
|
312
|
+
*/
|
|
313
|
+
getShellRcFile(shell) {
|
|
314
|
+
const rcFiles = {
|
|
315
|
+
zsh: "~/.zshrc",
|
|
316
|
+
bash: "~/.bashrc",
|
|
317
|
+
fish: "~/.config/fish/config.fish"
|
|
318
|
+
};
|
|
319
|
+
return rcFiles[shell] ?? "~/.bashrc";
|
|
320
|
+
}
|
|
321
|
+
/**
|
|
322
|
+
* Cleanup versioned CLI executables for a specific identifier
|
|
323
|
+
* Removes all symlinks matching the pattern: {binName}-{identifier}
|
|
324
|
+
*
|
|
325
|
+
* @param identifier - Issue/PR number or branch identifier
|
|
326
|
+
* @returns Array of removed symlink names
|
|
327
|
+
*/
|
|
328
|
+
async cleanupVersionedExecutables(identifier) {
|
|
329
|
+
const removed = [];
|
|
330
|
+
try {
|
|
331
|
+
const files = await fs2.readdir(this.iloomBinDir);
|
|
332
|
+
for (const file of files) {
|
|
333
|
+
if (this.matchesIdentifier(file, identifier)) {
|
|
334
|
+
const symlinkPath = path.join(this.iloomBinDir, file);
|
|
335
|
+
try {
|
|
336
|
+
await fs2.unlink(symlinkPath);
|
|
337
|
+
removed.push(file);
|
|
338
|
+
} catch (error) {
|
|
339
|
+
const isEnoent = error && typeof error === "object" && "code" in error && error.code === "ENOENT";
|
|
340
|
+
if (isEnoent) {
|
|
341
|
+
removed.push(file);
|
|
342
|
+
continue;
|
|
343
|
+
}
|
|
344
|
+
logger.warn(
|
|
345
|
+
`Failed to remove symlink ${file}: ${error instanceof Error ? error.message : "Unknown error"}`
|
|
346
|
+
);
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
} catch (error) {
|
|
351
|
+
const isEnoent = error && typeof error === "object" && "code" in error && error.code === "ENOENT";
|
|
352
|
+
if (isEnoent) {
|
|
353
|
+
logger.warn("No CLI executables directory found - nothing to cleanup");
|
|
354
|
+
return [];
|
|
355
|
+
}
|
|
356
|
+
throw error;
|
|
357
|
+
}
|
|
358
|
+
if (removed.length > 0) {
|
|
359
|
+
logger.success(`Removed CLI executables: ${removed.join(", ")}`);
|
|
360
|
+
}
|
|
361
|
+
return removed;
|
|
362
|
+
}
|
|
363
|
+
/**
|
|
364
|
+
* Find orphaned symlinks in ~/.iloom/bin
|
|
365
|
+
* Returns symlinks that point to non-existent targets
|
|
366
|
+
*
|
|
367
|
+
* @returns Array of orphaned symlink information
|
|
368
|
+
*/
|
|
369
|
+
async findOrphanedSymlinks() {
|
|
370
|
+
const orphaned = [];
|
|
371
|
+
try {
|
|
372
|
+
const files = await fs2.readdir(this.iloomBinDir);
|
|
373
|
+
for (const file of files) {
|
|
374
|
+
const symlinkPath = path.join(this.iloomBinDir, file);
|
|
375
|
+
try {
|
|
376
|
+
const stats = await fs2.lstat(symlinkPath);
|
|
377
|
+
if (stats.isSymbolicLink()) {
|
|
378
|
+
const target = await fs2.readlink(symlinkPath);
|
|
379
|
+
try {
|
|
380
|
+
await fs2.access(target);
|
|
381
|
+
} catch {
|
|
382
|
+
orphaned.push({
|
|
383
|
+
name: file,
|
|
384
|
+
path: symlinkPath,
|
|
385
|
+
brokenTarget: target
|
|
386
|
+
});
|
|
387
|
+
}
|
|
388
|
+
}
|
|
389
|
+
} catch (error) {
|
|
390
|
+
logger.warn(
|
|
391
|
+
`Failed to check symlink ${file}: ${error instanceof Error ? error.message : "Unknown error"}`
|
|
392
|
+
);
|
|
393
|
+
}
|
|
394
|
+
}
|
|
395
|
+
} catch (error) {
|
|
396
|
+
const isEnoent = error && typeof error === "object" && "code" in error && error.code === "ENOENT";
|
|
397
|
+
if (isEnoent) {
|
|
398
|
+
return [];
|
|
399
|
+
}
|
|
400
|
+
throw error;
|
|
401
|
+
}
|
|
402
|
+
return orphaned;
|
|
403
|
+
}
|
|
404
|
+
/**
|
|
405
|
+
* Cleanup all orphaned symlinks
|
|
406
|
+
* Removes symlinks that point to non-existent targets
|
|
407
|
+
*
|
|
408
|
+
* @returns Number of symlinks removed
|
|
409
|
+
*/
|
|
410
|
+
async cleanupOrphanedSymlinks() {
|
|
411
|
+
const orphaned = await this.findOrphanedSymlinks();
|
|
412
|
+
let removedCount = 0;
|
|
413
|
+
for (const symlink of orphaned) {
|
|
414
|
+
try {
|
|
415
|
+
await fs2.unlink(symlink.path);
|
|
416
|
+
removedCount++;
|
|
417
|
+
logger.success(`Removed orphaned symlink: ${symlink.name}`);
|
|
418
|
+
} catch (error) {
|
|
419
|
+
logger.warn(
|
|
420
|
+
`Failed to remove orphaned symlink ${symlink.name}: ${error instanceof Error ? error.message : "Unknown error"}`
|
|
421
|
+
);
|
|
422
|
+
}
|
|
423
|
+
}
|
|
424
|
+
return removedCount;
|
|
425
|
+
}
|
|
426
|
+
/**
|
|
427
|
+
* Check if a filename matches the versioned pattern for an identifier
|
|
428
|
+
* Pattern: {binName}-{identifier}
|
|
429
|
+
*
|
|
430
|
+
* @param fileName - Name of the file to check
|
|
431
|
+
* @param identifier - Issue/PR number or branch identifier
|
|
432
|
+
* @returns True if the filename matches the pattern
|
|
433
|
+
*/
|
|
434
|
+
matchesIdentifier(fileName, identifier) {
|
|
435
|
+
const suffix = `-${identifier}`;
|
|
436
|
+
return fileName.endsWith(suffix);
|
|
437
|
+
}
|
|
438
|
+
};
|
|
439
|
+
|
|
440
|
+
// src/lib/DatabaseManager.ts
|
|
441
|
+
var logger3 = createLogger({ prefix: "\u{1F5C2}\uFE0F" });
|
|
442
|
+
var DatabaseManager = class {
|
|
443
|
+
constructor(provider, environment, databaseUrlEnvVarName = "DATABASE_URL") {
|
|
444
|
+
this.provider = provider;
|
|
445
|
+
this.environment = environment;
|
|
446
|
+
this.databaseUrlEnvVarName = databaseUrlEnvVarName;
|
|
447
|
+
if (databaseUrlEnvVarName !== "DATABASE_URL") {
|
|
448
|
+
logger3.debug(`\u{1F527} DatabaseManager configured with custom variable: ${databaseUrlEnvVarName}`);
|
|
449
|
+
} else {
|
|
450
|
+
logger3.debug("\u{1F527} DatabaseManager using default variable: DATABASE_URL");
|
|
451
|
+
}
|
|
452
|
+
}
|
|
453
|
+
/**
|
|
454
|
+
* Get the configured database URL environment variable name
|
|
455
|
+
*/
|
|
456
|
+
getConfiguredVariableName() {
|
|
457
|
+
return this.databaseUrlEnvVarName;
|
|
458
|
+
}
|
|
459
|
+
/**
|
|
460
|
+
* Check if database branching should be used
|
|
461
|
+
* Requires BOTH conditions:
|
|
462
|
+
* 1. Database provider is properly configured (checked via provider.isConfigured())
|
|
463
|
+
* 2. .env file contains the configured database URL variable
|
|
464
|
+
*/
|
|
465
|
+
async shouldUseDatabaseBranching(envFilePath) {
|
|
466
|
+
if (!this.provider.isConfigured()) {
|
|
467
|
+
logger3.debug("Skipping database branching: Database provider not configured");
|
|
468
|
+
return false;
|
|
469
|
+
}
|
|
470
|
+
const hasDatabaseUrl = await this.hasDatabaseUrlInEnv(envFilePath);
|
|
471
|
+
if (!hasDatabaseUrl) {
|
|
472
|
+
logger3.debug(
|
|
473
|
+
"Skipping database branching: configured database URL variable not found in .env file"
|
|
474
|
+
);
|
|
475
|
+
return false;
|
|
476
|
+
}
|
|
477
|
+
return true;
|
|
478
|
+
}
|
|
479
|
+
/**
|
|
480
|
+
* Create database branch only if configured
|
|
481
|
+
* Returns connection string if branch was created, null if skipped
|
|
482
|
+
*
|
|
483
|
+
* @param branchName - Name of the branch to create
|
|
484
|
+
* @param envFilePath - Path to .env file for configuration checks
|
|
485
|
+
* @param cwd - Optional working directory to run commands from
|
|
486
|
+
*/
|
|
487
|
+
async createBranchIfConfigured(branchName, envFilePath, cwd) {
|
|
488
|
+
if (!await this.shouldUseDatabaseBranching(envFilePath)) {
|
|
489
|
+
return null;
|
|
490
|
+
}
|
|
491
|
+
if (!await this.provider.isCliAvailable()) {
|
|
492
|
+
logger3.warn("Skipping database branch creation: Neon CLI not available");
|
|
493
|
+
logger3.warn("Install with: npm install -g neonctl");
|
|
494
|
+
return null;
|
|
495
|
+
}
|
|
496
|
+
try {
|
|
497
|
+
const isAuth = await this.provider.isAuthenticated(cwd);
|
|
498
|
+
if (!isAuth) {
|
|
499
|
+
logger3.warn("Skipping database branch creation: Not authenticated with Neon CLI");
|
|
500
|
+
logger3.warn("Run: neon auth");
|
|
501
|
+
return null;
|
|
502
|
+
}
|
|
503
|
+
} catch (error) {
|
|
504
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
505
|
+
logger3.error(`Database authentication check failed: ${errorMessage}`);
|
|
506
|
+
throw error;
|
|
507
|
+
}
|
|
508
|
+
try {
|
|
509
|
+
const connectionString = await this.provider.createBranch(branchName, void 0, cwd);
|
|
510
|
+
logger3.success(`Database branch ready: ${this.provider.sanitizeBranchName(branchName)}`);
|
|
511
|
+
return connectionString;
|
|
512
|
+
} catch (error) {
|
|
513
|
+
logger3.error(
|
|
514
|
+
`Failed to create database branch: ${error instanceof Error ? error.message : String(error)}`
|
|
515
|
+
);
|
|
516
|
+
throw error;
|
|
517
|
+
}
|
|
518
|
+
}
|
|
519
|
+
/**
|
|
520
|
+
* Delete database branch only if configured
|
|
521
|
+
* Returns result object indicating what happened
|
|
522
|
+
*
|
|
523
|
+
* @param branchName - Name of the branch to delete
|
|
524
|
+
* @param shouldCleanup - Boolean indicating if database cleanup should be performed (pre-fetched config)
|
|
525
|
+
* @param isPreview - Whether this is a preview database branch
|
|
526
|
+
* @param cwd - Optional working directory to run commands from (prevents issues with deleted directories)
|
|
527
|
+
*/
|
|
528
|
+
async deleteBranchIfConfigured(branchName, shouldCleanup, isPreview = false, cwd) {
|
|
529
|
+
if (shouldCleanup === false) {
|
|
530
|
+
return {
|
|
531
|
+
success: true,
|
|
532
|
+
deleted: false,
|
|
533
|
+
notFound: true,
|
|
534
|
+
// Treat "not configured" as "nothing to delete"
|
|
535
|
+
branchName
|
|
536
|
+
};
|
|
537
|
+
}
|
|
538
|
+
if (!this.provider.isConfigured()) {
|
|
539
|
+
logger3.debug("Skipping database branch deletion: Database provider not configured");
|
|
540
|
+
return {
|
|
541
|
+
success: true,
|
|
542
|
+
deleted: false,
|
|
543
|
+
notFound: true,
|
|
544
|
+
branchName
|
|
545
|
+
};
|
|
546
|
+
}
|
|
547
|
+
if (!await this.provider.isCliAvailable()) {
|
|
548
|
+
logger3.info("Skipping database branch deletion: CLI tool not available");
|
|
549
|
+
return {
|
|
550
|
+
success: false,
|
|
551
|
+
deleted: false,
|
|
552
|
+
notFound: true,
|
|
553
|
+
error: "CLI tool not available",
|
|
554
|
+
branchName
|
|
555
|
+
};
|
|
556
|
+
}
|
|
557
|
+
try {
|
|
558
|
+
const isAuth = await this.provider.isAuthenticated(cwd);
|
|
559
|
+
if (!isAuth) {
|
|
560
|
+
logger3.warn("Skipping database branch deletion: Not authenticated with DB Provider");
|
|
561
|
+
return {
|
|
562
|
+
success: false,
|
|
563
|
+
deleted: false,
|
|
564
|
+
notFound: false,
|
|
565
|
+
error: "Not authenticated with DB Provider",
|
|
566
|
+
branchName
|
|
567
|
+
};
|
|
568
|
+
}
|
|
569
|
+
} catch (error) {
|
|
570
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
571
|
+
logger3.error(`Database authentication check failed: ${errorMessage}`);
|
|
572
|
+
return {
|
|
573
|
+
success: false,
|
|
574
|
+
deleted: false,
|
|
575
|
+
notFound: false,
|
|
576
|
+
error: `Authentication check failed: ${errorMessage}`,
|
|
577
|
+
branchName
|
|
578
|
+
};
|
|
579
|
+
}
|
|
580
|
+
try {
|
|
581
|
+
const result = await this.provider.deleteBranch(branchName, isPreview, cwd);
|
|
582
|
+
return result;
|
|
583
|
+
} catch (error) {
|
|
584
|
+
logger3.warn(
|
|
585
|
+
`Unexpected error in database deletion: ${error instanceof Error ? error.message : String(error)}`
|
|
586
|
+
);
|
|
587
|
+
return {
|
|
588
|
+
success: false,
|
|
589
|
+
deleted: false,
|
|
590
|
+
notFound: false,
|
|
591
|
+
error: error instanceof Error ? error.message : String(error),
|
|
592
|
+
branchName
|
|
593
|
+
};
|
|
594
|
+
}
|
|
595
|
+
}
|
|
596
|
+
/**
|
|
597
|
+
* Check if .env has the configured database URL variable
|
|
598
|
+
* CRITICAL: If user explicitly configured a custom variable name (not default),
|
|
599
|
+
* throw an error if it's missing from .env
|
|
600
|
+
*/
|
|
601
|
+
async hasDatabaseUrlInEnv(envFilePath) {
|
|
602
|
+
try {
|
|
603
|
+
const envMap = await this.environment.readEnvFile(envFilePath);
|
|
604
|
+
if (this.databaseUrlEnvVarName !== "DATABASE_URL") {
|
|
605
|
+
logger3.debug(`Looking for custom database URL variable: ${this.databaseUrlEnvVarName}`);
|
|
606
|
+
} else {
|
|
607
|
+
logger3.debug("Looking for default database URL variable: DATABASE_URL");
|
|
608
|
+
}
|
|
609
|
+
if (envMap.has(this.databaseUrlEnvVarName)) {
|
|
610
|
+
if (this.databaseUrlEnvVarName !== "DATABASE_URL") {
|
|
611
|
+
logger3.debug(`\u2705 Found custom database URL variable: ${this.databaseUrlEnvVarName}`);
|
|
612
|
+
} else {
|
|
613
|
+
logger3.debug(`\u2705 Found default database URL variable: DATABASE_URL`);
|
|
614
|
+
}
|
|
615
|
+
return true;
|
|
616
|
+
}
|
|
617
|
+
if (this.databaseUrlEnvVarName !== "DATABASE_URL") {
|
|
618
|
+
logger3.debug(`\u274C Custom database URL variable '${this.databaseUrlEnvVarName}' not found in .env file`);
|
|
619
|
+
throw new Error(
|
|
620
|
+
`Configured database URL environment variable '${this.databaseUrlEnvVarName}' not found in .env file. Please add it to your .env file or update your iloom configuration.`
|
|
621
|
+
);
|
|
622
|
+
}
|
|
623
|
+
const hasDefaultVar = envMap.has("DATABASE_URL");
|
|
624
|
+
if (hasDefaultVar) {
|
|
625
|
+
logger3.debug("\u2705 Found fallback DATABASE_URL variable");
|
|
626
|
+
} else {
|
|
627
|
+
logger3.debug("\u274C No DATABASE_URL variable found in .env file");
|
|
628
|
+
}
|
|
629
|
+
return hasDefaultVar;
|
|
630
|
+
} catch (error) {
|
|
631
|
+
if (error instanceof Error && error.message.includes("not found in .env")) {
|
|
632
|
+
throw error;
|
|
633
|
+
}
|
|
634
|
+
return false;
|
|
635
|
+
}
|
|
636
|
+
}
|
|
637
|
+
};
|
|
638
|
+
|
|
639
|
+
export {
|
|
640
|
+
EnvironmentManager,
|
|
641
|
+
CLIIsolationManager,
|
|
642
|
+
DatabaseManager
|
|
643
|
+
};
|
|
644
|
+
//# sourceMappingURL=chunk-HPJJSYNS.js.map
|