@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
package/dist/index.js
ADDED
|
@@ -0,0 +1,3058 @@
|
|
|
1
|
+
var __defProp = Object.defineProperty;
|
|
2
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
3
|
+
var __esm = (fn, res) => function __init() {
|
|
4
|
+
return fn && (res = (0, fn[__getOwnPropNames(fn)[0]])(fn = 0)), res;
|
|
5
|
+
};
|
|
6
|
+
var __export = (target, all) => {
|
|
7
|
+
for (var name in all)
|
|
8
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
// src/utils/logger.ts
|
|
12
|
+
var logger_exports = {};
|
|
13
|
+
__export(logger_exports, {
|
|
14
|
+
createLogger: () => createLogger,
|
|
15
|
+
default: () => logger_default,
|
|
16
|
+
logger: () => logger
|
|
17
|
+
});
|
|
18
|
+
import chalk, { Chalk } from "chalk";
|
|
19
|
+
function formatMessage(message, ...args) {
|
|
20
|
+
const formattedArgs = args.map(
|
|
21
|
+
(arg) => typeof arg === "object" ? JSON.stringify(arg, null, 2) : String(arg)
|
|
22
|
+
);
|
|
23
|
+
return formattedArgs.length > 0 ? `${message} ${formattedArgs.join(" ")}` : message;
|
|
24
|
+
}
|
|
25
|
+
function formatWithEmoji(message, emoji, colorFn) {
|
|
26
|
+
if (message.trim()) {
|
|
27
|
+
return colorFn(`${emoji} ${message}`);
|
|
28
|
+
} else {
|
|
29
|
+
return "";
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
function createLogger(options = {}) {
|
|
33
|
+
const { prefix = "", timestamp = false, silent = false, forceColor, debug = globalDebugEnabled } = options;
|
|
34
|
+
let localDebugEnabled = debug;
|
|
35
|
+
const customStdoutChalk = forceColor !== void 0 ? new Chalk({ level: forceColor ? 3 : 0 }) : stdoutChalk;
|
|
36
|
+
const customStderrChalk = forceColor !== void 0 ? new Chalk({ level: forceColor ? 3 : 0 }) : stderrChalk;
|
|
37
|
+
const prefixStr = prefix ? `[${prefix}] ` : "";
|
|
38
|
+
const getTimestamp = () => timestamp ? `[${(/* @__PURE__ */ new Date()).toISOString()}] ` : "";
|
|
39
|
+
if (silent) {
|
|
40
|
+
return {
|
|
41
|
+
info: () => {
|
|
42
|
+
},
|
|
43
|
+
success: () => {
|
|
44
|
+
},
|
|
45
|
+
warn: () => {
|
|
46
|
+
},
|
|
47
|
+
error: () => {
|
|
48
|
+
},
|
|
49
|
+
debug: () => {
|
|
50
|
+
},
|
|
51
|
+
setDebug: () => {
|
|
52
|
+
},
|
|
53
|
+
isDebugEnabled: () => {
|
|
54
|
+
return false;
|
|
55
|
+
}
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
return {
|
|
59
|
+
info: (message, ...args) => {
|
|
60
|
+
const formatted = formatMessage(message, ...args);
|
|
61
|
+
const fullMessage = `${getTimestamp()}${prefixStr}${formatted}`;
|
|
62
|
+
const output = formatWithEmoji(fullMessage, "\u{1F5C2}\uFE0F ", customStdoutChalk.blue);
|
|
63
|
+
console.log(output);
|
|
64
|
+
},
|
|
65
|
+
success: (message, ...args) => {
|
|
66
|
+
const formatted = formatMessage(message, ...args);
|
|
67
|
+
const fullMessage = `${getTimestamp()}${prefixStr}${formatted}`;
|
|
68
|
+
const output = formatWithEmoji(fullMessage, "\u2705", customStdoutChalk.green);
|
|
69
|
+
console.log(output);
|
|
70
|
+
},
|
|
71
|
+
warn: (message, ...args) => {
|
|
72
|
+
const formatted = formatMessage(message, ...args);
|
|
73
|
+
const fullMessage = `${getTimestamp()}${prefixStr}${formatted}`;
|
|
74
|
+
const output = formatWithEmoji(fullMessage, "\u26A0\uFE0F ", customStderrChalk.yellow);
|
|
75
|
+
console.error(output);
|
|
76
|
+
},
|
|
77
|
+
error: (message, ...args) => {
|
|
78
|
+
const formatted = formatMessage(message, ...args);
|
|
79
|
+
const fullMessage = `${getTimestamp()}${prefixStr}${formatted}`;
|
|
80
|
+
const output = formatWithEmoji(fullMessage, "\u274C", customStderrChalk.red);
|
|
81
|
+
console.error(output);
|
|
82
|
+
},
|
|
83
|
+
debug: (message, ...args) => {
|
|
84
|
+
if (localDebugEnabled) {
|
|
85
|
+
const formatted = formatMessage(message, ...args);
|
|
86
|
+
const fullMessage = `${getTimestamp()}${prefixStr}${formatted}`;
|
|
87
|
+
const output = formatWithEmoji(fullMessage, "\u{1F50D}", customStdoutChalk.gray);
|
|
88
|
+
console.log(output);
|
|
89
|
+
}
|
|
90
|
+
},
|
|
91
|
+
setDebug: (enabled) => {
|
|
92
|
+
localDebugEnabled = enabled;
|
|
93
|
+
},
|
|
94
|
+
isDebugEnabled: () => {
|
|
95
|
+
return globalDebugEnabled;
|
|
96
|
+
}
|
|
97
|
+
};
|
|
98
|
+
}
|
|
99
|
+
var stdoutChalk, stderrChalk, globalDebugEnabled, logger, logger_default;
|
|
100
|
+
var init_logger = __esm({
|
|
101
|
+
"src/utils/logger.ts"() {
|
|
102
|
+
"use strict";
|
|
103
|
+
stdoutChalk = new Chalk({ level: chalk.level });
|
|
104
|
+
stderrChalk = new Chalk({ level: chalk.level });
|
|
105
|
+
globalDebugEnabled = false;
|
|
106
|
+
logger = {
|
|
107
|
+
info: (message, ...args) => {
|
|
108
|
+
const formatted = formatMessage(message, ...args);
|
|
109
|
+
const output = formatWithEmoji(formatted, "\u{1F5C2}\uFE0F ", stdoutChalk.blue);
|
|
110
|
+
console.log(output);
|
|
111
|
+
},
|
|
112
|
+
success: (message, ...args) => {
|
|
113
|
+
const formatted = formatMessage(message, ...args);
|
|
114
|
+
const output = formatWithEmoji(formatted, "\u2705", stdoutChalk.green);
|
|
115
|
+
console.log(output);
|
|
116
|
+
},
|
|
117
|
+
warn: (message, ...args) => {
|
|
118
|
+
const formatted = formatMessage(message, ...args);
|
|
119
|
+
const output = formatWithEmoji(formatted, "\u26A0\uFE0F ", stderrChalk.yellow);
|
|
120
|
+
console.error(output);
|
|
121
|
+
},
|
|
122
|
+
error: (message, ...args) => {
|
|
123
|
+
const formatted = formatMessage(message, ...args);
|
|
124
|
+
const output = formatWithEmoji(formatted, "\u274C", stderrChalk.red);
|
|
125
|
+
console.error(output);
|
|
126
|
+
},
|
|
127
|
+
debug: (message, ...args) => {
|
|
128
|
+
if (globalDebugEnabled) {
|
|
129
|
+
const formatted = formatMessage(message, ...args);
|
|
130
|
+
const output = formatWithEmoji(formatted, "\u{1F50D}", stdoutChalk.gray);
|
|
131
|
+
console.log(output);
|
|
132
|
+
}
|
|
133
|
+
},
|
|
134
|
+
setDebug: (enabled) => {
|
|
135
|
+
globalDebugEnabled = enabled;
|
|
136
|
+
},
|
|
137
|
+
isDebugEnabled: () => {
|
|
138
|
+
return globalDebugEnabled;
|
|
139
|
+
}
|
|
140
|
+
};
|
|
141
|
+
logger_default = logger;
|
|
142
|
+
}
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
// src/lib/SettingsManager.ts
|
|
146
|
+
var SettingsManager_exports = {};
|
|
147
|
+
__export(SettingsManager_exports, {
|
|
148
|
+
AgentSettingsSchema: () => AgentSettingsSchema,
|
|
149
|
+
CapabilitiesSettingsSchema: () => CapabilitiesSettingsSchema,
|
|
150
|
+
IloomSettingsSchema: () => IloomSettingsSchema,
|
|
151
|
+
SettingsManager: () => SettingsManager,
|
|
152
|
+
WorkflowPermissionSchema: () => WorkflowPermissionSchema,
|
|
153
|
+
WorkflowsSettingsSchema: () => WorkflowsSettingsSchema
|
|
154
|
+
});
|
|
155
|
+
import { readFile } from "fs/promises";
|
|
156
|
+
import path from "path";
|
|
157
|
+
import { z } from "zod";
|
|
158
|
+
import deepmerge from "deepmerge";
|
|
159
|
+
var AgentSettingsSchema, WorkflowPermissionSchema, WorkflowsSettingsSchema, CapabilitiesSettingsSchema, IloomSettingsSchema, SettingsManager;
|
|
160
|
+
var init_SettingsManager = __esm({
|
|
161
|
+
"src/lib/SettingsManager.ts"() {
|
|
162
|
+
"use strict";
|
|
163
|
+
init_logger();
|
|
164
|
+
AgentSettingsSchema = z.object({
|
|
165
|
+
model: z.enum(["sonnet", "opus", "haiku"]).optional().describe("Claude model shorthand: sonnet, opus, or haiku")
|
|
166
|
+
// Future: could add other per-agent overrides
|
|
167
|
+
});
|
|
168
|
+
WorkflowPermissionSchema = z.object({
|
|
169
|
+
permissionMode: z.enum(["plan", "acceptEdits", "bypassPermissions", "default"]).optional().describe("Permission mode for Claude CLI in this workflow type"),
|
|
170
|
+
noVerify: z.boolean().optional().describe("Skip pre-commit hooks (--no-verify) when committing during finish workflow"),
|
|
171
|
+
startIde: z.boolean().default(true).describe("Launch IDE (code) when starting this workflow type"),
|
|
172
|
+
startDevServer: z.boolean().default(true).describe("Launch development server when starting this workflow type"),
|
|
173
|
+
startAiAgent: z.boolean().default(true).describe("Launch Claude AI agent when starting this workflow type"),
|
|
174
|
+
startTerminal: z.boolean().default(false).describe("Launch terminal window without dev server when starting this workflow type")
|
|
175
|
+
});
|
|
176
|
+
WorkflowsSettingsSchema = z.object({
|
|
177
|
+
issue: WorkflowPermissionSchema.optional(),
|
|
178
|
+
pr: WorkflowPermissionSchema.optional(),
|
|
179
|
+
regular: WorkflowPermissionSchema.optional()
|
|
180
|
+
}).optional();
|
|
181
|
+
CapabilitiesSettingsSchema = z.object({
|
|
182
|
+
web: z.object({
|
|
183
|
+
basePort: z.number().min(1, "Base port must be >= 1").max(65535, "Base port must be <= 65535").optional().describe("Base port for web workspace port calculations (default: 3000)")
|
|
184
|
+
}).optional(),
|
|
185
|
+
database: z.object({
|
|
186
|
+
databaseUrlEnvVarName: z.string().min(1, "Database URL variable name cannot be empty").regex(/^[A-Z_][A-Z0-9_]*$/, "Must be valid env var name (uppercase, underscores)").optional().default("DATABASE_URL").describe("Name of environment variable for database connection URL")
|
|
187
|
+
}).optional()
|
|
188
|
+
}).optional();
|
|
189
|
+
IloomSettingsSchema = z.object({
|
|
190
|
+
mainBranch: z.string().min(1, "Settings 'mainBranch' cannot be empty").optional().describe("Name of the main/primary branch for the repository"),
|
|
191
|
+
worktreePrefix: z.string().optional().refine(
|
|
192
|
+
(val) => {
|
|
193
|
+
if (val === void 0) return true;
|
|
194
|
+
if (val === "") return true;
|
|
195
|
+
const allowedChars = /^[a-zA-Z0-9\-_/]+$/;
|
|
196
|
+
if (!allowedChars.test(val)) return false;
|
|
197
|
+
if (/^[-_/]+$/.test(val)) return false;
|
|
198
|
+
const segments = val.split("/");
|
|
199
|
+
for (const segment of segments) {
|
|
200
|
+
if (segment && /^[-_]+$/.test(segment)) {
|
|
201
|
+
return false;
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
return true;
|
|
205
|
+
},
|
|
206
|
+
{
|
|
207
|
+
message: "worktreePrefix contains invalid characters. Only alphanumeric characters, hyphens (-), underscores (_), and forward slashes (/) are allowed. Use forward slashes for nested directories."
|
|
208
|
+
}
|
|
209
|
+
).describe(
|
|
210
|
+
"Prefix for worktree directories. Empty string disables prefix. Defaults to <repo-name>-looms if not set."
|
|
211
|
+
),
|
|
212
|
+
protectedBranches: z.array(z.string().min(1, "Protected branch name cannot be empty")).optional().describe('List of branches that cannot be deleted (defaults to [mainBranch, "main", "master", "develop"])'),
|
|
213
|
+
workflows: WorkflowsSettingsSchema.describe("Per-workflow-type permission configurations"),
|
|
214
|
+
agents: z.record(z.string(), AgentSettingsSchema).optional().nullable().describe("Per-agent configuration overrides"),
|
|
215
|
+
capabilities: CapabilitiesSettingsSchema.describe("Project capability configurations")
|
|
216
|
+
});
|
|
217
|
+
SettingsManager = class {
|
|
218
|
+
/**
|
|
219
|
+
* Load settings from <PROJECT_ROOT>/.iloom/settings.json and settings.local.json
|
|
220
|
+
* Merges settings.local.json over settings.json with priority
|
|
221
|
+
* CLI overrides have highest priority if provided
|
|
222
|
+
* Returns empty object if both files don't exist (not an error)
|
|
223
|
+
*/
|
|
224
|
+
async loadSettings(projectRoot, cliOverrides) {
|
|
225
|
+
const root = this.getProjectRoot(projectRoot);
|
|
226
|
+
const baseSettings = await this.loadSettingsFile(root, "settings.json");
|
|
227
|
+
const baseSettingsPath = path.join(root, ".iloom", "settings.json");
|
|
228
|
+
logger.debug(`\u{1F4C4} Base settings from ${baseSettingsPath}:`, JSON.stringify(baseSettings, null, 2));
|
|
229
|
+
const localSettings = await this.loadSettingsFile(root, "settings.local.json");
|
|
230
|
+
const localSettingsPath = path.join(root, ".iloom", "settings.local.json");
|
|
231
|
+
logger.debug(`\u{1F4C4} Local settings from ${localSettingsPath}:`, JSON.stringify(localSettings, null, 2));
|
|
232
|
+
let merged = this.mergeSettings(baseSettings, localSettings);
|
|
233
|
+
logger.debug("\u{1F504} After merging base + local settings:", JSON.stringify(merged, null, 2));
|
|
234
|
+
if (cliOverrides && Object.keys(cliOverrides).length > 0) {
|
|
235
|
+
logger.debug("\u2699\uFE0F CLI overrides to apply:", JSON.stringify(cliOverrides, null, 2));
|
|
236
|
+
merged = this.mergeSettings(merged, cliOverrides);
|
|
237
|
+
logger.debug("\u{1F504} After applying CLI overrides:", JSON.stringify(merged, null, 2));
|
|
238
|
+
}
|
|
239
|
+
try {
|
|
240
|
+
const finalSettings = IloomSettingsSchema.parse(merged);
|
|
241
|
+
this.logFinalConfiguration(finalSettings);
|
|
242
|
+
return finalSettings;
|
|
243
|
+
} catch (error) {
|
|
244
|
+
if (error instanceof z.ZodError) {
|
|
245
|
+
const errorMsg = this.formatAllZodErrors(error, "<merged settings>");
|
|
246
|
+
if (cliOverrides && Object.keys(cliOverrides).length > 0) {
|
|
247
|
+
throw new Error(`${errorMsg.message}
|
|
248
|
+
|
|
249
|
+
Note: CLI overrides were applied. Check your --set arguments.`);
|
|
250
|
+
}
|
|
251
|
+
throw errorMsg;
|
|
252
|
+
}
|
|
253
|
+
throw error;
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
/**
|
|
257
|
+
* Log the final merged configuration for debugging
|
|
258
|
+
*/
|
|
259
|
+
logFinalConfiguration(settings) {
|
|
260
|
+
logger.debug("\u{1F4CB} Final merged configuration:", JSON.stringify(settings, null, 2));
|
|
261
|
+
}
|
|
262
|
+
/**
|
|
263
|
+
* Load and parse a single settings file
|
|
264
|
+
* Returns empty object if file doesn't exist (not an error)
|
|
265
|
+
*/
|
|
266
|
+
async loadSettingsFile(projectRoot, filename) {
|
|
267
|
+
const settingsPath = path.join(projectRoot, ".iloom", filename);
|
|
268
|
+
try {
|
|
269
|
+
const content = await readFile(settingsPath, "utf-8");
|
|
270
|
+
let parsed;
|
|
271
|
+
try {
|
|
272
|
+
parsed = JSON.parse(content);
|
|
273
|
+
} catch (error) {
|
|
274
|
+
throw new Error(
|
|
275
|
+
`Failed to parse settings file at ${settingsPath}: ${error instanceof Error ? error.message : "Invalid JSON"}`
|
|
276
|
+
);
|
|
277
|
+
}
|
|
278
|
+
try {
|
|
279
|
+
const validated = IloomSettingsSchema.strict().parse(parsed);
|
|
280
|
+
return validated;
|
|
281
|
+
} catch (error) {
|
|
282
|
+
if (error instanceof z.ZodError) {
|
|
283
|
+
const errorMsg = this.formatAllZodErrors(error, filename);
|
|
284
|
+
throw errorMsg;
|
|
285
|
+
}
|
|
286
|
+
throw error;
|
|
287
|
+
}
|
|
288
|
+
} catch (error) {
|
|
289
|
+
if (error.code === "ENOENT") {
|
|
290
|
+
logger.debug(`No settings file found at ${settingsPath}, using defaults`);
|
|
291
|
+
return {};
|
|
292
|
+
}
|
|
293
|
+
throw error;
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
/**
|
|
297
|
+
* Deep merge two settings objects with priority to override
|
|
298
|
+
* Uses deepmerge library with array replacement strategy
|
|
299
|
+
*/
|
|
300
|
+
mergeSettings(base, override) {
|
|
301
|
+
return deepmerge(base, override, {
|
|
302
|
+
// Replace arrays instead of concatenating them
|
|
303
|
+
arrayMerge: (_destinationArray, sourceArray) => sourceArray
|
|
304
|
+
});
|
|
305
|
+
}
|
|
306
|
+
/**
|
|
307
|
+
* Format all Zod validation errors into a single error message
|
|
308
|
+
*/
|
|
309
|
+
formatAllZodErrors(error, settingsPath) {
|
|
310
|
+
const errorMessages = error.issues.map((issue) => {
|
|
311
|
+
const path5 = issue.path.length > 0 ? issue.path.join(".") : "root";
|
|
312
|
+
return ` - ${path5}: ${issue.message}`;
|
|
313
|
+
});
|
|
314
|
+
return new Error(
|
|
315
|
+
`Settings validation failed at ${settingsPath}:
|
|
316
|
+
${errorMessages.join("\n")}`
|
|
317
|
+
);
|
|
318
|
+
}
|
|
319
|
+
/**
|
|
320
|
+
* Validate settings structure and model names using Zod schema
|
|
321
|
+
* This method is kept for testing purposes but uses Zod internally
|
|
322
|
+
* @internal - Only used in tests via bracket notation
|
|
323
|
+
*/
|
|
324
|
+
// @ts-expect-error - Used in tests via bracket notation, TypeScript can't detect this usage
|
|
325
|
+
validateSettings(settings) {
|
|
326
|
+
try {
|
|
327
|
+
IloomSettingsSchema.parse(settings);
|
|
328
|
+
} catch (error) {
|
|
329
|
+
if (error instanceof z.ZodError) {
|
|
330
|
+
throw this.formatAllZodErrors(error, "<validation>");
|
|
331
|
+
}
|
|
332
|
+
throw error;
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
/**
|
|
336
|
+
* Get project root (defaults to process.cwd())
|
|
337
|
+
*/
|
|
338
|
+
getProjectRoot(projectRoot) {
|
|
339
|
+
return projectRoot ?? process.cwd();
|
|
340
|
+
}
|
|
341
|
+
/**
|
|
342
|
+
* Get effective protected branches list with mainBranch always included
|
|
343
|
+
*
|
|
344
|
+
* This method provides a single source of truth for protected branches logic:
|
|
345
|
+
* 1. Use configured protectedBranches if provided
|
|
346
|
+
* 2. Otherwise use defaults: [mainBranch, 'main', 'master', 'develop']
|
|
347
|
+
* 3. ALWAYS ensure mainBranch is included even if user configured custom list
|
|
348
|
+
*
|
|
349
|
+
* @param projectRoot - Optional project root directory (defaults to process.cwd())
|
|
350
|
+
* @returns Array of protected branch names with mainBranch guaranteed to be included
|
|
351
|
+
*/
|
|
352
|
+
async getProtectedBranches(projectRoot) {
|
|
353
|
+
const settings = await this.loadSettings(projectRoot);
|
|
354
|
+
const mainBranch = settings.mainBranch ?? "main";
|
|
355
|
+
let protectedBranches;
|
|
356
|
+
if (settings.protectedBranches) {
|
|
357
|
+
protectedBranches = settings.protectedBranches.includes(mainBranch) ? settings.protectedBranches : [mainBranch, ...settings.protectedBranches];
|
|
358
|
+
} else {
|
|
359
|
+
protectedBranches = [mainBranch, "main", "master", "develop"];
|
|
360
|
+
}
|
|
361
|
+
return protectedBranches;
|
|
362
|
+
}
|
|
363
|
+
};
|
|
364
|
+
}
|
|
365
|
+
});
|
|
366
|
+
|
|
367
|
+
// src/utils/terminal.ts
|
|
368
|
+
var terminal_exports = {};
|
|
369
|
+
__export(terminal_exports, {
|
|
370
|
+
detectITerm2: () => detectITerm2,
|
|
371
|
+
detectPlatform: () => detectPlatform,
|
|
372
|
+
openDualTerminalWindow: () => openDualTerminalWindow,
|
|
373
|
+
openMultipleTerminalWindows: () => openMultipleTerminalWindows,
|
|
374
|
+
openTerminalWindow: () => openTerminalWindow
|
|
375
|
+
});
|
|
376
|
+
import { execa as execa2 } from "execa";
|
|
377
|
+
import { existsSync } from "fs";
|
|
378
|
+
function detectPlatform() {
|
|
379
|
+
const platform = process.platform;
|
|
380
|
+
if (platform === "darwin") return "darwin";
|
|
381
|
+
if (platform === "linux") return "linux";
|
|
382
|
+
if (platform === "win32") return "win32";
|
|
383
|
+
return "unsupported";
|
|
384
|
+
}
|
|
385
|
+
async function detectITerm2() {
|
|
386
|
+
const platform = detectPlatform();
|
|
387
|
+
if (platform !== "darwin") return false;
|
|
388
|
+
return existsSync("/Applications/iTerm.app");
|
|
389
|
+
}
|
|
390
|
+
async function openTerminalWindow(options) {
|
|
391
|
+
const platform = detectPlatform();
|
|
392
|
+
if (platform !== "darwin") {
|
|
393
|
+
throw new Error(
|
|
394
|
+
`Terminal window launching not yet supported on ${platform}. Currently only macOS is supported.`
|
|
395
|
+
);
|
|
396
|
+
}
|
|
397
|
+
const applescript = buildAppleScript(options);
|
|
398
|
+
try {
|
|
399
|
+
await execa2("osascript", ["-e", applescript]);
|
|
400
|
+
await execa2("osascript", ["-e", 'tell application "Terminal" to activate']);
|
|
401
|
+
} catch (error) {
|
|
402
|
+
throw new Error(
|
|
403
|
+
`Failed to open terminal window: ${error instanceof Error ? error.message : "Unknown error"}`
|
|
404
|
+
);
|
|
405
|
+
}
|
|
406
|
+
}
|
|
407
|
+
function buildAppleScript(options) {
|
|
408
|
+
const {
|
|
409
|
+
workspacePath,
|
|
410
|
+
command,
|
|
411
|
+
backgroundColor,
|
|
412
|
+
port,
|
|
413
|
+
includeEnvSetup,
|
|
414
|
+
includePortExport
|
|
415
|
+
} = options;
|
|
416
|
+
const commands = [];
|
|
417
|
+
if (workspacePath) {
|
|
418
|
+
commands.push(`cd '${escapePathForAppleScript(workspacePath)}'`);
|
|
419
|
+
}
|
|
420
|
+
if (includeEnvSetup) {
|
|
421
|
+
commands.push("source .env");
|
|
422
|
+
}
|
|
423
|
+
if (includePortExport && port !== void 0) {
|
|
424
|
+
commands.push(`export PORT=${port}`);
|
|
425
|
+
}
|
|
426
|
+
if (command) {
|
|
427
|
+
commands.push(command);
|
|
428
|
+
}
|
|
429
|
+
const fullCommand = commands.join(" && ");
|
|
430
|
+
const historyFreeCommand = ` ${fullCommand}`;
|
|
431
|
+
let script = `tell application "Terminal"
|
|
432
|
+
`;
|
|
433
|
+
script += ` set newTab to do script "${escapeForAppleScript(historyFreeCommand)}"
|
|
434
|
+
`;
|
|
435
|
+
if (backgroundColor) {
|
|
436
|
+
const { r, g, b } = backgroundColor;
|
|
437
|
+
script += ` set background color of newTab to {${Math.round(r * 257)}, ${Math.round(g * 257)}, ${Math.round(b * 257)}}
|
|
438
|
+
`;
|
|
439
|
+
}
|
|
440
|
+
script += `end tell`;
|
|
441
|
+
return script;
|
|
442
|
+
}
|
|
443
|
+
function escapePathForAppleScript(path5) {
|
|
444
|
+
return path5.replace(/'/g, "'\\''");
|
|
445
|
+
}
|
|
446
|
+
function escapeForAppleScript(command) {
|
|
447
|
+
return command.replace(/\\/g, "\\\\").replace(/"/g, '\\"');
|
|
448
|
+
}
|
|
449
|
+
function buildCommandSequence(options) {
|
|
450
|
+
const {
|
|
451
|
+
workspacePath,
|
|
452
|
+
command,
|
|
453
|
+
port,
|
|
454
|
+
includeEnvSetup,
|
|
455
|
+
includePortExport
|
|
456
|
+
} = options;
|
|
457
|
+
const commands = [];
|
|
458
|
+
if (workspacePath) {
|
|
459
|
+
commands.push(`cd '${escapePathForAppleScript(workspacePath)}'`);
|
|
460
|
+
}
|
|
461
|
+
if (includeEnvSetup) {
|
|
462
|
+
commands.push("source .env");
|
|
463
|
+
}
|
|
464
|
+
if (includePortExport && port !== void 0) {
|
|
465
|
+
commands.push(`export PORT=${port}`);
|
|
466
|
+
}
|
|
467
|
+
if (command) {
|
|
468
|
+
commands.push(command);
|
|
469
|
+
}
|
|
470
|
+
const fullCommand = commands.join(" && ");
|
|
471
|
+
return ` ${fullCommand}`;
|
|
472
|
+
}
|
|
473
|
+
function buildITerm2MultiTabScript(optionsArray) {
|
|
474
|
+
if (optionsArray.length < 2) {
|
|
475
|
+
throw new Error("buildITerm2MultiTabScript requires at least 2 terminal options");
|
|
476
|
+
}
|
|
477
|
+
let script = 'tell application id "com.googlecode.iterm2"\n';
|
|
478
|
+
script += " create window with default profile\n";
|
|
479
|
+
script += " set newWindow to current window\n";
|
|
480
|
+
const options1 = optionsArray[0];
|
|
481
|
+
if (!options1) {
|
|
482
|
+
throw new Error("First terminal option is undefined");
|
|
483
|
+
}
|
|
484
|
+
const command1 = buildCommandSequence(options1);
|
|
485
|
+
script += " set s1 to current session of newWindow\n\n";
|
|
486
|
+
if (options1.backgroundColor) {
|
|
487
|
+
const { r, g, b } = options1.backgroundColor;
|
|
488
|
+
script += ` set background color of s1 to {${Math.round(r * 257)}, ${Math.round(g * 257)}, ${Math.round(b * 257)}}
|
|
489
|
+
`;
|
|
490
|
+
}
|
|
491
|
+
script += ` tell s1 to write text "${escapeForAppleScript(command1)}"
|
|
492
|
+
|
|
493
|
+
`;
|
|
494
|
+
if (options1.title) {
|
|
495
|
+
script += ` set name of s1 to "${escapeForAppleScript(options1.title)}"
|
|
496
|
+
|
|
497
|
+
`;
|
|
498
|
+
}
|
|
499
|
+
for (let i = 1; i < optionsArray.length; i++) {
|
|
500
|
+
const options = optionsArray[i];
|
|
501
|
+
if (!options) {
|
|
502
|
+
throw new Error(`Terminal option at index ${i} is undefined`);
|
|
503
|
+
}
|
|
504
|
+
const command = buildCommandSequence(options);
|
|
505
|
+
const sessionVar = `s${i + 1}`;
|
|
506
|
+
script += " tell newWindow\n";
|
|
507
|
+
script += ` set newTab${i} to (create tab with default profile)
|
|
508
|
+
`;
|
|
509
|
+
script += " end tell\n";
|
|
510
|
+
script += ` set ${sessionVar} to current session of newTab${i}
|
|
511
|
+
|
|
512
|
+
`;
|
|
513
|
+
if (options.backgroundColor) {
|
|
514
|
+
const { r, g, b } = options.backgroundColor;
|
|
515
|
+
script += ` set background color of ${sessionVar} to {${Math.round(r * 257)}, ${Math.round(g * 257)}, ${Math.round(b * 257)}}
|
|
516
|
+
`;
|
|
517
|
+
}
|
|
518
|
+
script += ` tell ${sessionVar} to write text "${escapeForAppleScript(command)}"
|
|
519
|
+
|
|
520
|
+
`;
|
|
521
|
+
if (options.title) {
|
|
522
|
+
script += ` set name of ${sessionVar} to "${escapeForAppleScript(options.title)}"
|
|
523
|
+
|
|
524
|
+
`;
|
|
525
|
+
}
|
|
526
|
+
}
|
|
527
|
+
script += " activate\n";
|
|
528
|
+
script += "end tell";
|
|
529
|
+
return script;
|
|
530
|
+
}
|
|
531
|
+
async function openMultipleTerminalWindows(optionsArray) {
|
|
532
|
+
if (optionsArray.length < 2) {
|
|
533
|
+
throw new Error("openMultipleTerminalWindows requires at least 2 terminal options. Use openTerminalWindow for single terminal.");
|
|
534
|
+
}
|
|
535
|
+
const platform = detectPlatform();
|
|
536
|
+
if (platform !== "darwin") {
|
|
537
|
+
throw new Error(
|
|
538
|
+
`Terminal window launching not yet supported on ${platform}. Currently only macOS is supported.`
|
|
539
|
+
);
|
|
540
|
+
}
|
|
541
|
+
const hasITerm2 = await detectITerm2();
|
|
542
|
+
if (hasITerm2) {
|
|
543
|
+
const applescript = buildITerm2MultiTabScript(optionsArray);
|
|
544
|
+
try {
|
|
545
|
+
await execa2("osascript", ["-e", applescript]);
|
|
546
|
+
} catch (error) {
|
|
547
|
+
throw new Error(
|
|
548
|
+
`Failed to open iTerm2 window: ${error instanceof Error ? error.message : "Unknown error"}`
|
|
549
|
+
);
|
|
550
|
+
}
|
|
551
|
+
} else {
|
|
552
|
+
for (let i = 0; i < optionsArray.length; i++) {
|
|
553
|
+
const options = optionsArray[i];
|
|
554
|
+
if (!options) {
|
|
555
|
+
throw new Error(`Terminal option at index ${i} is undefined`);
|
|
556
|
+
}
|
|
557
|
+
await openTerminalWindow(options);
|
|
558
|
+
if (i < optionsArray.length - 1) {
|
|
559
|
+
await new Promise((resolve) => setTimeout(resolve, 1e3));
|
|
560
|
+
}
|
|
561
|
+
}
|
|
562
|
+
}
|
|
563
|
+
}
|
|
564
|
+
async function openDualTerminalWindow(options1, options2) {
|
|
565
|
+
await openMultipleTerminalWindows([options1, options2]);
|
|
566
|
+
}
|
|
567
|
+
var init_terminal = __esm({
|
|
568
|
+
"src/utils/terminal.ts"() {
|
|
569
|
+
"use strict";
|
|
570
|
+
}
|
|
571
|
+
});
|
|
572
|
+
|
|
573
|
+
// src/utils/color.ts
|
|
574
|
+
var color_exports = {};
|
|
575
|
+
__export(color_exports, {
|
|
576
|
+
calculateForegroundColor: () => calculateForegroundColor,
|
|
577
|
+
generateColorFromBranchName: () => generateColorFromBranchName,
|
|
578
|
+
getColorPalette: () => getColorPalette,
|
|
579
|
+
hexToRgb: () => hexToRgb,
|
|
580
|
+
lightenColor: () => lightenColor,
|
|
581
|
+
rgbToHex: () => rgbToHex,
|
|
582
|
+
saturateColor: () => saturateColor
|
|
583
|
+
});
|
|
584
|
+
import { createHash } from "crypto";
|
|
585
|
+
function getColorPalette() {
|
|
586
|
+
return [
|
|
587
|
+
// First 10 colors preserved for backward compatibility
|
|
588
|
+
{ r: 220, g: 235, b: 248 },
|
|
589
|
+
// 0: Soft blue
|
|
590
|
+
{ r: 248, g: 220, b: 235 },
|
|
591
|
+
// 1: Soft pink
|
|
592
|
+
{ r: 220, g: 248, b: 235 },
|
|
593
|
+
// 2: Soft green
|
|
594
|
+
{ r: 248, g: 240, b: 220 },
|
|
595
|
+
// 3: Soft cream
|
|
596
|
+
{ r: 240, g: 220, b: 248 },
|
|
597
|
+
// 4: Soft lavender
|
|
598
|
+
{ r: 220, g: 240, b: 248 },
|
|
599
|
+
// 5: Soft cyan
|
|
600
|
+
{ r: 235, g: 235, b: 235 },
|
|
601
|
+
// 6: Soft grey
|
|
602
|
+
{ r: 228, g: 238, b: 248 },
|
|
603
|
+
// 7: Soft ice blue
|
|
604
|
+
{ r: 248, g: 228, b: 238 },
|
|
605
|
+
// 8: Soft rose
|
|
606
|
+
{ r: 228, g: 248, b: 238 },
|
|
607
|
+
// 9: Soft mint
|
|
608
|
+
// 30 new colors (indices 10-39)
|
|
609
|
+
{ r: 235, g: 245, b: 250 },
|
|
610
|
+
// 10: Pale sky blue
|
|
611
|
+
{ r: 250, g: 235, b: 245 },
|
|
612
|
+
// 11: Pale orchid
|
|
613
|
+
{ r: 235, g: 250, b: 245 },
|
|
614
|
+
// 12: Pale seafoam
|
|
615
|
+
{ r: 250, g: 245, b: 235 },
|
|
616
|
+
// 13: Pale peach
|
|
617
|
+
{ r: 245, g: 235, b: 250 },
|
|
618
|
+
// 14: Pale periwinkle
|
|
619
|
+
{ r: 235, g: 245, b: 235 },
|
|
620
|
+
// 15: Pale sage
|
|
621
|
+
{ r: 245, g: 250, b: 235 },
|
|
622
|
+
// 16: Pale lemon
|
|
623
|
+
{ r: 245, g: 235, b: 235 },
|
|
624
|
+
// 17: Pale blush
|
|
625
|
+
{ r: 235, g: 235, b: 250 },
|
|
626
|
+
// 18: Pale lavender blue
|
|
627
|
+
{ r: 250, g: 235, b: 235 },
|
|
628
|
+
// 19: Pale coral
|
|
629
|
+
{ r: 235, g: 250, b: 250 },
|
|
630
|
+
// 20: Pale aqua
|
|
631
|
+
{ r: 240, g: 248, b: 255 },
|
|
632
|
+
// 21: Alice blue
|
|
633
|
+
{ r: 255, g: 240, b: 248 },
|
|
634
|
+
// 22: Lavender blush
|
|
635
|
+
{ r: 240, g: 255, b: 248 },
|
|
636
|
+
// 23: Honeydew tint
|
|
637
|
+
{ r: 255, g: 248, b: 240 },
|
|
638
|
+
// 24: Antique white
|
|
639
|
+
{ r: 248, g: 240, b: 255 },
|
|
640
|
+
// 25: Magnolia
|
|
641
|
+
{ r: 240, g: 248, b: 240 },
|
|
642
|
+
// 26: Mint cream tint
|
|
643
|
+
{ r: 248, g: 255, b: 240 },
|
|
644
|
+
// 27: Ivory tint
|
|
645
|
+
{ r: 248, g: 240, b: 240 },
|
|
646
|
+
// 28: Misty rose tint
|
|
647
|
+
{ r: 240, g: 240, b: 255 },
|
|
648
|
+
// 29: Ghost white tint
|
|
649
|
+
{ r: 255, g: 245, b: 238 },
|
|
650
|
+
// 30: Seashell
|
|
651
|
+
{ r: 245, g: 255, b: 250 },
|
|
652
|
+
// 31: Azure mist
|
|
653
|
+
{ r: 250, g: 245, b: 255 },
|
|
654
|
+
// 32: Lilac mist
|
|
655
|
+
{ r: 255, g: 250, b: 245 },
|
|
656
|
+
// 33: Snow peach
|
|
657
|
+
{ r: 238, g: 245, b: 255 },
|
|
658
|
+
// 34: Powder blue
|
|
659
|
+
{ r: 255, g: 238, b: 245 },
|
|
660
|
+
// 35: Pink lace
|
|
661
|
+
{ r: 245, g: 255, b: 238 },
|
|
662
|
+
// 36: Pale lime
|
|
663
|
+
{ r: 238, g: 255, b: 245 },
|
|
664
|
+
// 37: Pale turquoise
|
|
665
|
+
{ r: 245, g: 238, b: 255 },
|
|
666
|
+
// 38: Pale violet
|
|
667
|
+
{ r: 255, g: 245, b: 255 }
|
|
668
|
+
// 39: Pale magenta
|
|
669
|
+
];
|
|
670
|
+
}
|
|
671
|
+
function rgbToHex(r, g, b) {
|
|
672
|
+
if (r < 0 || r > 255 || g < 0 || g > 255 || b < 0 || b > 255) {
|
|
673
|
+
throw new Error("RGB values must be between 0 and 255");
|
|
674
|
+
}
|
|
675
|
+
const rHex = r.toString(16).padStart(2, "0");
|
|
676
|
+
const gHex = g.toString(16).padStart(2, "0");
|
|
677
|
+
const bHex = b.toString(16).padStart(2, "0");
|
|
678
|
+
return `#${rHex}${gHex}${bHex}`;
|
|
679
|
+
}
|
|
680
|
+
function hexToRgb(hex) {
|
|
681
|
+
const cleanHex = hex.startsWith("#") ? hex.slice(1) : hex;
|
|
682
|
+
if (cleanHex.length !== 6 || !/^[0-9a-fA-F]{6}$/.test(cleanHex)) {
|
|
683
|
+
throw new Error("Invalid hex color format. Expected format: #RRGGBB or RRGGBB");
|
|
684
|
+
}
|
|
685
|
+
const r = parseInt(cleanHex.slice(0, 2), 16);
|
|
686
|
+
const g = parseInt(cleanHex.slice(2, 4), 16);
|
|
687
|
+
const b = parseInt(cleanHex.slice(4, 6), 16);
|
|
688
|
+
return { r, g, b };
|
|
689
|
+
}
|
|
690
|
+
function generateColorFromBranchName(branchName) {
|
|
691
|
+
const hash = createHash("sha256").update(branchName).digest("hex");
|
|
692
|
+
const hashPrefix = hash.slice(0, 8);
|
|
693
|
+
const palette = getColorPalette();
|
|
694
|
+
const hashAsInt = parseInt(hashPrefix, 16);
|
|
695
|
+
const index = hashAsInt % palette.length;
|
|
696
|
+
logger_default.debug(`[generateColorFromBranchName] Branch name: ${branchName}, Hash: ${hash}, Hash prefix: ${hashPrefix}, Hash as int: ${hashAsInt}, Index: ${index}`);
|
|
697
|
+
const rgb = palette[index];
|
|
698
|
+
if (!rgb) {
|
|
699
|
+
throw new Error(`Invalid color index: ${index}`);
|
|
700
|
+
}
|
|
701
|
+
const hex = rgbToHex(rgb.r, rgb.g, rgb.b);
|
|
702
|
+
return {
|
|
703
|
+
rgb,
|
|
704
|
+
hex,
|
|
705
|
+
index
|
|
706
|
+
};
|
|
707
|
+
}
|
|
708
|
+
function lightenColor(rgb, amount) {
|
|
709
|
+
const clamp = (value) => Math.min(255, Math.max(0, Math.round(value)));
|
|
710
|
+
return {
|
|
711
|
+
r: clamp(rgb.r + (255 - rgb.r) * amount),
|
|
712
|
+
g: clamp(rgb.g + (255 - rgb.g) * amount),
|
|
713
|
+
b: clamp(rgb.b + (255 - rgb.b) * amount)
|
|
714
|
+
};
|
|
715
|
+
}
|
|
716
|
+
function saturateColor(rgb, amount) {
|
|
717
|
+
const clamp = (value) => Math.min(255, Math.max(0, Math.round(value)));
|
|
718
|
+
const avg = (rgb.r + rgb.g + rgb.b) / 3;
|
|
719
|
+
return {
|
|
720
|
+
r: clamp(rgb.r + (rgb.r - avg) * amount),
|
|
721
|
+
g: clamp(rgb.g + (rgb.g - avg) * amount),
|
|
722
|
+
b: clamp(rgb.b + (rgb.b - avg) * amount)
|
|
723
|
+
};
|
|
724
|
+
}
|
|
725
|
+
function calculateForegroundColor(rgb) {
|
|
726
|
+
const toLinear = (channel) => {
|
|
727
|
+
const c = channel / 255;
|
|
728
|
+
return c <= 0.03928 ? c / 12.92 : Math.pow((c + 0.055) / 1.055, 2.4);
|
|
729
|
+
};
|
|
730
|
+
const r = toLinear(rgb.r);
|
|
731
|
+
const g = toLinear(rgb.g);
|
|
732
|
+
const b = toLinear(rgb.b);
|
|
733
|
+
const luminance = 0.2126 * r + 0.7152 * g + 0.0722 * b;
|
|
734
|
+
return luminance > 0.5 ? "#000000" : "#ffffff";
|
|
735
|
+
}
|
|
736
|
+
var init_color = __esm({
|
|
737
|
+
"src/utils/color.ts"() {
|
|
738
|
+
"use strict";
|
|
739
|
+
init_logger();
|
|
740
|
+
}
|
|
741
|
+
});
|
|
742
|
+
|
|
743
|
+
// src/utils/claude.ts
|
|
744
|
+
var claude_exports = {};
|
|
745
|
+
__export(claude_exports, {
|
|
746
|
+
detectClaudeCli: () => detectClaudeCli,
|
|
747
|
+
generateBranchName: () => generateBranchName,
|
|
748
|
+
getClaudeVersion: () => getClaudeVersion,
|
|
749
|
+
launchClaude: () => launchClaude,
|
|
750
|
+
launchClaudeInNewTerminalWindow: () => launchClaudeInNewTerminalWindow
|
|
751
|
+
});
|
|
752
|
+
import { execa as execa3 } from "execa";
|
|
753
|
+
import { existsSync as existsSync2 } from "fs";
|
|
754
|
+
import { join } from "path";
|
|
755
|
+
async function detectClaudeCli() {
|
|
756
|
+
try {
|
|
757
|
+
await execa3("command", ["-v", "claude"], {
|
|
758
|
+
shell: true,
|
|
759
|
+
timeout: 5e3
|
|
760
|
+
});
|
|
761
|
+
return true;
|
|
762
|
+
} catch (error) {
|
|
763
|
+
logger.debug("Claude CLI not available", { error });
|
|
764
|
+
return false;
|
|
765
|
+
}
|
|
766
|
+
}
|
|
767
|
+
async function getClaudeVersion() {
|
|
768
|
+
try {
|
|
769
|
+
const result = await execa3("claude", ["--version"], {
|
|
770
|
+
timeout: 5e3
|
|
771
|
+
});
|
|
772
|
+
return result.stdout.trim();
|
|
773
|
+
} catch (error) {
|
|
774
|
+
logger.warn("Failed to get Claude version", { error });
|
|
775
|
+
return null;
|
|
776
|
+
}
|
|
777
|
+
}
|
|
778
|
+
function parseJsonStreamOutput(output) {
|
|
779
|
+
try {
|
|
780
|
+
const lines = output.split("\n").filter((line) => line.trim());
|
|
781
|
+
let lastResult = "";
|
|
782
|
+
for (const line of lines) {
|
|
783
|
+
try {
|
|
784
|
+
const jsonObj = JSON.parse(line);
|
|
785
|
+
if (jsonObj && typeof jsonObj === "object" && jsonObj.type === "result" && "result" in jsonObj) {
|
|
786
|
+
lastResult = jsonObj.result;
|
|
787
|
+
}
|
|
788
|
+
} catch {
|
|
789
|
+
continue;
|
|
790
|
+
}
|
|
791
|
+
}
|
|
792
|
+
return lastResult || output;
|
|
793
|
+
} catch {
|
|
794
|
+
return output;
|
|
795
|
+
}
|
|
796
|
+
}
|
|
797
|
+
async function launchClaude(prompt, options = {}) {
|
|
798
|
+
const { model, permissionMode, addDir, headless = false, appendSystemPrompt, mcpConfig, allowedTools, disallowedTools, agents } = options;
|
|
799
|
+
const args = [];
|
|
800
|
+
if (headless) {
|
|
801
|
+
args.push("-p");
|
|
802
|
+
args.push("--output-format", "stream-json");
|
|
803
|
+
args.push("--verbose");
|
|
804
|
+
}
|
|
805
|
+
if (model) {
|
|
806
|
+
args.push("--model", model);
|
|
807
|
+
}
|
|
808
|
+
if (permissionMode && permissionMode !== "default") {
|
|
809
|
+
args.push("--permission-mode", permissionMode);
|
|
810
|
+
}
|
|
811
|
+
if (addDir) {
|
|
812
|
+
args.push("--add-dir", addDir);
|
|
813
|
+
}
|
|
814
|
+
args.push("--add-dir", "/tmp");
|
|
815
|
+
if (appendSystemPrompt) {
|
|
816
|
+
args.push("--append-system-prompt", appendSystemPrompt);
|
|
817
|
+
}
|
|
818
|
+
if (mcpConfig && mcpConfig.length > 0) {
|
|
819
|
+
for (const config of mcpConfig) {
|
|
820
|
+
args.push("--mcp-config", JSON.stringify(config));
|
|
821
|
+
}
|
|
822
|
+
}
|
|
823
|
+
if (allowedTools && allowedTools.length > 0) {
|
|
824
|
+
args.push("--allowed-tools", ...allowedTools);
|
|
825
|
+
}
|
|
826
|
+
if (disallowedTools && disallowedTools.length > 0) {
|
|
827
|
+
args.push("--disallowed-tools", ...disallowedTools);
|
|
828
|
+
}
|
|
829
|
+
if (agents) {
|
|
830
|
+
args.push("--agents", JSON.stringify(agents));
|
|
831
|
+
}
|
|
832
|
+
try {
|
|
833
|
+
if (headless) {
|
|
834
|
+
const isDebugMode = logger.isDebugEnabled();
|
|
835
|
+
const execaOptions = {
|
|
836
|
+
input: prompt,
|
|
837
|
+
timeout: 0,
|
|
838
|
+
// Disable timeout for long responses
|
|
839
|
+
...addDir && { cwd: addDir },
|
|
840
|
+
// Run Claude in the worktree directory
|
|
841
|
+
verbose: isDebugMode,
|
|
842
|
+
...isDebugMode && { stdio: ["pipe", "pipe", "pipe"] }
|
|
843
|
+
// Enable streaming in debug mode
|
|
844
|
+
};
|
|
845
|
+
const subprocess = execa3("claude", args, execaOptions);
|
|
846
|
+
const isJsonStreamFormat = args.includes("--output-format") && args.includes("stream-json");
|
|
847
|
+
let outputBuffer = "";
|
|
848
|
+
let isStreaming = false;
|
|
849
|
+
let isFirstProgress = true;
|
|
850
|
+
if (subprocess.stdout && typeof subprocess.stdout.on === "function") {
|
|
851
|
+
isStreaming = true;
|
|
852
|
+
subprocess.stdout.on("data", (chunk) => {
|
|
853
|
+
const text = chunk.toString();
|
|
854
|
+
outputBuffer += text;
|
|
855
|
+
if (isDebugMode) {
|
|
856
|
+
process.stdout.write(text);
|
|
857
|
+
} else {
|
|
858
|
+
if (isFirstProgress) {
|
|
859
|
+
process.stdout.write("\u{1F916} .");
|
|
860
|
+
isFirstProgress = false;
|
|
861
|
+
} else {
|
|
862
|
+
process.stdout.write(".");
|
|
863
|
+
}
|
|
864
|
+
}
|
|
865
|
+
});
|
|
866
|
+
}
|
|
867
|
+
const result = await subprocess;
|
|
868
|
+
if (isStreaming) {
|
|
869
|
+
const rawOutput = outputBuffer.trim();
|
|
870
|
+
if (!isDebugMode) {
|
|
871
|
+
process.stdout.write("\n");
|
|
872
|
+
}
|
|
873
|
+
return isJsonStreamFormat ? parseJsonStreamOutput(rawOutput) : rawOutput;
|
|
874
|
+
} else {
|
|
875
|
+
if (isDebugMode) {
|
|
876
|
+
process.stdout.write(result.stdout);
|
|
877
|
+
if (result.stdout && !result.stdout.endsWith("\n")) {
|
|
878
|
+
process.stdout.write("\n");
|
|
879
|
+
}
|
|
880
|
+
} else {
|
|
881
|
+
process.stdout.write("\u{1F916} .");
|
|
882
|
+
process.stdout.write("\n");
|
|
883
|
+
}
|
|
884
|
+
const rawOutput = result.stdout.trim();
|
|
885
|
+
return isJsonStreamFormat ? parseJsonStreamOutput(rawOutput) : rawOutput;
|
|
886
|
+
}
|
|
887
|
+
} else {
|
|
888
|
+
await execa3("claude", [...args, "--", prompt], {
|
|
889
|
+
...addDir && { cwd: addDir },
|
|
890
|
+
stdio: "inherit",
|
|
891
|
+
// Let user interact directly in current terminal
|
|
892
|
+
timeout: 0,
|
|
893
|
+
// Disable timeout
|
|
894
|
+
verbose: logger.isDebugEnabled()
|
|
895
|
+
});
|
|
896
|
+
return;
|
|
897
|
+
}
|
|
898
|
+
} catch (error) {
|
|
899
|
+
const execaError = error;
|
|
900
|
+
const errorMessage = execaError.stderr ?? execaError.message ?? "Unknown Claude CLI error";
|
|
901
|
+
throw new Error(`Claude CLI error: ${errorMessage}`);
|
|
902
|
+
}
|
|
903
|
+
}
|
|
904
|
+
async function launchClaudeInNewTerminalWindow(_prompt, options) {
|
|
905
|
+
const { workspacePath, branchName, oneShot = "default", port, setArguments, executablePath } = options;
|
|
906
|
+
if (!workspacePath) {
|
|
907
|
+
throw new Error("workspacePath is required for terminal window launch");
|
|
908
|
+
}
|
|
909
|
+
const { openTerminalWindow: openTerminalWindow2 } = await Promise.resolve().then(() => (init_terminal(), terminal_exports));
|
|
910
|
+
const executable = executablePath ?? "iloom";
|
|
911
|
+
let launchCommand = `${executable} spin`;
|
|
912
|
+
if (oneShot !== "default") {
|
|
913
|
+
launchCommand += ` --one-shot=${oneShot}`;
|
|
914
|
+
}
|
|
915
|
+
if (setArguments && setArguments.length > 0) {
|
|
916
|
+
for (const setArg of setArguments) {
|
|
917
|
+
launchCommand += ` --set ${setArg}`;
|
|
918
|
+
}
|
|
919
|
+
}
|
|
920
|
+
let backgroundColor;
|
|
921
|
+
if (branchName) {
|
|
922
|
+
try {
|
|
923
|
+
const { generateColorFromBranchName: generateColorFromBranchName2 } = await Promise.resolve().then(() => (init_color(), color_exports));
|
|
924
|
+
const colorData = generateColorFromBranchName2(branchName);
|
|
925
|
+
backgroundColor = colorData.rgb;
|
|
926
|
+
} catch (error) {
|
|
927
|
+
logger.warn(
|
|
928
|
+
`Failed to generate terminal color: ${error instanceof Error ? error.message : "Unknown error"}`
|
|
929
|
+
);
|
|
930
|
+
}
|
|
931
|
+
}
|
|
932
|
+
const hasEnvFile = existsSync2(join(workspacePath, ".env"));
|
|
933
|
+
await openTerminalWindow2({
|
|
934
|
+
workspacePath,
|
|
935
|
+
command: launchCommand,
|
|
936
|
+
...backgroundColor && { backgroundColor },
|
|
937
|
+
includeEnvSetup: hasEnvFile,
|
|
938
|
+
// source .env only if it exists
|
|
939
|
+
...port !== void 0 && { port, includePortExport: true }
|
|
940
|
+
});
|
|
941
|
+
}
|
|
942
|
+
async function generateBranchName(issueTitle, issueNumber, model = "haiku") {
|
|
943
|
+
try {
|
|
944
|
+
const isAvailable = await detectClaudeCli();
|
|
945
|
+
if (!isAvailable) {
|
|
946
|
+
logger.warn("Claude CLI not available, using fallback branch name");
|
|
947
|
+
return `feat/issue-${issueNumber}`;
|
|
948
|
+
}
|
|
949
|
+
logger.debug("Generating branch name with Claude", { issueNumber, issueTitle });
|
|
950
|
+
const prompt = `<Task>
|
|
951
|
+
Generate a git branch name for the following issue:
|
|
952
|
+
<Issue>
|
|
953
|
+
<IssueNumber>${issueNumber}</IssueNumber>
|
|
954
|
+
<IssueTitle>${issueTitle}</IssueTitle>
|
|
955
|
+
</Issue>
|
|
956
|
+
|
|
957
|
+
<Requirements>
|
|
958
|
+
<IssueNumber>Must use this exact issue number: ${issueNumber}</IssueNumber>
|
|
959
|
+
<Format>Format must be: {prefix}/issue-${issueNumber}-{description}</Format>
|
|
960
|
+
<Prefix>Prefix must be one of: feat, fix, docs, refactor, test, chore</Prefix>
|
|
961
|
+
<MaxLength>Maximum 50 characters total</MaxLength>
|
|
962
|
+
<Characters>Only lowercase letters, numbers, and hyphens allowed</Characters>
|
|
963
|
+
<Output>Reply with ONLY the branch name, nothing else</Output>
|
|
964
|
+
</Requirements>
|
|
965
|
+
</Task>`;
|
|
966
|
+
logger.debug("Sending prompt to Claude", { prompt });
|
|
967
|
+
const result = await launchClaude(prompt, {
|
|
968
|
+
model,
|
|
969
|
+
headless: true
|
|
970
|
+
});
|
|
971
|
+
const branchName = result.trim();
|
|
972
|
+
logger.debug("Claude returned branch name", { branchName, issueNumber });
|
|
973
|
+
if (!branchName || !isValidBranchName(branchName, issueNumber)) {
|
|
974
|
+
logger.warn("Invalid branch name from Claude, using fallback", { branchName });
|
|
975
|
+
return `feat/issue-${issueNumber}`;
|
|
976
|
+
}
|
|
977
|
+
return branchName;
|
|
978
|
+
} catch (error) {
|
|
979
|
+
logger.warn("Failed to generate branch name with Claude", { error });
|
|
980
|
+
return `feat/issue-${issueNumber}`;
|
|
981
|
+
}
|
|
982
|
+
}
|
|
983
|
+
function isValidBranchName(name, issueNumber) {
|
|
984
|
+
const pattern = new RegExp(`^(feat|fix|docs|refactor|test|chore)/issue-${issueNumber}-[a-z0-9-]+$`);
|
|
985
|
+
return pattern.test(name) && name.length <= 50;
|
|
986
|
+
}
|
|
987
|
+
var init_claude = __esm({
|
|
988
|
+
"src/utils/claude.ts"() {
|
|
989
|
+
"use strict";
|
|
990
|
+
init_logger();
|
|
991
|
+
}
|
|
992
|
+
});
|
|
993
|
+
|
|
994
|
+
// src/lib/WorkspaceManager.ts
|
|
995
|
+
var WorkspaceManager = class {
|
|
996
|
+
// TODO: Implement in Issue #6
|
|
997
|
+
};
|
|
998
|
+
|
|
999
|
+
// src/lib/GitWorktreeManager.ts
|
|
1000
|
+
import path3 from "path";
|
|
1001
|
+
import fs from "fs-extra";
|
|
1002
|
+
|
|
1003
|
+
// src/utils/git.ts
|
|
1004
|
+
init_logger();
|
|
1005
|
+
import path2 from "path";
|
|
1006
|
+
import { execa } from "execa";
|
|
1007
|
+
async function executeGitCommand(args, options) {
|
|
1008
|
+
try {
|
|
1009
|
+
const result = await execa("git", args, {
|
|
1010
|
+
cwd: (options == null ? void 0 : options.cwd) ?? process.cwd(),
|
|
1011
|
+
timeout: (options == null ? void 0 : options.timeout) ?? 3e4,
|
|
1012
|
+
encoding: "utf8",
|
|
1013
|
+
stdio: (options == null ? void 0 : options.stdio) ?? "pipe",
|
|
1014
|
+
verbose: logger.isDebugEnabled()
|
|
1015
|
+
});
|
|
1016
|
+
return result.stdout;
|
|
1017
|
+
} catch (error) {
|
|
1018
|
+
const execaError = error;
|
|
1019
|
+
const stderr = execaError.stderr ?? execaError.message ?? "Unknown Git error";
|
|
1020
|
+
throw new Error(`Git command failed: ${stderr}`);
|
|
1021
|
+
}
|
|
1022
|
+
}
|
|
1023
|
+
function parseWorktreeList(output, defaultBranch) {
|
|
1024
|
+
var _a, _b;
|
|
1025
|
+
const worktrees = [];
|
|
1026
|
+
const lines = output.trim().split("\n");
|
|
1027
|
+
let i = 0;
|
|
1028
|
+
while (i < lines.length) {
|
|
1029
|
+
const pathLine = lines[i];
|
|
1030
|
+
if (!(pathLine == null ? void 0 : pathLine.startsWith("worktree "))) {
|
|
1031
|
+
i++;
|
|
1032
|
+
continue;
|
|
1033
|
+
}
|
|
1034
|
+
const pathMatch = pathLine.match(/^worktree (.+)$/);
|
|
1035
|
+
if (!pathMatch) {
|
|
1036
|
+
i++;
|
|
1037
|
+
continue;
|
|
1038
|
+
}
|
|
1039
|
+
let branch = "";
|
|
1040
|
+
let commit = "";
|
|
1041
|
+
let detached = false;
|
|
1042
|
+
let bare = false;
|
|
1043
|
+
let locked = false;
|
|
1044
|
+
let lockReason;
|
|
1045
|
+
i++;
|
|
1046
|
+
while (i < lines.length && !((_a = lines[i]) == null ? void 0 : _a.startsWith("worktree "))) {
|
|
1047
|
+
const line = (_b = lines[i]) == null ? void 0 : _b.trim();
|
|
1048
|
+
if (!line) {
|
|
1049
|
+
i++;
|
|
1050
|
+
continue;
|
|
1051
|
+
}
|
|
1052
|
+
if (line === "bare") {
|
|
1053
|
+
bare = true;
|
|
1054
|
+
branch = defaultBranch ?? "main";
|
|
1055
|
+
} else if (line === "detached") {
|
|
1056
|
+
detached = true;
|
|
1057
|
+
branch = "HEAD";
|
|
1058
|
+
} else if (line.startsWith("locked")) {
|
|
1059
|
+
locked = true;
|
|
1060
|
+
const lockMatch = line.match(/^locked (.+)$/);
|
|
1061
|
+
lockReason = lockMatch == null ? void 0 : lockMatch[1];
|
|
1062
|
+
branch = branch || "unknown";
|
|
1063
|
+
} else if (line.startsWith("HEAD ")) {
|
|
1064
|
+
const commitMatch = line.match(/^HEAD ([a-f0-9]+)/);
|
|
1065
|
+
if (commitMatch) {
|
|
1066
|
+
commit = commitMatch[1] ?? "";
|
|
1067
|
+
}
|
|
1068
|
+
} else if (line.startsWith("branch ")) {
|
|
1069
|
+
const branchMatch = line.match(/^branch refs\/heads\/(.+)$/);
|
|
1070
|
+
branch = (branchMatch == null ? void 0 : branchMatch[1]) ?? line.replace("branch ", "");
|
|
1071
|
+
}
|
|
1072
|
+
i++;
|
|
1073
|
+
}
|
|
1074
|
+
const worktree = {
|
|
1075
|
+
path: pathMatch[1] ?? "",
|
|
1076
|
+
branch,
|
|
1077
|
+
commit,
|
|
1078
|
+
bare,
|
|
1079
|
+
detached,
|
|
1080
|
+
locked
|
|
1081
|
+
};
|
|
1082
|
+
if (lockReason !== void 0) {
|
|
1083
|
+
worktree.lockReason = lockReason;
|
|
1084
|
+
}
|
|
1085
|
+
worktrees.push(worktree);
|
|
1086
|
+
}
|
|
1087
|
+
return worktrees;
|
|
1088
|
+
}
|
|
1089
|
+
function isPRBranch(branchName) {
|
|
1090
|
+
const prPatterns = [
|
|
1091
|
+
/^pr\/\d+/i,
|
|
1092
|
+
// pr/123, pr/123-feature-name
|
|
1093
|
+
/^pull\/\d+/i,
|
|
1094
|
+
// pull/123
|
|
1095
|
+
/^\d+[-_]/,
|
|
1096
|
+
// 123-feature-name, 123_feature_name
|
|
1097
|
+
/^feature\/pr[-_]?\d+/i,
|
|
1098
|
+
// feature/pr123, feature/pr-123
|
|
1099
|
+
/^hotfix\/pr[-_]?\d+/i
|
|
1100
|
+
// hotfix/pr123
|
|
1101
|
+
];
|
|
1102
|
+
return prPatterns.some((pattern) => pattern.test(branchName));
|
|
1103
|
+
}
|
|
1104
|
+
function extractPRNumber(branchName) {
|
|
1105
|
+
const patterns = [
|
|
1106
|
+
/^pr\/(\d+)/i,
|
|
1107
|
+
// pr/123
|
|
1108
|
+
/^pull\/(\d+)/i,
|
|
1109
|
+
// pull/123
|
|
1110
|
+
/^(\d+)[-_]/,
|
|
1111
|
+
// 123-feature-name
|
|
1112
|
+
/^feature\/pr[-_]?(\d+)/i,
|
|
1113
|
+
// feature/pr123
|
|
1114
|
+
/^hotfix\/pr[-_]?(\d+)/i,
|
|
1115
|
+
// hotfix/pr123
|
|
1116
|
+
/pr[-_]?(\d+)/i
|
|
1117
|
+
// anywhere with pr123 or pr-123
|
|
1118
|
+
];
|
|
1119
|
+
for (const pattern of patterns) {
|
|
1120
|
+
const match = branchName.match(pattern);
|
|
1121
|
+
if (match == null ? void 0 : match[1]) {
|
|
1122
|
+
const num = parseInt(match[1], 10);
|
|
1123
|
+
if (!isNaN(num)) return num;
|
|
1124
|
+
}
|
|
1125
|
+
}
|
|
1126
|
+
return null;
|
|
1127
|
+
}
|
|
1128
|
+
function isWorktreePath(path5) {
|
|
1129
|
+
const worktreePatterns = [
|
|
1130
|
+
/\/worktrees?\//i,
|
|
1131
|
+
// Contains /worktree/ or /worktrees/
|
|
1132
|
+
/\/workspace[-_]?\d+/i,
|
|
1133
|
+
// workspace123, workspace-123
|
|
1134
|
+
/\/issue[-_]?\d+/i,
|
|
1135
|
+
// issue123, issue-123
|
|
1136
|
+
/\/pr[-_]?\d+/i,
|
|
1137
|
+
// pr123, pr-123
|
|
1138
|
+
/-worktree$/i,
|
|
1139
|
+
// ends with -worktree
|
|
1140
|
+
/\.worktree$/i
|
|
1141
|
+
// ends with .worktree
|
|
1142
|
+
];
|
|
1143
|
+
return worktreePatterns.some((pattern) => pattern.test(path5));
|
|
1144
|
+
}
|
|
1145
|
+
function generateWorktreePath(branchName, rootDir = process.cwd(), options) {
|
|
1146
|
+
let sanitized = branchName.replace(/\//g, "-");
|
|
1147
|
+
if ((options == null ? void 0 : options.isPR) && (options == null ? void 0 : options.prNumber)) {
|
|
1148
|
+
sanitized = `${sanitized}_pr_${options.prNumber}`;
|
|
1149
|
+
}
|
|
1150
|
+
const parentDir = path2.dirname(rootDir);
|
|
1151
|
+
let prefix;
|
|
1152
|
+
if ((options == null ? void 0 : options.prefix) === void 0) {
|
|
1153
|
+
const mainFolderName = path2.basename(rootDir);
|
|
1154
|
+
prefix = mainFolderName ? `${mainFolderName}-looms/` : "looms/";
|
|
1155
|
+
} else if (options.prefix === "") {
|
|
1156
|
+
prefix = "";
|
|
1157
|
+
} else {
|
|
1158
|
+
prefix = options.prefix;
|
|
1159
|
+
const hasNestedPath = prefix.includes("/");
|
|
1160
|
+
if (hasNestedPath) {
|
|
1161
|
+
const endsWithSeparator = /[-_/]$/.test(prefix);
|
|
1162
|
+
if (!endsWithSeparator) {
|
|
1163
|
+
prefix = `${prefix}-`;
|
|
1164
|
+
}
|
|
1165
|
+
} else {
|
|
1166
|
+
const endsWithSeparator = /[-_]$/.test(prefix);
|
|
1167
|
+
if (!endsWithSeparator) {
|
|
1168
|
+
prefix = `${prefix}-`;
|
|
1169
|
+
}
|
|
1170
|
+
}
|
|
1171
|
+
}
|
|
1172
|
+
if (prefix === "") {
|
|
1173
|
+
return path2.join(parentDir, sanitized);
|
|
1174
|
+
} else if (prefix.endsWith("/")) {
|
|
1175
|
+
return path2.join(parentDir, prefix, sanitized);
|
|
1176
|
+
} else if (prefix.includes("/")) {
|
|
1177
|
+
const lastSlashIndex = prefix.lastIndexOf("/");
|
|
1178
|
+
const dirPath = prefix.substring(0, lastSlashIndex);
|
|
1179
|
+
const prefixWithSeparator = prefix.substring(lastSlashIndex + 1);
|
|
1180
|
+
return path2.join(parentDir, dirPath, `${prefixWithSeparator}${sanitized}`);
|
|
1181
|
+
} else {
|
|
1182
|
+
return path2.join(parentDir, `${prefix}${sanitized}`);
|
|
1183
|
+
}
|
|
1184
|
+
}
|
|
1185
|
+
async function isValidGitRepo(path5) {
|
|
1186
|
+
try {
|
|
1187
|
+
await executeGitCommand(["rev-parse", "--git-dir"], { cwd: path5 });
|
|
1188
|
+
return true;
|
|
1189
|
+
} catch {
|
|
1190
|
+
return false;
|
|
1191
|
+
}
|
|
1192
|
+
}
|
|
1193
|
+
async function getCurrentBranch(path5 = process.cwd()) {
|
|
1194
|
+
try {
|
|
1195
|
+
const result = await executeGitCommand(["branch", "--show-current"], { cwd: path5 });
|
|
1196
|
+
return result.trim();
|
|
1197
|
+
} catch {
|
|
1198
|
+
return null;
|
|
1199
|
+
}
|
|
1200
|
+
}
|
|
1201
|
+
async function branchExists(branchName, path5 = process.cwd(), includeRemote = true) {
|
|
1202
|
+
try {
|
|
1203
|
+
const localResult = await executeGitCommand(["branch", "--list", branchName], { cwd: path5 });
|
|
1204
|
+
if (localResult.trim()) {
|
|
1205
|
+
return true;
|
|
1206
|
+
}
|
|
1207
|
+
if (includeRemote) {
|
|
1208
|
+
const remoteResult = await executeGitCommand(["branch", "-r", "--list", `*/${branchName}`], {
|
|
1209
|
+
cwd: path5
|
|
1210
|
+
});
|
|
1211
|
+
if (remoteResult.trim()) {
|
|
1212
|
+
return true;
|
|
1213
|
+
}
|
|
1214
|
+
}
|
|
1215
|
+
return false;
|
|
1216
|
+
} catch {
|
|
1217
|
+
return false;
|
|
1218
|
+
}
|
|
1219
|
+
}
|
|
1220
|
+
async function getRepoRoot(path5 = process.cwd()) {
|
|
1221
|
+
try {
|
|
1222
|
+
const result = await executeGitCommand(["rev-parse", "--show-toplevel"], { cwd: path5 });
|
|
1223
|
+
return result.trim();
|
|
1224
|
+
} catch {
|
|
1225
|
+
return null;
|
|
1226
|
+
}
|
|
1227
|
+
}
|
|
1228
|
+
async function findMainWorktreePath(path5 = process.cwd(), options) {
|
|
1229
|
+
try {
|
|
1230
|
+
const output = await executeGitCommand(["worktree", "list", "--porcelain"], { cwd: path5 });
|
|
1231
|
+
const worktrees = parseWorktreeList(output, options == null ? void 0 : options.mainBranch);
|
|
1232
|
+
if (worktrees.length === 0) {
|
|
1233
|
+
throw new Error("No worktrees found in repository");
|
|
1234
|
+
}
|
|
1235
|
+
if (options == null ? void 0 : options.mainBranch) {
|
|
1236
|
+
const specified = worktrees.find((wt) => wt.branch === options.mainBranch);
|
|
1237
|
+
if (!(specified == null ? void 0 : specified.path)) {
|
|
1238
|
+
throw new Error(
|
|
1239
|
+
`No worktree found with branch '${options.mainBranch}' (specified in settings). Available worktrees: ${worktrees.map((wt) => `${wt.path} (${wt.branch})`).join(", ")}`
|
|
1240
|
+
);
|
|
1241
|
+
}
|
|
1242
|
+
return specified.path;
|
|
1243
|
+
}
|
|
1244
|
+
const mainBranch = worktrees.find((wt) => wt.branch === "main");
|
|
1245
|
+
if (mainBranch == null ? void 0 : mainBranch.path) {
|
|
1246
|
+
return mainBranch.path;
|
|
1247
|
+
}
|
|
1248
|
+
const firstWorktree = worktrees[0];
|
|
1249
|
+
if (!(firstWorktree == null ? void 0 : firstWorktree.path)) {
|
|
1250
|
+
throw new Error("Failed to determine primary worktree path");
|
|
1251
|
+
}
|
|
1252
|
+
return firstWorktree.path;
|
|
1253
|
+
} catch (error) {
|
|
1254
|
+
if (error instanceof Error && (error.message.includes("No worktree found with branch") || error.message.includes("No worktrees found") || error.message.includes("Failed to determine primary worktree"))) {
|
|
1255
|
+
throw error;
|
|
1256
|
+
}
|
|
1257
|
+
throw new Error(`Failed to find main worktree: ${error instanceof Error ? error.message : String(error)}`);
|
|
1258
|
+
}
|
|
1259
|
+
}
|
|
1260
|
+
async function findMainWorktreePathWithSettings(path5, settingsManager) {
|
|
1261
|
+
if (!settingsManager) {
|
|
1262
|
+
const { SettingsManager: SM } = await Promise.resolve().then(() => (init_SettingsManager(), SettingsManager_exports));
|
|
1263
|
+
settingsManager = new SM();
|
|
1264
|
+
}
|
|
1265
|
+
const settings = await settingsManager.loadSettings(path5);
|
|
1266
|
+
const findOptions = settings.mainBranch ? { mainBranch: settings.mainBranch } : void 0;
|
|
1267
|
+
return findMainWorktreePath(path5, findOptions);
|
|
1268
|
+
}
|
|
1269
|
+
async function hasUncommittedChanges(path5 = process.cwd()) {
|
|
1270
|
+
try {
|
|
1271
|
+
const result = await executeGitCommand(["status", "--porcelain"], { cwd: path5 });
|
|
1272
|
+
return result.trim().length > 0;
|
|
1273
|
+
} catch {
|
|
1274
|
+
return false;
|
|
1275
|
+
}
|
|
1276
|
+
}
|
|
1277
|
+
async function getDefaultBranch(path5 = process.cwd()) {
|
|
1278
|
+
try {
|
|
1279
|
+
const remoteResult = await executeGitCommand(["symbolic-ref", "refs/remotes/origin/HEAD"], {
|
|
1280
|
+
cwd: path5
|
|
1281
|
+
});
|
|
1282
|
+
const match = remoteResult.match(/refs\/remotes\/origin\/(.+)/);
|
|
1283
|
+
if (match) return match[1] ?? "main";
|
|
1284
|
+
const commonDefaults = ["main", "master", "develop"];
|
|
1285
|
+
for (const branch of commonDefaults) {
|
|
1286
|
+
if (await branchExists(branch, path5)) {
|
|
1287
|
+
return branch;
|
|
1288
|
+
}
|
|
1289
|
+
}
|
|
1290
|
+
return "main";
|
|
1291
|
+
} catch {
|
|
1292
|
+
return "main";
|
|
1293
|
+
}
|
|
1294
|
+
}
|
|
1295
|
+
async function findAllBranchesForIssue(issueNumber, path5 = process.cwd(), settingsManager) {
|
|
1296
|
+
if (!settingsManager) {
|
|
1297
|
+
const { SettingsManager: SM } = await Promise.resolve().then(() => (init_SettingsManager(), SettingsManager_exports));
|
|
1298
|
+
settingsManager = new SM();
|
|
1299
|
+
}
|
|
1300
|
+
const protectedBranches = await settingsManager.getProtectedBranches(path5);
|
|
1301
|
+
const output = await executeGitCommand(["branch", "-a"], { cwd: path5 });
|
|
1302
|
+
const branches = [];
|
|
1303
|
+
const lines = output.split("\n").filter(Boolean);
|
|
1304
|
+
for (const line of lines) {
|
|
1305
|
+
if (line.includes("remotes/origin/HEAD")) {
|
|
1306
|
+
continue;
|
|
1307
|
+
}
|
|
1308
|
+
let cleanBranch = line.replace(/^[*+ ]+/, "");
|
|
1309
|
+
cleanBranch = cleanBranch.replace(/^origin\//, "");
|
|
1310
|
+
cleanBranch = cleanBranch.replace(/^remotes\/origin\//, "");
|
|
1311
|
+
cleanBranch = cleanBranch.trim();
|
|
1312
|
+
if (protectedBranches.includes(cleanBranch)) {
|
|
1313
|
+
continue;
|
|
1314
|
+
}
|
|
1315
|
+
const notPartOfNumber = new RegExp(`(?<!\\d)${issueNumber}(?!\\d)`);
|
|
1316
|
+
if (!notPartOfNumber.test(cleanBranch)) {
|
|
1317
|
+
continue;
|
|
1318
|
+
}
|
|
1319
|
+
const beforeNumber = cleanBranch.substring(0, cleanBranch.indexOf(String(issueNumber)));
|
|
1320
|
+
if (beforeNumber) {
|
|
1321
|
+
const lastWord = beforeNumber.match(/([a-zA-Z]+)[-_/\s]*$/);
|
|
1322
|
+
if (lastWord == null ? void 0 : lastWord[1]) {
|
|
1323
|
+
const word = lastWord[1].toLowerCase();
|
|
1324
|
+
const knownPrefixes = [
|
|
1325
|
+
"issue",
|
|
1326
|
+
"issues",
|
|
1327
|
+
"feat",
|
|
1328
|
+
"feature",
|
|
1329
|
+
"features",
|
|
1330
|
+
"fix",
|
|
1331
|
+
"fixes",
|
|
1332
|
+
"bugfix",
|
|
1333
|
+
"hotfix",
|
|
1334
|
+
"pr",
|
|
1335
|
+
"pull",
|
|
1336
|
+
"test",
|
|
1337
|
+
"tests",
|
|
1338
|
+
"chore",
|
|
1339
|
+
"docs",
|
|
1340
|
+
"refactor",
|
|
1341
|
+
"perf",
|
|
1342
|
+
"style",
|
|
1343
|
+
"ci",
|
|
1344
|
+
"build",
|
|
1345
|
+
"revert"
|
|
1346
|
+
];
|
|
1347
|
+
if (!knownPrefixes.includes(word)) {
|
|
1348
|
+
continue;
|
|
1349
|
+
}
|
|
1350
|
+
}
|
|
1351
|
+
}
|
|
1352
|
+
if (!branches.includes(cleanBranch)) {
|
|
1353
|
+
branches.push(cleanBranch);
|
|
1354
|
+
}
|
|
1355
|
+
}
|
|
1356
|
+
return branches;
|
|
1357
|
+
}
|
|
1358
|
+
async function isEmptyRepository(path5 = process.cwd()) {
|
|
1359
|
+
try {
|
|
1360
|
+
await executeGitCommand(["rev-parse", "--verify", "HEAD"], { cwd: path5 });
|
|
1361
|
+
return false;
|
|
1362
|
+
} catch {
|
|
1363
|
+
return true;
|
|
1364
|
+
}
|
|
1365
|
+
}
|
|
1366
|
+
async function ensureRepositoryHasCommits(path5 = process.cwd()) {
|
|
1367
|
+
const isEmpty = await isEmptyRepository(path5);
|
|
1368
|
+
if (isEmpty) {
|
|
1369
|
+
await executeGitCommand(["commit", "--no-verify", "--allow-empty", "-m", "Initial commit"], { cwd: path5 });
|
|
1370
|
+
}
|
|
1371
|
+
}
|
|
1372
|
+
async function pushBranchToRemote(branchName, worktreePath, options) {
|
|
1373
|
+
if (options == null ? void 0 : options.dryRun) {
|
|
1374
|
+
return;
|
|
1375
|
+
}
|
|
1376
|
+
try {
|
|
1377
|
+
await executeGitCommand(["push", "origin", branchName], {
|
|
1378
|
+
cwd: worktreePath,
|
|
1379
|
+
timeout: 12e4
|
|
1380
|
+
// 120 second timeout for push operations
|
|
1381
|
+
});
|
|
1382
|
+
} catch (error) {
|
|
1383
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
1384
|
+
if (errorMessage.includes("failed to push") || errorMessage.includes("rejected")) {
|
|
1385
|
+
throw new Error(
|
|
1386
|
+
`Failed to push changes to origin/${branchName}
|
|
1387
|
+
|
|
1388
|
+
Possible causes:
|
|
1389
|
+
\u2022 Remote branch was deleted
|
|
1390
|
+
\u2022 Push was rejected (non-fast-forward)
|
|
1391
|
+
\u2022 Network connectivity issues
|
|
1392
|
+
|
|
1393
|
+
To retry: il finish --pr <number>
|
|
1394
|
+
To force push: git push origin ${branchName} --force`
|
|
1395
|
+
);
|
|
1396
|
+
}
|
|
1397
|
+
if (errorMessage.includes("Could not resolve host") || errorMessage.includes("network")) {
|
|
1398
|
+
throw new Error(
|
|
1399
|
+
`Failed to push changes to origin/${branchName}: Network connectivity issues
|
|
1400
|
+
|
|
1401
|
+
Check your internet connection and try again.`
|
|
1402
|
+
);
|
|
1403
|
+
}
|
|
1404
|
+
if (errorMessage.includes("No such remote")) {
|
|
1405
|
+
throw new Error(
|
|
1406
|
+
`Failed to push changes: Remote 'origin' not found
|
|
1407
|
+
|
|
1408
|
+
Configure remote: git remote add origin <url>`
|
|
1409
|
+
);
|
|
1410
|
+
}
|
|
1411
|
+
throw new Error(`Failed to push to remote: ${errorMessage}`);
|
|
1412
|
+
}
|
|
1413
|
+
}
|
|
1414
|
+
|
|
1415
|
+
// src/lib/GitWorktreeManager.ts
|
|
1416
|
+
var GitWorktreeManager = class {
|
|
1417
|
+
constructor(workingDirectory = process.cwd()) {
|
|
1418
|
+
this._workingDirectory = workingDirectory;
|
|
1419
|
+
}
|
|
1420
|
+
/**
|
|
1421
|
+
* Get the working directory for git operations (main worktree path)
|
|
1422
|
+
*/
|
|
1423
|
+
get workingDirectory() {
|
|
1424
|
+
return this._workingDirectory;
|
|
1425
|
+
}
|
|
1426
|
+
/**
|
|
1427
|
+
* List all worktrees in the repository
|
|
1428
|
+
* Defaults to porcelain format for reliable machine parsing
|
|
1429
|
+
* Equivalent to: git worktree list --porcelain
|
|
1430
|
+
*/
|
|
1431
|
+
async listWorktrees(options = {}) {
|
|
1432
|
+
const args = ["worktree", "list"];
|
|
1433
|
+
if (options.porcelain !== false) args.push("--porcelain");
|
|
1434
|
+
if (options.verbose) args.push("-v");
|
|
1435
|
+
const output = await executeGitCommand(args, { cwd: this._workingDirectory });
|
|
1436
|
+
return parseWorktreeList(output);
|
|
1437
|
+
}
|
|
1438
|
+
/**
|
|
1439
|
+
* Find worktree for a specific branch
|
|
1440
|
+
* Ports: find_worktree_for_branch() from find-worktree-for-branch.sh
|
|
1441
|
+
*/
|
|
1442
|
+
async findWorktreeForBranch(branchName) {
|
|
1443
|
+
const worktrees = await this.listWorktrees();
|
|
1444
|
+
return worktrees.find((wt) => wt.branch === branchName) ?? null;
|
|
1445
|
+
}
|
|
1446
|
+
/**
|
|
1447
|
+
* Check if a worktree is the main repository worktree
|
|
1448
|
+
* The main worktree is the first one listed by git worktree list (Git guarantee)
|
|
1449
|
+
* This cannot be determined by path comparison because --show-toplevel returns
|
|
1450
|
+
* the same value for all worktrees.
|
|
1451
|
+
*/
|
|
1452
|
+
async isMainWorktree(worktree) {
|
|
1453
|
+
const worktrees = await this.listWorktrees();
|
|
1454
|
+
const mainWorktree = worktrees[0];
|
|
1455
|
+
return mainWorktree !== void 0 && mainWorktree.path === worktree.path;
|
|
1456
|
+
}
|
|
1457
|
+
/**
|
|
1458
|
+
* Check if a worktree is a PR worktree based on naming patterns
|
|
1459
|
+
* Ports: is_pr_worktree() from worktree-utils.sh
|
|
1460
|
+
*/
|
|
1461
|
+
isPRWorktree(worktree) {
|
|
1462
|
+
return isPRBranch(worktree.branch);
|
|
1463
|
+
}
|
|
1464
|
+
/**
|
|
1465
|
+
* Get PR number from worktree branch name
|
|
1466
|
+
* Ports: get_pr_number_from_worktree() from worktree-utils.sh
|
|
1467
|
+
*/
|
|
1468
|
+
getPRNumberFromWorktree(worktree) {
|
|
1469
|
+
return extractPRNumber(worktree.branch);
|
|
1470
|
+
}
|
|
1471
|
+
/**
|
|
1472
|
+
* Create a new worktree
|
|
1473
|
+
* Ports worktree creation logic from new-branch-workflow.sh
|
|
1474
|
+
* @returns The absolute path to the created worktree
|
|
1475
|
+
*/
|
|
1476
|
+
async createWorktree(options) {
|
|
1477
|
+
if (!options.branch) {
|
|
1478
|
+
throw new Error("Branch name is required");
|
|
1479
|
+
}
|
|
1480
|
+
const absolutePath = path3.resolve(options.path);
|
|
1481
|
+
if (await fs.pathExists(absolutePath)) {
|
|
1482
|
+
if (!options.force) {
|
|
1483
|
+
throw new Error(`Path already exists: ${absolutePath}`);
|
|
1484
|
+
}
|
|
1485
|
+
await fs.remove(absolutePath);
|
|
1486
|
+
}
|
|
1487
|
+
const args = ["worktree", "add"];
|
|
1488
|
+
if (options.createBranch) {
|
|
1489
|
+
args.push("-b", options.branch);
|
|
1490
|
+
}
|
|
1491
|
+
if (options.force) {
|
|
1492
|
+
args.push("--force");
|
|
1493
|
+
}
|
|
1494
|
+
args.push(absolutePath);
|
|
1495
|
+
if (!options.createBranch) {
|
|
1496
|
+
args.push(options.branch);
|
|
1497
|
+
} else if (options.baseBranch) {
|
|
1498
|
+
args.push(options.baseBranch);
|
|
1499
|
+
}
|
|
1500
|
+
await executeGitCommand(args, { cwd: this._workingDirectory });
|
|
1501
|
+
return absolutePath;
|
|
1502
|
+
}
|
|
1503
|
+
/**
|
|
1504
|
+
* Remove a worktree and optionally clean up associated files
|
|
1505
|
+
* Ports worktree removal logic from cleanup-worktree.sh
|
|
1506
|
+
* @returns A message describing what was done (for dry-run mode)
|
|
1507
|
+
*/
|
|
1508
|
+
async removeWorktree(worktreePath, options = {}) {
|
|
1509
|
+
const worktrees = await this.listWorktrees({ porcelain: true });
|
|
1510
|
+
const worktree = worktrees.find((wt) => wt.path === worktreePath);
|
|
1511
|
+
if (!worktree) {
|
|
1512
|
+
const { logger: logger4 } = await Promise.resolve().then(() => (init_logger(), logger_exports));
|
|
1513
|
+
logger4.debug(`Looking for worktree path: ${worktreePath}`);
|
|
1514
|
+
logger4.debug(`Found ${worktrees.length} worktrees:`);
|
|
1515
|
+
worktrees.forEach((wt, i) => {
|
|
1516
|
+
logger4.debug(` ${i}: path="${wt.path}", branch="${wt.branch}"`);
|
|
1517
|
+
});
|
|
1518
|
+
throw new Error(`Worktree not found: ${worktreePath}`);
|
|
1519
|
+
}
|
|
1520
|
+
if (!options.force && !options.dryRun) {
|
|
1521
|
+
const hasChanges = await hasUncommittedChanges(worktreePath);
|
|
1522
|
+
if (hasChanges) {
|
|
1523
|
+
throw new Error(`Worktree has uncommitted changes: ${worktreePath}. Use --force to override.`);
|
|
1524
|
+
}
|
|
1525
|
+
}
|
|
1526
|
+
if (options.dryRun) {
|
|
1527
|
+
const actions = ["Remove worktree registration"];
|
|
1528
|
+
if (options.removeDirectory) actions.push("Remove directory from disk");
|
|
1529
|
+
if (options.removeBranch) actions.push(`Remove branch: ${worktree.branch}`);
|
|
1530
|
+
return `Would perform: ${actions.join(", ")}`;
|
|
1531
|
+
}
|
|
1532
|
+
const args = ["worktree", "remove"];
|
|
1533
|
+
if (options.force) args.push("--force");
|
|
1534
|
+
args.push(worktreePath);
|
|
1535
|
+
await executeGitCommand(args, { cwd: this._workingDirectory });
|
|
1536
|
+
if (options.removeDirectory && await fs.pathExists(worktreePath)) {
|
|
1537
|
+
await fs.remove(worktreePath);
|
|
1538
|
+
}
|
|
1539
|
+
if (options.removeBranch && !worktree.bare) {
|
|
1540
|
+
try {
|
|
1541
|
+
await executeGitCommand(["branch", "-D", worktree.branch], {
|
|
1542
|
+
cwd: this._workingDirectory
|
|
1543
|
+
});
|
|
1544
|
+
} catch (error) {
|
|
1545
|
+
throw new Error(
|
|
1546
|
+
`Worktree removed but failed to delete branch ${worktree.branch}: ${error instanceof Error ? error.message : "Unknown error"}`
|
|
1547
|
+
);
|
|
1548
|
+
}
|
|
1549
|
+
}
|
|
1550
|
+
}
|
|
1551
|
+
/**
|
|
1552
|
+
* Validate worktree state and integrity
|
|
1553
|
+
*/
|
|
1554
|
+
async validateWorktree(worktreePath) {
|
|
1555
|
+
const issues = [];
|
|
1556
|
+
let existsOnDisk = false;
|
|
1557
|
+
let isValidRepo = false;
|
|
1558
|
+
let hasValidBranch = false;
|
|
1559
|
+
try {
|
|
1560
|
+
existsOnDisk = await fs.pathExists(worktreePath);
|
|
1561
|
+
if (!existsOnDisk) {
|
|
1562
|
+
issues.push("Worktree directory does not exist on disk");
|
|
1563
|
+
}
|
|
1564
|
+
if (existsOnDisk) {
|
|
1565
|
+
isValidRepo = await isValidGitRepo(worktreePath);
|
|
1566
|
+
if (!isValidRepo) {
|
|
1567
|
+
issues.push("Directory is not a valid Git repository");
|
|
1568
|
+
}
|
|
1569
|
+
}
|
|
1570
|
+
if (isValidRepo) {
|
|
1571
|
+
const currentBranch = await getCurrentBranch(worktreePath);
|
|
1572
|
+
hasValidBranch = currentBranch !== null;
|
|
1573
|
+
if (!hasValidBranch) {
|
|
1574
|
+
issues.push("Could not determine current branch");
|
|
1575
|
+
}
|
|
1576
|
+
}
|
|
1577
|
+
const worktrees = await this.listWorktrees();
|
|
1578
|
+
const isRegistered = worktrees.some((wt) => wt.path === worktreePath);
|
|
1579
|
+
if (!isRegistered) {
|
|
1580
|
+
issues.push("Worktree is not registered with Git");
|
|
1581
|
+
}
|
|
1582
|
+
} catch (error) {
|
|
1583
|
+
issues.push(`Validation error: ${error instanceof Error ? error.message : "Unknown error"}`);
|
|
1584
|
+
}
|
|
1585
|
+
return {
|
|
1586
|
+
isValid: issues.length === 0,
|
|
1587
|
+
issues,
|
|
1588
|
+
existsOnDisk,
|
|
1589
|
+
isValidRepo,
|
|
1590
|
+
hasValidBranch
|
|
1591
|
+
};
|
|
1592
|
+
}
|
|
1593
|
+
/**
|
|
1594
|
+
* Get detailed status information for a worktree
|
|
1595
|
+
*/
|
|
1596
|
+
async getWorktreeStatus(worktreePath) {
|
|
1597
|
+
const statusOutput = await executeGitCommand(["status", "--porcelain=v1"], {
|
|
1598
|
+
cwd: worktreePath
|
|
1599
|
+
});
|
|
1600
|
+
let modified = 0;
|
|
1601
|
+
let staged = 0;
|
|
1602
|
+
let deleted = 0;
|
|
1603
|
+
let untracked = 0;
|
|
1604
|
+
const lines = statusOutput.trim().split("\n").filter(Boolean);
|
|
1605
|
+
for (const line of lines) {
|
|
1606
|
+
const status = line.substring(0, 2);
|
|
1607
|
+
if (status[0] === "M" || status[1] === "M") modified++;
|
|
1608
|
+
if (status[0] === "A" || status[0] === "D" || status[0] === "R") staged++;
|
|
1609
|
+
if (status[0] === "D" || status[1] === "D") deleted++;
|
|
1610
|
+
if (status === "??") untracked++;
|
|
1611
|
+
}
|
|
1612
|
+
const currentBranch = await getCurrentBranch(worktreePath) ?? "unknown";
|
|
1613
|
+
const detached = currentBranch === "unknown";
|
|
1614
|
+
let ahead = 0;
|
|
1615
|
+
let behind = 0;
|
|
1616
|
+
try {
|
|
1617
|
+
const aheadBehindOutput = await executeGitCommand(
|
|
1618
|
+
["rev-list", "--left-right", "--count", `origin/${currentBranch}...HEAD`],
|
|
1619
|
+
{ cwd: worktreePath }
|
|
1620
|
+
);
|
|
1621
|
+
const parts = aheadBehindOutput.trim().split(" ");
|
|
1622
|
+
const behindStr = parts[0];
|
|
1623
|
+
const aheadStr = parts[1];
|
|
1624
|
+
behind = behindStr ? parseInt(behindStr, 10) || 0 : 0;
|
|
1625
|
+
ahead = aheadStr ? parseInt(aheadStr, 10) || 0 : 0;
|
|
1626
|
+
} catch {
|
|
1627
|
+
}
|
|
1628
|
+
return {
|
|
1629
|
+
modified,
|
|
1630
|
+
staged,
|
|
1631
|
+
deleted,
|
|
1632
|
+
untracked,
|
|
1633
|
+
hasChanges: modified + staged + deleted + untracked > 0,
|
|
1634
|
+
branch: currentBranch,
|
|
1635
|
+
detached,
|
|
1636
|
+
ahead,
|
|
1637
|
+
behind
|
|
1638
|
+
};
|
|
1639
|
+
}
|
|
1640
|
+
/**
|
|
1641
|
+
* Generate a suggested worktree path for a branch
|
|
1642
|
+
*/
|
|
1643
|
+
generateWorktreePath(branchName, customRoot, options) {
|
|
1644
|
+
const root = customRoot ?? this._workingDirectory;
|
|
1645
|
+
return generateWorktreePath(branchName, root, options);
|
|
1646
|
+
}
|
|
1647
|
+
/**
|
|
1648
|
+
* Sanitize a branch name for use as a directory name
|
|
1649
|
+
* Replaces slashes with dashes and removes invalid filesystem characters
|
|
1650
|
+
* Ports logic from bash script line 593: ${BRANCH_NAME//\\//-}
|
|
1651
|
+
*/
|
|
1652
|
+
sanitizeBranchName(branchName) {
|
|
1653
|
+
return branchName.replace(/\//g, "-").replace(/[^a-zA-Z0-9-]/g, "-").replace(/-+/g, "-").replace(/^-|-$/g, "").toLowerCase();
|
|
1654
|
+
}
|
|
1655
|
+
/**
|
|
1656
|
+
* Check if repository is in a valid state for worktree operations
|
|
1657
|
+
*/
|
|
1658
|
+
async isRepoReady() {
|
|
1659
|
+
try {
|
|
1660
|
+
const repoRoot = await getRepoRoot(this._workingDirectory);
|
|
1661
|
+
return repoRoot !== null;
|
|
1662
|
+
} catch {
|
|
1663
|
+
return false;
|
|
1664
|
+
}
|
|
1665
|
+
}
|
|
1666
|
+
/**
|
|
1667
|
+
* Get repository information
|
|
1668
|
+
*/
|
|
1669
|
+
async getRepoInfo() {
|
|
1670
|
+
const root = await getRepoRoot(this._workingDirectory);
|
|
1671
|
+
const defaultBranch = await getDefaultBranch(this._workingDirectory);
|
|
1672
|
+
const currentBranch = await getCurrentBranch(this._workingDirectory);
|
|
1673
|
+
return {
|
|
1674
|
+
root,
|
|
1675
|
+
defaultBranch,
|
|
1676
|
+
currentBranch
|
|
1677
|
+
};
|
|
1678
|
+
}
|
|
1679
|
+
/**
|
|
1680
|
+
* Prune stale worktree entries (worktrees that no longer exist on disk)
|
|
1681
|
+
*/
|
|
1682
|
+
async pruneWorktrees() {
|
|
1683
|
+
await executeGitCommand(["worktree", "prune", "-v"], { cwd: this._workingDirectory });
|
|
1684
|
+
}
|
|
1685
|
+
/**
|
|
1686
|
+
* Lock a worktree to prevent it from being pruned or moved
|
|
1687
|
+
*/
|
|
1688
|
+
async lockWorktree(worktreePath, reason) {
|
|
1689
|
+
const args = ["worktree", "lock", worktreePath];
|
|
1690
|
+
if (reason) args.push("--reason", reason);
|
|
1691
|
+
await executeGitCommand(args, { cwd: this._workingDirectory });
|
|
1692
|
+
}
|
|
1693
|
+
/**
|
|
1694
|
+
* Unlock a previously locked worktree
|
|
1695
|
+
*/
|
|
1696
|
+
async unlockWorktree(worktreePath) {
|
|
1697
|
+
await executeGitCommand(["worktree", "unlock", worktreePath], { cwd: this._workingDirectory });
|
|
1698
|
+
}
|
|
1699
|
+
/**
|
|
1700
|
+
* Find worktrees matching an identifier (branch name, path, or PR number)
|
|
1701
|
+
*/
|
|
1702
|
+
async findWorktreesByIdentifier(identifier) {
|
|
1703
|
+
const worktrees = await this.listWorktrees({ porcelain: true });
|
|
1704
|
+
return worktrees.filter(
|
|
1705
|
+
(wt) => {
|
|
1706
|
+
var _a;
|
|
1707
|
+
return wt.branch.includes(identifier) || wt.path.includes(identifier) || ((_a = this.getPRNumberFromWorktree(wt)) == null ? void 0 : _a.toString()) === identifier;
|
|
1708
|
+
}
|
|
1709
|
+
);
|
|
1710
|
+
}
|
|
1711
|
+
/**
|
|
1712
|
+
* Find worktree for a specific issue number using exact pattern matching
|
|
1713
|
+
* Matches: issue-{N} at start OR after /, -, _ (but NOT issue-{N}X where X is a digit)
|
|
1714
|
+
* Supports patterns like: issue-44, feat/issue-44-feature, feat-issue-44, bugfix_issue-44, etc.
|
|
1715
|
+
* Avoids false matches like: tissue-44, myissue-44
|
|
1716
|
+
* Ports: find_existing_worktree() from bash script lines 131-165
|
|
1717
|
+
*/
|
|
1718
|
+
async findWorktreeForIssue(issueNumber) {
|
|
1719
|
+
const worktrees = await this.listWorktrees({ porcelain: true });
|
|
1720
|
+
const pattern = new RegExp(`(?:^|[/_-])issue-${issueNumber}(?:-|$)`);
|
|
1721
|
+
return worktrees.find((wt) => pattern.test(wt.branch)) ?? null;
|
|
1722
|
+
}
|
|
1723
|
+
/**
|
|
1724
|
+
* Find worktree for a specific PR by branch name
|
|
1725
|
+
* Ports: find_existing_worktree() for PR type from bash script lines 149-160
|
|
1726
|
+
*/
|
|
1727
|
+
async findWorktreeForPR(prNumber, branchName) {
|
|
1728
|
+
const worktrees = await this.listWorktrees({ porcelain: true });
|
|
1729
|
+
const byBranch = worktrees.find((wt) => wt.branch === branchName);
|
|
1730
|
+
if (byBranch) return byBranch;
|
|
1731
|
+
const pathPattern = new RegExp(`_pr_${prNumber}$`);
|
|
1732
|
+
return worktrees.find((wt) => pathPattern.test(wt.path)) ?? null;
|
|
1733
|
+
}
|
|
1734
|
+
/**
|
|
1735
|
+
* Remove multiple worktrees
|
|
1736
|
+
* Returns a summary of successes and failures
|
|
1737
|
+
* Automatically filters out the main worktree
|
|
1738
|
+
*/
|
|
1739
|
+
async removeWorktrees(worktrees, options = {}) {
|
|
1740
|
+
const successes = [];
|
|
1741
|
+
const failures = [];
|
|
1742
|
+
const skipped = [];
|
|
1743
|
+
for (const worktree of worktrees) {
|
|
1744
|
+
if (await this.isMainWorktree(worktree)) {
|
|
1745
|
+
skipped.push({ worktree, reason: "Cannot remove main worktree" });
|
|
1746
|
+
continue;
|
|
1747
|
+
}
|
|
1748
|
+
try {
|
|
1749
|
+
await this.removeWorktree(worktree.path, {
|
|
1750
|
+
...options,
|
|
1751
|
+
removeDirectory: true
|
|
1752
|
+
});
|
|
1753
|
+
successes.push({ worktree });
|
|
1754
|
+
} catch (error) {
|
|
1755
|
+
failures.push({
|
|
1756
|
+
worktree,
|
|
1757
|
+
error: error instanceof Error ? error.message : "Unknown error"
|
|
1758
|
+
});
|
|
1759
|
+
}
|
|
1760
|
+
}
|
|
1761
|
+
return { successes, failures, skipped };
|
|
1762
|
+
}
|
|
1763
|
+
/**
|
|
1764
|
+
* Format worktree information for display
|
|
1765
|
+
*/
|
|
1766
|
+
formatWorktree(worktree) {
|
|
1767
|
+
const prNumber = this.getPRNumberFromWorktree(worktree);
|
|
1768
|
+
const prLabel = prNumber ? ` (PR #${prNumber})` : "";
|
|
1769
|
+
const bareLabel = worktree.bare ? " [main]" : "";
|
|
1770
|
+
return {
|
|
1771
|
+
title: `${worktree.branch}${prLabel}${bareLabel}`,
|
|
1772
|
+
path: worktree.path,
|
|
1773
|
+
commit: worktree.commit.substring(0, 7)
|
|
1774
|
+
};
|
|
1775
|
+
}
|
|
1776
|
+
};
|
|
1777
|
+
|
|
1778
|
+
// src/types/github.ts
|
|
1779
|
+
var GitHubError = class extends Error {
|
|
1780
|
+
constructor(code, message, details) {
|
|
1781
|
+
super(message);
|
|
1782
|
+
this.code = code;
|
|
1783
|
+
this.details = details;
|
|
1784
|
+
this.name = "GitHubError";
|
|
1785
|
+
}
|
|
1786
|
+
};
|
|
1787
|
+
|
|
1788
|
+
// src/utils/github.ts
|
|
1789
|
+
init_logger();
|
|
1790
|
+
import { execa as execa4 } from "execa";
|
|
1791
|
+
async function executeGhCommand(args, options) {
|
|
1792
|
+
const result = await execa4("gh", args, {
|
|
1793
|
+
cwd: (options == null ? void 0 : options.cwd) ?? process.cwd(),
|
|
1794
|
+
timeout: (options == null ? void 0 : options.timeout) ?? 3e4,
|
|
1795
|
+
encoding: "utf8"
|
|
1796
|
+
});
|
|
1797
|
+
const isJson = args.includes("--json") || args.includes("--jq") || args.includes("--format") && args[args.indexOf("--format") + 1] === "json";
|
|
1798
|
+
const data = isJson ? JSON.parse(result.stdout) : result.stdout;
|
|
1799
|
+
return data;
|
|
1800
|
+
}
|
|
1801
|
+
async function checkGhAuth() {
|
|
1802
|
+
var _a, _b;
|
|
1803
|
+
try {
|
|
1804
|
+
const output = await executeGhCommand(["auth", "status"]);
|
|
1805
|
+
const scopeMatch = output.match(/Token scopes: (.+)/);
|
|
1806
|
+
const userMatch = output.match(/Logged in to github\.com as ([^\s]+)/);
|
|
1807
|
+
const username = userMatch == null ? void 0 : userMatch[1];
|
|
1808
|
+
return {
|
|
1809
|
+
hasAuth: true,
|
|
1810
|
+
scopes: ((_a = scopeMatch == null ? void 0 : scopeMatch[1]) == null ? void 0 : _a.split(", ").map((scope) => scope.replace(/^'|'$/g, ""))) ?? [],
|
|
1811
|
+
...username && { username }
|
|
1812
|
+
};
|
|
1813
|
+
} catch (error) {
|
|
1814
|
+
if (error instanceof Error && "stderr" in error && ((_b = error.stderr) == null ? void 0 : _b.includes("You are not logged into any GitHub hosts"))) {
|
|
1815
|
+
return { hasAuth: false, scopes: [] };
|
|
1816
|
+
}
|
|
1817
|
+
throw error;
|
|
1818
|
+
}
|
|
1819
|
+
}
|
|
1820
|
+
async function hasProjectScope() {
|
|
1821
|
+
const auth = await checkGhAuth();
|
|
1822
|
+
return auth.scopes.includes("project");
|
|
1823
|
+
}
|
|
1824
|
+
async function fetchGhIssue(issueNumber, repo) {
|
|
1825
|
+
logger.debug("Fetching GitHub issue", { issueNumber, repo });
|
|
1826
|
+
const args = [
|
|
1827
|
+
"issue",
|
|
1828
|
+
"view",
|
|
1829
|
+
String(issueNumber),
|
|
1830
|
+
"--json",
|
|
1831
|
+
"number,title,body,state,labels,assignees,url,createdAt,updatedAt"
|
|
1832
|
+
];
|
|
1833
|
+
if (repo) {
|
|
1834
|
+
args.push("--repo", repo);
|
|
1835
|
+
}
|
|
1836
|
+
return executeGhCommand(args);
|
|
1837
|
+
}
|
|
1838
|
+
async function fetchGhPR(prNumber) {
|
|
1839
|
+
logger.debug("Fetching GitHub PR", { prNumber });
|
|
1840
|
+
return executeGhCommand([
|
|
1841
|
+
"pr",
|
|
1842
|
+
"view",
|
|
1843
|
+
String(prNumber),
|
|
1844
|
+
"--json",
|
|
1845
|
+
"number,title,body,state,headRefName,baseRefName,url,isDraft,mergeable,createdAt,updatedAt"
|
|
1846
|
+
]);
|
|
1847
|
+
}
|
|
1848
|
+
async function fetchProjectList(owner) {
|
|
1849
|
+
const result = await executeGhCommand([
|
|
1850
|
+
"project",
|
|
1851
|
+
"list",
|
|
1852
|
+
"--owner",
|
|
1853
|
+
owner,
|
|
1854
|
+
"--limit",
|
|
1855
|
+
"100",
|
|
1856
|
+
"--format",
|
|
1857
|
+
"json"
|
|
1858
|
+
]);
|
|
1859
|
+
return (result == null ? void 0 : result.projects) ?? [];
|
|
1860
|
+
}
|
|
1861
|
+
async function fetchProjectItems(projectNumber, owner) {
|
|
1862
|
+
const result = await executeGhCommand([
|
|
1863
|
+
"project",
|
|
1864
|
+
"item-list",
|
|
1865
|
+
String(projectNumber),
|
|
1866
|
+
"--owner",
|
|
1867
|
+
owner,
|
|
1868
|
+
"--limit",
|
|
1869
|
+
"10000",
|
|
1870
|
+
"--format",
|
|
1871
|
+
"json"
|
|
1872
|
+
]);
|
|
1873
|
+
return (result == null ? void 0 : result.items) ?? [];
|
|
1874
|
+
}
|
|
1875
|
+
async function fetchProjectFields(projectNumber, owner) {
|
|
1876
|
+
const result = await executeGhCommand([
|
|
1877
|
+
"project",
|
|
1878
|
+
"field-list",
|
|
1879
|
+
String(projectNumber),
|
|
1880
|
+
"--owner",
|
|
1881
|
+
owner,
|
|
1882
|
+
"--format",
|
|
1883
|
+
"json"
|
|
1884
|
+
]);
|
|
1885
|
+
return result ?? { fields: [] };
|
|
1886
|
+
}
|
|
1887
|
+
async function updateProjectItemField(itemId, projectId, fieldId, optionId) {
|
|
1888
|
+
await executeGhCommand([
|
|
1889
|
+
"project",
|
|
1890
|
+
"item-edit",
|
|
1891
|
+
"--id",
|
|
1892
|
+
itemId,
|
|
1893
|
+
"--project-id",
|
|
1894
|
+
projectId,
|
|
1895
|
+
"--field-id",
|
|
1896
|
+
fieldId,
|
|
1897
|
+
"--single-select-option-id",
|
|
1898
|
+
optionId,
|
|
1899
|
+
"--format",
|
|
1900
|
+
"json"
|
|
1901
|
+
]);
|
|
1902
|
+
}
|
|
1903
|
+
var SimpleBranchNameStrategy = class {
|
|
1904
|
+
async generate(issueNumber, title) {
|
|
1905
|
+
const slug = title.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "").substring(0, 20);
|
|
1906
|
+
return `feat/issue-${issueNumber}-${slug}`;
|
|
1907
|
+
}
|
|
1908
|
+
};
|
|
1909
|
+
var ClaudeBranchNameStrategy = class {
|
|
1910
|
+
constructor(claudeModel = "haiku") {
|
|
1911
|
+
this.claudeModel = claudeModel;
|
|
1912
|
+
}
|
|
1913
|
+
async generate(issueNumber, title) {
|
|
1914
|
+
const { generateBranchName: generateBranchName2 } = await Promise.resolve().then(() => (init_claude(), claude_exports));
|
|
1915
|
+
return generateBranchName2(title, issueNumber, this.claudeModel);
|
|
1916
|
+
}
|
|
1917
|
+
};
|
|
1918
|
+
async function createIssue(title, body, options) {
|
|
1919
|
+
const { repo, labels } = options ?? {};
|
|
1920
|
+
logger.debug("Creating GitHub issue", { title, repo, labels });
|
|
1921
|
+
const args = [
|
|
1922
|
+
"issue",
|
|
1923
|
+
"create",
|
|
1924
|
+
"--title",
|
|
1925
|
+
title,
|
|
1926
|
+
"--body",
|
|
1927
|
+
body
|
|
1928
|
+
];
|
|
1929
|
+
if (repo) {
|
|
1930
|
+
args.splice(2, 0, "--repo", repo);
|
|
1931
|
+
}
|
|
1932
|
+
if (labels && labels.length > 0) {
|
|
1933
|
+
args.push("--label", labels.join(","));
|
|
1934
|
+
}
|
|
1935
|
+
const execaOptions = {
|
|
1936
|
+
timeout: 3e4,
|
|
1937
|
+
encoding: "utf8"
|
|
1938
|
+
};
|
|
1939
|
+
if (!repo) {
|
|
1940
|
+
execaOptions.cwd = process.cwd();
|
|
1941
|
+
}
|
|
1942
|
+
const result = await execa4("gh", args, execaOptions);
|
|
1943
|
+
const urlMatch = result.stdout.trim().match(/https:\/\/github\.com\/[^/]+\/[^/]+\/issues\/(\d+)/);
|
|
1944
|
+
if (!(urlMatch == null ? void 0 : urlMatch[1])) {
|
|
1945
|
+
throw new Error(`Failed to parse issue URL from gh output: ${result.stdout}`);
|
|
1946
|
+
}
|
|
1947
|
+
const issueNumber = parseInt(urlMatch[1], 10);
|
|
1948
|
+
const issueUrl = urlMatch[0];
|
|
1949
|
+
return {
|
|
1950
|
+
number: issueNumber,
|
|
1951
|
+
url: issueUrl
|
|
1952
|
+
};
|
|
1953
|
+
}
|
|
1954
|
+
|
|
1955
|
+
// src/lib/GitHubService.ts
|
|
1956
|
+
init_logger();
|
|
1957
|
+
|
|
1958
|
+
// src/utils/prompt.ts
|
|
1959
|
+
init_logger();
|
|
1960
|
+
import * as readline from "readline";
|
|
1961
|
+
async function promptConfirmation(message, defaultValue = false) {
|
|
1962
|
+
const rl = readline.createInterface({
|
|
1963
|
+
input: process.stdin,
|
|
1964
|
+
output: process.stdout
|
|
1965
|
+
});
|
|
1966
|
+
const suffix = defaultValue ? "[Y/n]" : "[y/N]";
|
|
1967
|
+
const fullMessage = `${message} ${suffix}: `;
|
|
1968
|
+
return new Promise((resolve) => {
|
|
1969
|
+
rl.question(fullMessage, (answer) => {
|
|
1970
|
+
rl.close();
|
|
1971
|
+
const normalized = answer.trim().toLowerCase();
|
|
1972
|
+
if (normalized === "") {
|
|
1973
|
+
resolve(defaultValue);
|
|
1974
|
+
return;
|
|
1975
|
+
}
|
|
1976
|
+
if (normalized === "y" || normalized === "yes") {
|
|
1977
|
+
resolve(true);
|
|
1978
|
+
return;
|
|
1979
|
+
}
|
|
1980
|
+
if (normalized === "n" || normalized === "no") {
|
|
1981
|
+
resolve(false);
|
|
1982
|
+
return;
|
|
1983
|
+
}
|
|
1984
|
+
logger.warn("Invalid input, using default value", {
|
|
1985
|
+
input: answer,
|
|
1986
|
+
defaultValue
|
|
1987
|
+
});
|
|
1988
|
+
resolve(defaultValue);
|
|
1989
|
+
});
|
|
1990
|
+
});
|
|
1991
|
+
}
|
|
1992
|
+
|
|
1993
|
+
// src/lib/GitHubService.ts
|
|
1994
|
+
var GitHubService = class {
|
|
1995
|
+
constructor(options) {
|
|
1996
|
+
if (options == null ? void 0 : options.branchNameStrategy) {
|
|
1997
|
+
this.defaultBranchNameStrategy = options.branchNameStrategy;
|
|
1998
|
+
} else if ((options == null ? void 0 : options.useClaude) !== false) {
|
|
1999
|
+
this.defaultBranchNameStrategy = new ClaudeBranchNameStrategy(
|
|
2000
|
+
options == null ? void 0 : options.claudeModel
|
|
2001
|
+
);
|
|
2002
|
+
} else {
|
|
2003
|
+
this.defaultBranchNameStrategy = new SimpleBranchNameStrategy();
|
|
2004
|
+
}
|
|
2005
|
+
this.prompter = (options == null ? void 0 : options.prompter) ?? promptConfirmation;
|
|
2006
|
+
}
|
|
2007
|
+
// Input detection
|
|
2008
|
+
async detectInputType(input) {
|
|
2009
|
+
const numberMatch = input.match(/^#?(\d+)$/);
|
|
2010
|
+
if (!(numberMatch == null ? void 0 : numberMatch[1])) {
|
|
2011
|
+
return { type: "unknown", number: null, rawInput: input };
|
|
2012
|
+
}
|
|
2013
|
+
const number = parseInt(numberMatch[1], 10);
|
|
2014
|
+
logger.debug("Checking if input is a PR", { number });
|
|
2015
|
+
const pr = await this.isValidPR(number);
|
|
2016
|
+
if (pr) {
|
|
2017
|
+
return { type: "pr", number, rawInput: input };
|
|
2018
|
+
}
|
|
2019
|
+
logger.debug("Checking if input is an issue", { number });
|
|
2020
|
+
const issue = await this.isValidIssue(number);
|
|
2021
|
+
if (issue) {
|
|
2022
|
+
return { type: "issue", number, rawInput: input };
|
|
2023
|
+
}
|
|
2024
|
+
return { type: "unknown", number: null, rawInput: input };
|
|
2025
|
+
}
|
|
2026
|
+
// Issue fetching with validation
|
|
2027
|
+
async fetchIssue(issueNumber) {
|
|
2028
|
+
var _a;
|
|
2029
|
+
try {
|
|
2030
|
+
return await this.fetchIssueInternal(issueNumber);
|
|
2031
|
+
} catch (error) {
|
|
2032
|
+
if (error instanceof Error && "stderr" in error && ((_a = error.stderr) == null ? void 0 : _a.includes("Could not resolve"))) {
|
|
2033
|
+
throw new GitHubError(
|
|
2034
|
+
"NOT_FOUND" /* NOT_FOUND */,
|
|
2035
|
+
`Issue #${issueNumber} not found`,
|
|
2036
|
+
error
|
|
2037
|
+
);
|
|
2038
|
+
}
|
|
2039
|
+
throw error;
|
|
2040
|
+
}
|
|
2041
|
+
}
|
|
2042
|
+
// Silent issue validation (for detection phase)
|
|
2043
|
+
async isValidIssue(issueNumber) {
|
|
2044
|
+
var _a;
|
|
2045
|
+
try {
|
|
2046
|
+
return await this.fetchIssueInternal(issueNumber);
|
|
2047
|
+
} catch (error) {
|
|
2048
|
+
if (error instanceof Error && "stderr" in error && ((_a = error.stderr) == null ? void 0 : _a.includes("Could not resolve"))) {
|
|
2049
|
+
return false;
|
|
2050
|
+
}
|
|
2051
|
+
throw error;
|
|
2052
|
+
}
|
|
2053
|
+
}
|
|
2054
|
+
// Internal issue fetching logic (shared by fetchIssue and isValidIssue)
|
|
2055
|
+
async fetchIssueInternal(issueNumber) {
|
|
2056
|
+
const ghIssue = await fetchGhIssue(issueNumber);
|
|
2057
|
+
return this.mapGitHubIssueToIssue(ghIssue);
|
|
2058
|
+
}
|
|
2059
|
+
async validateIssueState(issue) {
|
|
2060
|
+
if (issue.state === "closed") {
|
|
2061
|
+
const response = await this.promptUserConfirmation(
|
|
2062
|
+
`Issue #${issue.number} is closed. Continue anyway?`
|
|
2063
|
+
);
|
|
2064
|
+
if (!response) {
|
|
2065
|
+
throw new GitHubError(
|
|
2066
|
+
"INVALID_STATE" /* INVALID_STATE */,
|
|
2067
|
+
"User cancelled due to closed issue"
|
|
2068
|
+
);
|
|
2069
|
+
}
|
|
2070
|
+
}
|
|
2071
|
+
}
|
|
2072
|
+
// PR fetching with validation
|
|
2073
|
+
async fetchPR(prNumber) {
|
|
2074
|
+
var _a;
|
|
2075
|
+
try {
|
|
2076
|
+
return await this.fetchPRInternal(prNumber);
|
|
2077
|
+
} catch (error) {
|
|
2078
|
+
if (error instanceof Error && "stderr" in error && ((_a = error.stderr) == null ? void 0 : _a.includes("Could not resolve"))) {
|
|
2079
|
+
throw new GitHubError(
|
|
2080
|
+
"NOT_FOUND" /* NOT_FOUND */,
|
|
2081
|
+
`PR #${prNumber} not found`,
|
|
2082
|
+
error
|
|
2083
|
+
);
|
|
2084
|
+
}
|
|
2085
|
+
throw error;
|
|
2086
|
+
}
|
|
2087
|
+
}
|
|
2088
|
+
// Silent PR validation (for detection phase)
|
|
2089
|
+
async isValidPR(prNumber) {
|
|
2090
|
+
var _a;
|
|
2091
|
+
try {
|
|
2092
|
+
return await this.fetchPRInternal(prNumber);
|
|
2093
|
+
} catch (error) {
|
|
2094
|
+
if (error instanceof Error && "stderr" in error && ((_a = error.stderr) == null ? void 0 : _a.includes("Could not resolve"))) {
|
|
2095
|
+
return false;
|
|
2096
|
+
}
|
|
2097
|
+
throw error;
|
|
2098
|
+
}
|
|
2099
|
+
}
|
|
2100
|
+
// Internal PR fetching logic (shared by fetchPR and isValidPR)
|
|
2101
|
+
async fetchPRInternal(prNumber) {
|
|
2102
|
+
const ghPR = await fetchGhPR(prNumber);
|
|
2103
|
+
return this.mapGitHubPRToPullRequest(ghPR);
|
|
2104
|
+
}
|
|
2105
|
+
async validatePRState(pr) {
|
|
2106
|
+
if (pr.state === "closed" || pr.state === "merged") {
|
|
2107
|
+
const response = await this.promptUserConfirmation(
|
|
2108
|
+
`PR #${pr.number} is ${pr.state}. Continue anyway?`
|
|
2109
|
+
);
|
|
2110
|
+
if (!response) {
|
|
2111
|
+
throw new GitHubError(
|
|
2112
|
+
"INVALID_STATE" /* INVALID_STATE */,
|
|
2113
|
+
`User cancelled due to ${pr.state} PR`
|
|
2114
|
+
);
|
|
2115
|
+
}
|
|
2116
|
+
}
|
|
2117
|
+
}
|
|
2118
|
+
// Branch name generation using strategy pattern
|
|
2119
|
+
async generateBranchName(options) {
|
|
2120
|
+
const { issueNumber, title, strategy } = options;
|
|
2121
|
+
const nameStrategy = strategy ?? this.defaultBranchNameStrategy;
|
|
2122
|
+
logger.debug("Generating branch name", {
|
|
2123
|
+
issueNumber,
|
|
2124
|
+
title,
|
|
2125
|
+
strategy: nameStrategy.constructor.name
|
|
2126
|
+
});
|
|
2127
|
+
return nameStrategy.generate(issueNumber, title);
|
|
2128
|
+
}
|
|
2129
|
+
// Issue creation
|
|
2130
|
+
async createIssue(title, body, repository, labels) {
|
|
2131
|
+
return createIssue(title, body, { repo: repository, labels });
|
|
2132
|
+
}
|
|
2133
|
+
async getIssueUrl(issueNumber, repo) {
|
|
2134
|
+
logger.debug("Fetching issue URL", { issueNumber, repo });
|
|
2135
|
+
const issue = await fetchGhIssue(issueNumber, repo);
|
|
2136
|
+
return issue.url;
|
|
2137
|
+
}
|
|
2138
|
+
// GitHub Projects integration
|
|
2139
|
+
async moveIssueToInProgress(issueNumber) {
|
|
2140
|
+
logger.info("Moving issue to In Progress in GitHub Projects", {
|
|
2141
|
+
issueNumber
|
|
2142
|
+
});
|
|
2143
|
+
if (!await hasProjectScope()) {
|
|
2144
|
+
logger.warn("Missing project scope in GitHub CLI auth");
|
|
2145
|
+
throw new GitHubError(
|
|
2146
|
+
"MISSING_SCOPE" /* MISSING_SCOPE */,
|
|
2147
|
+
"GitHub CLI lacks project scope. Run: gh auth refresh -s project"
|
|
2148
|
+
);
|
|
2149
|
+
}
|
|
2150
|
+
let owner;
|
|
2151
|
+
try {
|
|
2152
|
+
const repoInfo = await executeGhCommand(["repo", "view", "--json", "owner,name"]);
|
|
2153
|
+
owner = repoInfo.owner.login;
|
|
2154
|
+
} catch (error) {
|
|
2155
|
+
logger.warn("Could not determine repository info", { error });
|
|
2156
|
+
return;
|
|
2157
|
+
}
|
|
2158
|
+
let projects;
|
|
2159
|
+
try {
|
|
2160
|
+
projects = await fetchProjectList(owner);
|
|
2161
|
+
} catch (error) {
|
|
2162
|
+
logger.warn("Could not fetch projects", { owner, error });
|
|
2163
|
+
return;
|
|
2164
|
+
}
|
|
2165
|
+
if (!projects.length) {
|
|
2166
|
+
logger.warn("No projects found", { owner });
|
|
2167
|
+
return;
|
|
2168
|
+
}
|
|
2169
|
+
for (const project of projects) {
|
|
2170
|
+
await this.updateIssueStatusInProject(project, issueNumber, owner);
|
|
2171
|
+
}
|
|
2172
|
+
}
|
|
2173
|
+
async updateIssueStatusInProject(project, issueNumber, owner) {
|
|
2174
|
+
var _a;
|
|
2175
|
+
let items;
|
|
2176
|
+
try {
|
|
2177
|
+
items = await fetchProjectItems(project.number, owner);
|
|
2178
|
+
} catch (error) {
|
|
2179
|
+
logger.debug("Could not fetch project items", { project: project.number, error });
|
|
2180
|
+
return;
|
|
2181
|
+
}
|
|
2182
|
+
const item = items.find(
|
|
2183
|
+
(i) => i.content.type === "Issue" && i.content.number === issueNumber
|
|
2184
|
+
);
|
|
2185
|
+
if (!item) {
|
|
2186
|
+
logger.debug("Issue not found in project", {
|
|
2187
|
+
issueNumber,
|
|
2188
|
+
projectNumber: project.number
|
|
2189
|
+
});
|
|
2190
|
+
return;
|
|
2191
|
+
}
|
|
2192
|
+
let fieldsData;
|
|
2193
|
+
try {
|
|
2194
|
+
fieldsData = await fetchProjectFields(project.number, owner);
|
|
2195
|
+
} catch (error) {
|
|
2196
|
+
logger.debug("Could not fetch project fields", { project: project.number, error });
|
|
2197
|
+
return;
|
|
2198
|
+
}
|
|
2199
|
+
const statusField = fieldsData.fields.find((f) => f.name === "Status");
|
|
2200
|
+
if (!statusField) {
|
|
2201
|
+
logger.debug("No Status field found in project", { projectNumber: project.number });
|
|
2202
|
+
return;
|
|
2203
|
+
}
|
|
2204
|
+
const inProgressOption = (_a = statusField.options) == null ? void 0 : _a.find(
|
|
2205
|
+
(o) => o.name === "In Progress" || o.name === "In progress"
|
|
2206
|
+
);
|
|
2207
|
+
if (!inProgressOption) {
|
|
2208
|
+
logger.debug("No In Progress option found in Status field", { projectNumber: project.number });
|
|
2209
|
+
return;
|
|
2210
|
+
}
|
|
2211
|
+
try {
|
|
2212
|
+
await updateProjectItemField(
|
|
2213
|
+
item.id,
|
|
2214
|
+
project.id,
|
|
2215
|
+
statusField.id,
|
|
2216
|
+
inProgressOption.id
|
|
2217
|
+
);
|
|
2218
|
+
logger.info("Updated issue status in project", {
|
|
2219
|
+
issueNumber,
|
|
2220
|
+
projectNumber: project.number
|
|
2221
|
+
});
|
|
2222
|
+
} catch (error) {
|
|
2223
|
+
logger.debug("Could not update project item", { item: item.id, error });
|
|
2224
|
+
}
|
|
2225
|
+
}
|
|
2226
|
+
// Utility methods
|
|
2227
|
+
extractContext(entity) {
|
|
2228
|
+
if ("branch" in entity) {
|
|
2229
|
+
return `Pull Request #${entity.number}: ${entity.title}
|
|
2230
|
+
Branch: ${entity.branch}
|
|
2231
|
+
State: ${entity.state}`;
|
|
2232
|
+
} else {
|
|
2233
|
+
return `GitHub Issue #${entity.number}: ${entity.title}
|
|
2234
|
+
State: ${entity.state}`;
|
|
2235
|
+
}
|
|
2236
|
+
}
|
|
2237
|
+
mapGitHubIssueToIssue(ghIssue) {
|
|
2238
|
+
return {
|
|
2239
|
+
number: ghIssue.number,
|
|
2240
|
+
title: ghIssue.title,
|
|
2241
|
+
body: ghIssue.body,
|
|
2242
|
+
state: ghIssue.state.toLowerCase(),
|
|
2243
|
+
labels: ghIssue.labels.map((l) => l.name),
|
|
2244
|
+
assignees: ghIssue.assignees.map((a) => a.login),
|
|
2245
|
+
url: ghIssue.url
|
|
2246
|
+
};
|
|
2247
|
+
}
|
|
2248
|
+
mapGitHubPRToPullRequest(ghPR) {
|
|
2249
|
+
return {
|
|
2250
|
+
number: ghPR.number,
|
|
2251
|
+
title: ghPR.title,
|
|
2252
|
+
body: ghPR.body,
|
|
2253
|
+
state: ghPR.state.toLowerCase(),
|
|
2254
|
+
branch: ghPR.headRefName,
|
|
2255
|
+
baseBranch: ghPR.baseRefName,
|
|
2256
|
+
url: ghPR.url,
|
|
2257
|
+
isDraft: ghPR.isDraft
|
|
2258
|
+
};
|
|
2259
|
+
}
|
|
2260
|
+
async promptUserConfirmation(message) {
|
|
2261
|
+
return this.prompter(message);
|
|
2262
|
+
}
|
|
2263
|
+
// Allow setting strategy at runtime for specific operations
|
|
2264
|
+
setDefaultBranchNameStrategy(strategy) {
|
|
2265
|
+
this.defaultBranchNameStrategy = strategy;
|
|
2266
|
+
}
|
|
2267
|
+
// Get current strategy for testing
|
|
2268
|
+
getBranchNameStrategy() {
|
|
2269
|
+
return this.defaultBranchNameStrategy;
|
|
2270
|
+
}
|
|
2271
|
+
};
|
|
2272
|
+
|
|
2273
|
+
// src/lib/EnvironmentManager.ts
|
|
2274
|
+
init_logger();
|
|
2275
|
+
import fs2 from "fs-extra";
|
|
2276
|
+
|
|
2277
|
+
// src/utils/env.ts
|
|
2278
|
+
init_logger();
|
|
2279
|
+
import dotenvFlow from "dotenv-flow";
|
|
2280
|
+
function parseEnvFile(content) {
|
|
2281
|
+
const envMap = /* @__PURE__ */ new Map();
|
|
2282
|
+
const lines = content.split("\n");
|
|
2283
|
+
for (const line of lines) {
|
|
2284
|
+
const trimmedLine = line.trim();
|
|
2285
|
+
if (!trimmedLine || trimmedLine.startsWith("#")) {
|
|
2286
|
+
continue;
|
|
2287
|
+
}
|
|
2288
|
+
const cleanLine = trimmedLine.startsWith("export ") ? trimmedLine.substring(7) : trimmedLine;
|
|
2289
|
+
const equalsIndex = cleanLine.indexOf("=");
|
|
2290
|
+
if (equalsIndex === -1) {
|
|
2291
|
+
continue;
|
|
2292
|
+
}
|
|
2293
|
+
const key = cleanLine.substring(0, equalsIndex).trim();
|
|
2294
|
+
let value = cleanLine.substring(equalsIndex + 1);
|
|
2295
|
+
if (value.startsWith('"') && value.endsWith('"') || value.startsWith("'") && value.endsWith("'")) {
|
|
2296
|
+
value = value.substring(1, value.length - 1);
|
|
2297
|
+
value = value.replace(/\\"/g, '"').replace(/\\'/g, "'");
|
|
2298
|
+
value = value.replace(/\\n/g, "\n");
|
|
2299
|
+
}
|
|
2300
|
+
if (key) {
|
|
2301
|
+
envMap.set(key, value);
|
|
2302
|
+
}
|
|
2303
|
+
}
|
|
2304
|
+
return envMap;
|
|
2305
|
+
}
|
|
2306
|
+
function formatEnvLine(key, value) {
|
|
2307
|
+
const escapedValue = value.replace(/"/g, '\\"').replace(/\n/g, "\\n").replace(/\r/g, "\\r");
|
|
2308
|
+
return `${key}="${escapedValue}"`;
|
|
2309
|
+
}
|
|
2310
|
+
function validateEnvVariable(key, _value) {
|
|
2311
|
+
if (!key || key.length === 0) {
|
|
2312
|
+
return {
|
|
2313
|
+
valid: false,
|
|
2314
|
+
error: "Environment variable key cannot be empty"
|
|
2315
|
+
};
|
|
2316
|
+
}
|
|
2317
|
+
if (!isValidEnvKey(key)) {
|
|
2318
|
+
return {
|
|
2319
|
+
valid: false,
|
|
2320
|
+
error: `Invalid environment variable name: ${key}. Must start with a letter or underscore and contain only letters, numbers, and underscores.`
|
|
2321
|
+
};
|
|
2322
|
+
}
|
|
2323
|
+
return { valid: true };
|
|
2324
|
+
}
|
|
2325
|
+
function isValidEnvKey(key) {
|
|
2326
|
+
if (!key || key.length === 0) {
|
|
2327
|
+
return false;
|
|
2328
|
+
}
|
|
2329
|
+
const validKeyRegex = /^[A-Za-z_][A-Za-z0-9_]*$/;
|
|
2330
|
+
return validKeyRegex.test(key);
|
|
2331
|
+
}
|
|
2332
|
+
|
|
2333
|
+
// src/utils/port.ts
|
|
2334
|
+
import { createHash as createHash2 } from "crypto";
|
|
2335
|
+
function generatePortOffsetFromBranchName(branchName) {
|
|
2336
|
+
if (!branchName || branchName.trim().length === 0) {
|
|
2337
|
+
throw new Error("Branch name cannot be empty");
|
|
2338
|
+
}
|
|
2339
|
+
const hash = createHash2("sha256").update(branchName).digest("hex");
|
|
2340
|
+
const hashPrefix = hash.slice(0, 8);
|
|
2341
|
+
const hashAsInt = parseInt(hashPrefix, 16);
|
|
2342
|
+
const portOffset = hashAsInt % 999 + 1;
|
|
2343
|
+
return portOffset;
|
|
2344
|
+
}
|
|
2345
|
+
function calculatePortForBranch(branchName, basePort = 3e3) {
|
|
2346
|
+
const offset = generatePortOffsetFromBranchName(branchName);
|
|
2347
|
+
const port = basePort + offset;
|
|
2348
|
+
if (port > 65535) {
|
|
2349
|
+
throw new Error(
|
|
2350
|
+
`Calculated port ${port} exceeds maximum (65535). Use a lower base port (current: ${basePort}).`
|
|
2351
|
+
);
|
|
2352
|
+
}
|
|
2353
|
+
return port;
|
|
2354
|
+
}
|
|
2355
|
+
|
|
2356
|
+
// src/lib/EnvironmentManager.ts
|
|
2357
|
+
var logger2 = createLogger({ prefix: "\u{1F4DD}" });
|
|
2358
|
+
var EnvironmentManager = class {
|
|
2359
|
+
constructor() {
|
|
2360
|
+
this.backupSuffix = ".backup";
|
|
2361
|
+
}
|
|
2362
|
+
/**
|
|
2363
|
+
* Set or update an environment variable in a .env file
|
|
2364
|
+
* Ports functionality from bash/utils/env-utils.sh:setEnvVar()
|
|
2365
|
+
* @returns The backup path if a backup was created
|
|
2366
|
+
*/
|
|
2367
|
+
async setEnvVar(filePath, key, value, backup = false) {
|
|
2368
|
+
const validation = validateEnvVariable(key, value);
|
|
2369
|
+
if (!validation.valid) {
|
|
2370
|
+
throw new Error(validation.error ?? "Invalid variable name");
|
|
2371
|
+
}
|
|
2372
|
+
const fileExists = await fs2.pathExists(filePath);
|
|
2373
|
+
if (!fileExists) {
|
|
2374
|
+
logger2.info(`Creating ${filePath} with ${key}...`);
|
|
2375
|
+
const content = formatEnvLine(key, value);
|
|
2376
|
+
await fs2.writeFile(filePath, content, "utf8");
|
|
2377
|
+
logger2.success(`${filePath} created with ${key}`);
|
|
2378
|
+
return;
|
|
2379
|
+
}
|
|
2380
|
+
const existingContent = await fs2.readFile(filePath, "utf8");
|
|
2381
|
+
const envMap = parseEnvFile(existingContent);
|
|
2382
|
+
let backupPath;
|
|
2383
|
+
if (backup) {
|
|
2384
|
+
backupPath = await this.createBackup(filePath);
|
|
2385
|
+
}
|
|
2386
|
+
envMap.set(key, value);
|
|
2387
|
+
const lines = existingContent.split("\n");
|
|
2388
|
+
const newLines = [];
|
|
2389
|
+
let variableUpdated = false;
|
|
2390
|
+
for (const line of lines) {
|
|
2391
|
+
const trimmedLine = line.trim();
|
|
2392
|
+
if (!trimmedLine || trimmedLine.startsWith("#")) {
|
|
2393
|
+
newLines.push(line);
|
|
2394
|
+
continue;
|
|
2395
|
+
}
|
|
2396
|
+
const cleanLine = trimmedLine.startsWith("export ") ? trimmedLine.substring(7) : trimmedLine;
|
|
2397
|
+
const equalsIndex = cleanLine.indexOf("=");
|
|
2398
|
+
if (equalsIndex !== -1) {
|
|
2399
|
+
const lineKey = cleanLine.substring(0, equalsIndex).trim();
|
|
2400
|
+
if (lineKey === key) {
|
|
2401
|
+
newLines.push(formatEnvLine(key, value));
|
|
2402
|
+
variableUpdated = true;
|
|
2403
|
+
continue;
|
|
2404
|
+
}
|
|
2405
|
+
}
|
|
2406
|
+
newLines.push(line);
|
|
2407
|
+
}
|
|
2408
|
+
if (!variableUpdated) {
|
|
2409
|
+
logger2.info(`Adding ${key} to ${filePath}...`);
|
|
2410
|
+
newLines.push(formatEnvLine(key, value));
|
|
2411
|
+
logger2.success(`${key} added successfully`);
|
|
2412
|
+
} else {
|
|
2413
|
+
logger2.info(`Updating ${key} in ${filePath}...`);
|
|
2414
|
+
logger2.success(`${key} updated successfully`);
|
|
2415
|
+
}
|
|
2416
|
+
const newContent = newLines.join("\n");
|
|
2417
|
+
await fs2.writeFile(filePath, newContent, "utf8");
|
|
2418
|
+
return backupPath;
|
|
2419
|
+
}
|
|
2420
|
+
/**
|
|
2421
|
+
* Read and parse a .env file
|
|
2422
|
+
*/
|
|
2423
|
+
async readEnvFile(filePath) {
|
|
2424
|
+
try {
|
|
2425
|
+
const content = await fs2.readFile(filePath, "utf8");
|
|
2426
|
+
return parseEnvFile(content);
|
|
2427
|
+
} catch (error) {
|
|
2428
|
+
logger2.debug(
|
|
2429
|
+
`Could not read env file ${filePath}: ${error instanceof Error ? error.message : String(error)}`
|
|
2430
|
+
);
|
|
2431
|
+
return /* @__PURE__ */ new Map();
|
|
2432
|
+
}
|
|
2433
|
+
}
|
|
2434
|
+
/**
|
|
2435
|
+
* Generic file copy helper that only copies if source exists
|
|
2436
|
+
* Does not throw if source file doesn't exist - just logs and returns
|
|
2437
|
+
* @private
|
|
2438
|
+
*/
|
|
2439
|
+
async copyIfExists(source, destination) {
|
|
2440
|
+
const sourceExists = await fs2.pathExists(source);
|
|
2441
|
+
if (!sourceExists) {
|
|
2442
|
+
logger2.debug(`Source file ${source} does not exist, skipping copy`);
|
|
2443
|
+
return;
|
|
2444
|
+
}
|
|
2445
|
+
await fs2.copy(source, destination, { overwrite: false });
|
|
2446
|
+
logger2.success(`Copied ${source} to ${destination}`);
|
|
2447
|
+
}
|
|
2448
|
+
/**
|
|
2449
|
+
* Calculate unique port for workspace
|
|
2450
|
+
* Implements:
|
|
2451
|
+
* - Issue/PR: 3000 + issue/PR number
|
|
2452
|
+
* - Branch: 3000 + deterministic hash offset (1-999)
|
|
2453
|
+
*/
|
|
2454
|
+
calculatePort(options) {
|
|
2455
|
+
const basePort = options.basePort ?? 3e3;
|
|
2456
|
+
if (options.issueNumber !== void 0) {
|
|
2457
|
+
const port = basePort + options.issueNumber;
|
|
2458
|
+
if (port > 65535) {
|
|
2459
|
+
throw new Error(
|
|
2460
|
+
`Calculated port ${port} exceeds maximum (65535). Use a lower base port or issue number.`
|
|
2461
|
+
);
|
|
2462
|
+
}
|
|
2463
|
+
return port;
|
|
2464
|
+
}
|
|
2465
|
+
if (options.prNumber !== void 0) {
|
|
2466
|
+
const port = basePort + options.prNumber;
|
|
2467
|
+
if (port > 65535) {
|
|
2468
|
+
throw new Error(
|
|
2469
|
+
`Calculated port ${port} exceeds maximum (65535). Use a lower base port or PR number.`
|
|
2470
|
+
);
|
|
2471
|
+
}
|
|
2472
|
+
return port;
|
|
2473
|
+
}
|
|
2474
|
+
if (options.branchName !== void 0) {
|
|
2475
|
+
return calculatePortForBranch(options.branchName, basePort);
|
|
2476
|
+
}
|
|
2477
|
+
return basePort;
|
|
2478
|
+
}
|
|
2479
|
+
/**
|
|
2480
|
+
* Set port environment variable for workspace
|
|
2481
|
+
*/
|
|
2482
|
+
async setPortForWorkspace(envFilePath, issueNumber, prNumber, branchName) {
|
|
2483
|
+
const options = {};
|
|
2484
|
+
if (issueNumber !== void 0) {
|
|
2485
|
+
options.issueNumber = issueNumber;
|
|
2486
|
+
}
|
|
2487
|
+
if (prNumber !== void 0) {
|
|
2488
|
+
options.prNumber = prNumber;
|
|
2489
|
+
}
|
|
2490
|
+
if (branchName !== void 0) {
|
|
2491
|
+
options.branchName = branchName;
|
|
2492
|
+
}
|
|
2493
|
+
const port = this.calculatePort(options);
|
|
2494
|
+
await this.setEnvVar(envFilePath, "PORT", String(port));
|
|
2495
|
+
return port;
|
|
2496
|
+
}
|
|
2497
|
+
/**
|
|
2498
|
+
* Validate environment configuration
|
|
2499
|
+
*/
|
|
2500
|
+
async validateEnvFile(filePath) {
|
|
2501
|
+
try {
|
|
2502
|
+
const content = await fs2.readFile(filePath, "utf8");
|
|
2503
|
+
const envMap = parseEnvFile(content);
|
|
2504
|
+
const errors = [];
|
|
2505
|
+
for (const [key, value] of envMap.entries()) {
|
|
2506
|
+
const validation = validateEnvVariable(key, value);
|
|
2507
|
+
if (!validation.valid) {
|
|
2508
|
+
errors.push(`${key}: ${validation.error}`);
|
|
2509
|
+
}
|
|
2510
|
+
}
|
|
2511
|
+
return {
|
|
2512
|
+
valid: errors.length === 0,
|
|
2513
|
+
errors
|
|
2514
|
+
};
|
|
2515
|
+
} catch (error) {
|
|
2516
|
+
return {
|
|
2517
|
+
valid: false,
|
|
2518
|
+
errors: [
|
|
2519
|
+
`Failed to read or parse file: ${error instanceof Error ? error.message : String(error)}`
|
|
2520
|
+
]
|
|
2521
|
+
};
|
|
2522
|
+
}
|
|
2523
|
+
}
|
|
2524
|
+
/**
|
|
2525
|
+
* Create backup of existing file
|
|
2526
|
+
*/
|
|
2527
|
+
async createBackup(filePath) {
|
|
2528
|
+
const timestamp = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-");
|
|
2529
|
+
const backupPath = `${filePath}${this.backupSuffix}-${timestamp}`;
|
|
2530
|
+
await fs2.copy(filePath, backupPath);
|
|
2531
|
+
logger2.debug(`Created backup at ${backupPath}`);
|
|
2532
|
+
return backupPath;
|
|
2533
|
+
}
|
|
2534
|
+
};
|
|
2535
|
+
|
|
2536
|
+
// src/lib/DatabaseManager.ts
|
|
2537
|
+
init_logger();
|
|
2538
|
+
var logger3 = createLogger({ prefix: "\u{1F5C2}\uFE0F" });
|
|
2539
|
+
var DatabaseManager = class {
|
|
2540
|
+
constructor(provider, environment, databaseUrlEnvVarName = "DATABASE_URL") {
|
|
2541
|
+
this.provider = provider;
|
|
2542
|
+
this.environment = environment;
|
|
2543
|
+
this.databaseUrlEnvVarName = databaseUrlEnvVarName;
|
|
2544
|
+
if (databaseUrlEnvVarName !== "DATABASE_URL") {
|
|
2545
|
+
logger3.debug(`\u{1F527} DatabaseManager configured with custom variable: ${databaseUrlEnvVarName}`);
|
|
2546
|
+
} else {
|
|
2547
|
+
logger3.debug("\u{1F527} DatabaseManager using default variable: DATABASE_URL");
|
|
2548
|
+
}
|
|
2549
|
+
}
|
|
2550
|
+
/**
|
|
2551
|
+
* Get the configured database URL environment variable name
|
|
2552
|
+
*/
|
|
2553
|
+
getConfiguredVariableName() {
|
|
2554
|
+
return this.databaseUrlEnvVarName;
|
|
2555
|
+
}
|
|
2556
|
+
/**
|
|
2557
|
+
* Check if database branching should be used
|
|
2558
|
+
* Requires BOTH conditions:
|
|
2559
|
+
* 1. Database provider is properly configured (checked via provider.isConfigured())
|
|
2560
|
+
* 2. .env file contains the configured database URL variable
|
|
2561
|
+
*/
|
|
2562
|
+
async shouldUseDatabaseBranching(envFilePath) {
|
|
2563
|
+
if (!this.provider.isConfigured()) {
|
|
2564
|
+
logger3.debug("Skipping database branching: Database provider not configured");
|
|
2565
|
+
return false;
|
|
2566
|
+
}
|
|
2567
|
+
const hasDatabaseUrl = await this.hasDatabaseUrlInEnv(envFilePath);
|
|
2568
|
+
if (!hasDatabaseUrl) {
|
|
2569
|
+
logger3.debug(
|
|
2570
|
+
"Skipping database branching: configured database URL variable not found in .env file"
|
|
2571
|
+
);
|
|
2572
|
+
return false;
|
|
2573
|
+
}
|
|
2574
|
+
return true;
|
|
2575
|
+
}
|
|
2576
|
+
/**
|
|
2577
|
+
* Create database branch only if configured
|
|
2578
|
+
* Returns connection string if branch was created, null if skipped
|
|
2579
|
+
*
|
|
2580
|
+
* @param branchName - Name of the branch to create
|
|
2581
|
+
* @param envFilePath - Path to .env file for configuration checks
|
|
2582
|
+
* @param cwd - Optional working directory to run commands from
|
|
2583
|
+
*/
|
|
2584
|
+
async createBranchIfConfigured(branchName, envFilePath, cwd) {
|
|
2585
|
+
if (!await this.shouldUseDatabaseBranching(envFilePath)) {
|
|
2586
|
+
return null;
|
|
2587
|
+
}
|
|
2588
|
+
if (!await this.provider.isCliAvailable()) {
|
|
2589
|
+
logger3.warn("Skipping database branch creation: Neon CLI not available");
|
|
2590
|
+
logger3.warn("Install with: npm install -g neonctl");
|
|
2591
|
+
return null;
|
|
2592
|
+
}
|
|
2593
|
+
try {
|
|
2594
|
+
const isAuth = await this.provider.isAuthenticated(cwd);
|
|
2595
|
+
if (!isAuth) {
|
|
2596
|
+
logger3.warn("Skipping database branch creation: Not authenticated with Neon CLI");
|
|
2597
|
+
logger3.warn("Run: neon auth");
|
|
2598
|
+
return null;
|
|
2599
|
+
}
|
|
2600
|
+
} catch (error) {
|
|
2601
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
2602
|
+
logger3.error(`Database authentication check failed: ${errorMessage}`);
|
|
2603
|
+
throw error;
|
|
2604
|
+
}
|
|
2605
|
+
try {
|
|
2606
|
+
const connectionString = await this.provider.createBranch(branchName, void 0, cwd);
|
|
2607
|
+
logger3.success(`Database branch ready: ${this.provider.sanitizeBranchName(branchName)}`);
|
|
2608
|
+
return connectionString;
|
|
2609
|
+
} catch (error) {
|
|
2610
|
+
logger3.error(
|
|
2611
|
+
`Failed to create database branch: ${error instanceof Error ? error.message : String(error)}`
|
|
2612
|
+
);
|
|
2613
|
+
throw error;
|
|
2614
|
+
}
|
|
2615
|
+
}
|
|
2616
|
+
/**
|
|
2617
|
+
* Delete database branch only if configured
|
|
2618
|
+
* Returns result object indicating what happened
|
|
2619
|
+
*
|
|
2620
|
+
* @param branchName - Name of the branch to delete
|
|
2621
|
+
* @param shouldCleanup - Boolean indicating if database cleanup should be performed (pre-fetched config)
|
|
2622
|
+
* @param isPreview - Whether this is a preview database branch
|
|
2623
|
+
* @param cwd - Optional working directory to run commands from (prevents issues with deleted directories)
|
|
2624
|
+
*/
|
|
2625
|
+
async deleteBranchIfConfigured(branchName, shouldCleanup, isPreview = false, cwd) {
|
|
2626
|
+
if (shouldCleanup === false) {
|
|
2627
|
+
return {
|
|
2628
|
+
success: true,
|
|
2629
|
+
deleted: false,
|
|
2630
|
+
notFound: true,
|
|
2631
|
+
// Treat "not configured" as "nothing to delete"
|
|
2632
|
+
branchName
|
|
2633
|
+
};
|
|
2634
|
+
}
|
|
2635
|
+
if (!this.provider.isConfigured()) {
|
|
2636
|
+
logger3.debug("Skipping database branch deletion: Database provider not configured");
|
|
2637
|
+
return {
|
|
2638
|
+
success: true,
|
|
2639
|
+
deleted: false,
|
|
2640
|
+
notFound: true,
|
|
2641
|
+
branchName
|
|
2642
|
+
};
|
|
2643
|
+
}
|
|
2644
|
+
if (!await this.provider.isCliAvailable()) {
|
|
2645
|
+
logger3.info("Skipping database branch deletion: CLI tool not available");
|
|
2646
|
+
return {
|
|
2647
|
+
success: false,
|
|
2648
|
+
deleted: false,
|
|
2649
|
+
notFound: true,
|
|
2650
|
+
error: "CLI tool not available",
|
|
2651
|
+
branchName
|
|
2652
|
+
};
|
|
2653
|
+
}
|
|
2654
|
+
try {
|
|
2655
|
+
const isAuth = await this.provider.isAuthenticated(cwd);
|
|
2656
|
+
if (!isAuth) {
|
|
2657
|
+
logger3.warn("Skipping database branch deletion: Not authenticated with DB Provider");
|
|
2658
|
+
return {
|
|
2659
|
+
success: false,
|
|
2660
|
+
deleted: false,
|
|
2661
|
+
notFound: false,
|
|
2662
|
+
error: "Not authenticated with DB Provider",
|
|
2663
|
+
branchName
|
|
2664
|
+
};
|
|
2665
|
+
}
|
|
2666
|
+
} catch (error) {
|
|
2667
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
2668
|
+
logger3.error(`Database authentication check failed: ${errorMessage}`);
|
|
2669
|
+
return {
|
|
2670
|
+
success: false,
|
|
2671
|
+
deleted: false,
|
|
2672
|
+
notFound: false,
|
|
2673
|
+
error: `Authentication check failed: ${errorMessage}`,
|
|
2674
|
+
branchName
|
|
2675
|
+
};
|
|
2676
|
+
}
|
|
2677
|
+
try {
|
|
2678
|
+
const result = await this.provider.deleteBranch(branchName, isPreview, cwd);
|
|
2679
|
+
return result;
|
|
2680
|
+
} catch (error) {
|
|
2681
|
+
logger3.warn(
|
|
2682
|
+
`Unexpected error in database deletion: ${error instanceof Error ? error.message : String(error)}`
|
|
2683
|
+
);
|
|
2684
|
+
return {
|
|
2685
|
+
success: false,
|
|
2686
|
+
deleted: false,
|
|
2687
|
+
notFound: false,
|
|
2688
|
+
error: error instanceof Error ? error.message : String(error),
|
|
2689
|
+
branchName
|
|
2690
|
+
};
|
|
2691
|
+
}
|
|
2692
|
+
}
|
|
2693
|
+
/**
|
|
2694
|
+
* Check if .env has the configured database URL variable
|
|
2695
|
+
* CRITICAL: If user explicitly configured a custom variable name (not default),
|
|
2696
|
+
* throw an error if it's missing from .env
|
|
2697
|
+
*/
|
|
2698
|
+
async hasDatabaseUrlInEnv(envFilePath) {
|
|
2699
|
+
try {
|
|
2700
|
+
const envMap = await this.environment.readEnvFile(envFilePath);
|
|
2701
|
+
if (this.databaseUrlEnvVarName !== "DATABASE_URL") {
|
|
2702
|
+
logger3.debug(`Looking for custom database URL variable: ${this.databaseUrlEnvVarName}`);
|
|
2703
|
+
} else {
|
|
2704
|
+
logger3.debug("Looking for default database URL variable: DATABASE_URL");
|
|
2705
|
+
}
|
|
2706
|
+
if (envMap.has(this.databaseUrlEnvVarName)) {
|
|
2707
|
+
if (this.databaseUrlEnvVarName !== "DATABASE_URL") {
|
|
2708
|
+
logger3.debug(`\u2705 Found custom database URL variable: ${this.databaseUrlEnvVarName}`);
|
|
2709
|
+
} else {
|
|
2710
|
+
logger3.debug(`\u2705 Found default database URL variable: DATABASE_URL`);
|
|
2711
|
+
}
|
|
2712
|
+
return true;
|
|
2713
|
+
}
|
|
2714
|
+
if (this.databaseUrlEnvVarName !== "DATABASE_URL") {
|
|
2715
|
+
logger3.debug(`\u274C Custom database URL variable '${this.databaseUrlEnvVarName}' not found in .env file`);
|
|
2716
|
+
throw new Error(
|
|
2717
|
+
`Configured database URL environment variable '${this.databaseUrlEnvVarName}' not found in .env file. Please add it to your .env file or update your iloom configuration.`
|
|
2718
|
+
);
|
|
2719
|
+
}
|
|
2720
|
+
const hasDefaultVar = envMap.has("DATABASE_URL");
|
|
2721
|
+
if (hasDefaultVar) {
|
|
2722
|
+
logger3.debug("\u2705 Found fallback DATABASE_URL variable");
|
|
2723
|
+
} else {
|
|
2724
|
+
logger3.debug("\u274C No DATABASE_URL variable found in .env file");
|
|
2725
|
+
}
|
|
2726
|
+
return hasDefaultVar;
|
|
2727
|
+
} catch (error) {
|
|
2728
|
+
if (error instanceof Error && error.message.includes("not found in .env")) {
|
|
2729
|
+
throw error;
|
|
2730
|
+
}
|
|
2731
|
+
return false;
|
|
2732
|
+
}
|
|
2733
|
+
}
|
|
2734
|
+
};
|
|
2735
|
+
|
|
2736
|
+
// src/lib/ClaudeService.ts
|
|
2737
|
+
init_claude();
|
|
2738
|
+
|
|
2739
|
+
// src/lib/PromptTemplateManager.ts
|
|
2740
|
+
init_logger();
|
|
2741
|
+
import { readFile as readFile2 } from "fs/promises";
|
|
2742
|
+
import { accessSync } from "fs";
|
|
2743
|
+
import path4 from "path";
|
|
2744
|
+
import { fileURLToPath } from "url";
|
|
2745
|
+
var PromptTemplateManager = class {
|
|
2746
|
+
constructor(templateDir) {
|
|
2747
|
+
if (templateDir) {
|
|
2748
|
+
this.templateDir = templateDir;
|
|
2749
|
+
} else {
|
|
2750
|
+
const currentFileUrl = import.meta.url;
|
|
2751
|
+
const currentFilePath = fileURLToPath(currentFileUrl);
|
|
2752
|
+
const distDir = path4.dirname(currentFilePath);
|
|
2753
|
+
let templateDir2 = path4.join(distDir, "prompts");
|
|
2754
|
+
let currentDir = distDir;
|
|
2755
|
+
while (currentDir !== path4.dirname(currentDir)) {
|
|
2756
|
+
const candidatePath = path4.join(currentDir, "prompts");
|
|
2757
|
+
try {
|
|
2758
|
+
accessSync(candidatePath);
|
|
2759
|
+
templateDir2 = candidatePath;
|
|
2760
|
+
break;
|
|
2761
|
+
} catch {
|
|
2762
|
+
currentDir = path4.dirname(currentDir);
|
|
2763
|
+
}
|
|
2764
|
+
}
|
|
2765
|
+
this.templateDir = templateDir2;
|
|
2766
|
+
logger.debug("PromptTemplateManager initialized", {
|
|
2767
|
+
currentFilePath,
|
|
2768
|
+
distDir,
|
|
2769
|
+
templateDir: this.templateDir
|
|
2770
|
+
});
|
|
2771
|
+
}
|
|
2772
|
+
}
|
|
2773
|
+
/**
|
|
2774
|
+
* Load a template file by name
|
|
2775
|
+
*/
|
|
2776
|
+
async loadTemplate(templateName) {
|
|
2777
|
+
const templatePath = path4.join(this.templateDir, `${templateName}-prompt.txt`);
|
|
2778
|
+
logger.debug("Loading template", {
|
|
2779
|
+
templateName,
|
|
2780
|
+
templateDir: this.templateDir,
|
|
2781
|
+
templatePath
|
|
2782
|
+
});
|
|
2783
|
+
try {
|
|
2784
|
+
return await readFile2(templatePath, "utf-8");
|
|
2785
|
+
} catch (error) {
|
|
2786
|
+
logger.error("Failed to load template", { templateName, templatePath, error });
|
|
2787
|
+
throw new Error(`Template not found: ${templatePath}`);
|
|
2788
|
+
}
|
|
2789
|
+
}
|
|
2790
|
+
/**
|
|
2791
|
+
* Substitute variables in a template string
|
|
2792
|
+
*/
|
|
2793
|
+
substituteVariables(template, variables) {
|
|
2794
|
+
let result = template;
|
|
2795
|
+
result = this.processConditionalSections(result, variables);
|
|
2796
|
+
if (variables.ISSUE_NUMBER !== void 0) {
|
|
2797
|
+
result = result.replace(/ISSUE_NUMBER/g, String(variables.ISSUE_NUMBER));
|
|
2798
|
+
}
|
|
2799
|
+
if (variables.PR_NUMBER !== void 0) {
|
|
2800
|
+
result = result.replace(/PR_NUMBER/g, String(variables.PR_NUMBER));
|
|
2801
|
+
}
|
|
2802
|
+
if (variables.ISSUE_TITLE !== void 0) {
|
|
2803
|
+
result = result.replace(/ISSUE_TITLE/g, variables.ISSUE_TITLE);
|
|
2804
|
+
}
|
|
2805
|
+
if (variables.PR_TITLE !== void 0) {
|
|
2806
|
+
result = result.replace(/PR_TITLE/g, variables.PR_TITLE);
|
|
2807
|
+
}
|
|
2808
|
+
if (variables.WORKSPACE_PATH !== void 0) {
|
|
2809
|
+
result = result.replace(/WORKSPACE_PATH/g, variables.WORKSPACE_PATH);
|
|
2810
|
+
}
|
|
2811
|
+
if (variables.PORT !== void 0) {
|
|
2812
|
+
result = result.replace(/PORT/g, String(variables.PORT));
|
|
2813
|
+
}
|
|
2814
|
+
return result;
|
|
2815
|
+
}
|
|
2816
|
+
/**
|
|
2817
|
+
* Process conditional sections in template
|
|
2818
|
+
* Format: {{#IF ONE_SHOT_MODE}}content{{/IF ONE_SHOT_MODE}}
|
|
2819
|
+
*
|
|
2820
|
+
* Note: /s flag allows . to match newlines
|
|
2821
|
+
*/
|
|
2822
|
+
processConditionalSections(template, variables) {
|
|
2823
|
+
let result = template;
|
|
2824
|
+
const oneShotRegex = /\{\{#IF ONE_SHOT_MODE\}\}(.*?)\{\{\/IF ONE_SHOT_MODE\}\}/gs;
|
|
2825
|
+
if (variables.ONE_SHOT_MODE === true) {
|
|
2826
|
+
result = result.replace(oneShotRegex, "$1");
|
|
2827
|
+
} else {
|
|
2828
|
+
result = result.replace(oneShotRegex, "");
|
|
2829
|
+
}
|
|
2830
|
+
return result;
|
|
2831
|
+
}
|
|
2832
|
+
/**
|
|
2833
|
+
* Get a fully processed prompt for a workflow type
|
|
2834
|
+
*/
|
|
2835
|
+
async getPrompt(type, variables) {
|
|
2836
|
+
const template = await this.loadTemplate(type);
|
|
2837
|
+
return this.substituteVariables(template, variables);
|
|
2838
|
+
}
|
|
2839
|
+
};
|
|
2840
|
+
|
|
2841
|
+
// src/lib/ClaudeService.ts
|
|
2842
|
+
init_SettingsManager();
|
|
2843
|
+
init_logger();
|
|
2844
|
+
var ClaudeService = class {
|
|
2845
|
+
constructor(templateManager, settingsManager) {
|
|
2846
|
+
this.templateManager = templateManager ?? new PromptTemplateManager();
|
|
2847
|
+
this.settingsManager = settingsManager ?? new SettingsManager();
|
|
2848
|
+
}
|
|
2849
|
+
/**
|
|
2850
|
+
* Check if Claude CLI is available
|
|
2851
|
+
*/
|
|
2852
|
+
async isAvailable() {
|
|
2853
|
+
return detectClaudeCli();
|
|
2854
|
+
}
|
|
2855
|
+
/**
|
|
2856
|
+
* Get the appropriate model for a workflow type
|
|
2857
|
+
*/
|
|
2858
|
+
getModelForWorkflow(type) {
|
|
2859
|
+
if (type === "issue") {
|
|
2860
|
+
return "claude-sonnet-4-20250514";
|
|
2861
|
+
}
|
|
2862
|
+
return void 0;
|
|
2863
|
+
}
|
|
2864
|
+
/**
|
|
2865
|
+
* Get the appropriate permission mode for a workflow type
|
|
2866
|
+
*/
|
|
2867
|
+
getPermissionModeForWorkflow(type) {
|
|
2868
|
+
var _a;
|
|
2869
|
+
if ((_a = this.settings) == null ? void 0 : _a.workflows) {
|
|
2870
|
+
const workflowConfig = type === "issue" ? this.settings.workflows.issue : type === "pr" ? this.settings.workflows.pr : this.settings.workflows.regular;
|
|
2871
|
+
if (workflowConfig == null ? void 0 : workflowConfig.permissionMode) {
|
|
2872
|
+
return workflowConfig.permissionMode;
|
|
2873
|
+
}
|
|
2874
|
+
}
|
|
2875
|
+
if (type === "issue") {
|
|
2876
|
+
return "acceptEdits";
|
|
2877
|
+
}
|
|
2878
|
+
return "default";
|
|
2879
|
+
}
|
|
2880
|
+
/**
|
|
2881
|
+
* Launch Claude for a specific workflow
|
|
2882
|
+
*/
|
|
2883
|
+
async launchForWorkflow(options) {
|
|
2884
|
+
const { type, issueNumber, prNumber, title, workspacePath, port, headless = false, branchName, oneShot = "default", setArguments, executablePath } = options;
|
|
2885
|
+
try {
|
|
2886
|
+
this.settings ??= await this.settingsManager.loadSettings();
|
|
2887
|
+
const variables = {
|
|
2888
|
+
WORKSPACE_PATH: workspacePath
|
|
2889
|
+
};
|
|
2890
|
+
if (issueNumber !== void 0) {
|
|
2891
|
+
variables.ISSUE_NUMBER = issueNumber;
|
|
2892
|
+
}
|
|
2893
|
+
if (prNumber !== void 0) {
|
|
2894
|
+
variables.PR_NUMBER = prNumber;
|
|
2895
|
+
}
|
|
2896
|
+
if (title !== void 0) {
|
|
2897
|
+
if (type === "issue") {
|
|
2898
|
+
variables.ISSUE_TITLE = title;
|
|
2899
|
+
} else if (type === "pr") {
|
|
2900
|
+
variables.PR_TITLE = title;
|
|
2901
|
+
}
|
|
2902
|
+
}
|
|
2903
|
+
if (port !== void 0) {
|
|
2904
|
+
variables.PORT = port;
|
|
2905
|
+
}
|
|
2906
|
+
const prompt = await this.templateManager.getPrompt(type, variables);
|
|
2907
|
+
const model = this.getModelForWorkflow(type);
|
|
2908
|
+
const permissionMode = this.getPermissionModeForWorkflow(type);
|
|
2909
|
+
if (permissionMode === "bypassPermissions") {
|
|
2910
|
+
logger.warn(
|
|
2911
|
+
"\u26A0\uFE0F WARNING: Using bypassPermissions mode - Claude will execute all tool calls without confirmation. This can be dangerous. Use with caution."
|
|
2912
|
+
);
|
|
2913
|
+
}
|
|
2914
|
+
const claudeOptions = {
|
|
2915
|
+
addDir: workspacePath,
|
|
2916
|
+
headless
|
|
2917
|
+
};
|
|
2918
|
+
if (model !== void 0) {
|
|
2919
|
+
claudeOptions.model = model;
|
|
2920
|
+
}
|
|
2921
|
+
if (permissionMode !== void 0 && permissionMode !== "default") {
|
|
2922
|
+
claudeOptions.permissionMode = permissionMode;
|
|
2923
|
+
}
|
|
2924
|
+
if (branchName !== void 0) {
|
|
2925
|
+
claudeOptions.branchName = branchName;
|
|
2926
|
+
}
|
|
2927
|
+
if (port !== void 0) {
|
|
2928
|
+
claudeOptions.port = port;
|
|
2929
|
+
}
|
|
2930
|
+
if (setArguments !== void 0) {
|
|
2931
|
+
claudeOptions.setArguments = setArguments;
|
|
2932
|
+
}
|
|
2933
|
+
if (executablePath !== void 0) {
|
|
2934
|
+
claudeOptions.executablePath = executablePath;
|
|
2935
|
+
}
|
|
2936
|
+
logger.debug("Launching Claude for workflow", {
|
|
2937
|
+
type,
|
|
2938
|
+
model,
|
|
2939
|
+
permissionMode,
|
|
2940
|
+
headless,
|
|
2941
|
+
workspacePath
|
|
2942
|
+
});
|
|
2943
|
+
if (headless) {
|
|
2944
|
+
return await launchClaude(prompt, claudeOptions);
|
|
2945
|
+
} else {
|
|
2946
|
+
if (!claudeOptions.addDir) {
|
|
2947
|
+
throw new Error("workspacePath required for interactive workflow launch");
|
|
2948
|
+
}
|
|
2949
|
+
return await launchClaudeInNewTerminalWindow(prompt, {
|
|
2950
|
+
...claudeOptions,
|
|
2951
|
+
workspacePath: claudeOptions.addDir,
|
|
2952
|
+
oneShot
|
|
2953
|
+
});
|
|
2954
|
+
}
|
|
2955
|
+
} catch (error) {
|
|
2956
|
+
logger.error("Failed to launch Claude for workflow", { error, options });
|
|
2957
|
+
throw error;
|
|
2958
|
+
}
|
|
2959
|
+
}
|
|
2960
|
+
/**
|
|
2961
|
+
* Generate branch name with Claude, with fallback on failure
|
|
2962
|
+
*/
|
|
2963
|
+
async generateBranchNameWithFallback(issueTitle, issueNumber) {
|
|
2964
|
+
try {
|
|
2965
|
+
return await generateBranchName(issueTitle, issueNumber);
|
|
2966
|
+
} catch (error) {
|
|
2967
|
+
logger.warn("Claude branch name generation failed, using fallback", { error });
|
|
2968
|
+
return `feat/issue-${issueNumber}`;
|
|
2969
|
+
}
|
|
2970
|
+
}
|
|
2971
|
+
};
|
|
2972
|
+
|
|
2973
|
+
// src/lib/ClaudeContextManager.ts
|
|
2974
|
+
init_logger();
|
|
2975
|
+
var ClaudeContextManager = class {
|
|
2976
|
+
constructor(claudeService, _promptTemplateManager, settingsManager) {
|
|
2977
|
+
this.claudeService = claudeService ?? new ClaudeService(void 0, settingsManager);
|
|
2978
|
+
}
|
|
2979
|
+
/**
|
|
2980
|
+
* Prepare context for Claude launch
|
|
2981
|
+
* Placeholder for future .claude-context.md generation (Issue #11)
|
|
2982
|
+
*/
|
|
2983
|
+
async prepareContext(context) {
|
|
2984
|
+
if (!context.workspacePath) {
|
|
2985
|
+
throw new Error("Workspace path is required");
|
|
2986
|
+
}
|
|
2987
|
+
if (context.type === "issue" && typeof context.identifier !== "number") {
|
|
2988
|
+
throw new Error("Issue identifier must be a number");
|
|
2989
|
+
}
|
|
2990
|
+
if (context.type === "pr" && typeof context.identifier !== "number") {
|
|
2991
|
+
throw new Error("PR identifier must be a number");
|
|
2992
|
+
}
|
|
2993
|
+
logger.debug("Context prepared", { context });
|
|
2994
|
+
}
|
|
2995
|
+
/**
|
|
2996
|
+
* Launch Claude with the prepared context
|
|
2997
|
+
*/
|
|
2998
|
+
async launchWithContext(context, headless = false) {
|
|
2999
|
+
await this.prepareContext(context);
|
|
3000
|
+
const workflowOptions = {
|
|
3001
|
+
type: context.type,
|
|
3002
|
+
workspacePath: context.workspacePath,
|
|
3003
|
+
...context.port !== void 0 && { port: context.port },
|
|
3004
|
+
headless,
|
|
3005
|
+
oneShot: context.oneShot ?? "default"
|
|
3006
|
+
};
|
|
3007
|
+
if (context.title !== void 0) {
|
|
3008
|
+
workflowOptions.title = context.title;
|
|
3009
|
+
}
|
|
3010
|
+
if (context.branchName !== void 0) {
|
|
3011
|
+
workflowOptions.branchName = context.branchName;
|
|
3012
|
+
}
|
|
3013
|
+
if (context.setArguments !== void 0) {
|
|
3014
|
+
workflowOptions.setArguments = context.setArguments;
|
|
3015
|
+
}
|
|
3016
|
+
if (context.executablePath !== void 0) {
|
|
3017
|
+
workflowOptions.executablePath = context.executablePath;
|
|
3018
|
+
}
|
|
3019
|
+
if (context.type === "issue") {
|
|
3020
|
+
workflowOptions.issueNumber = context.identifier;
|
|
3021
|
+
} else if (context.type === "pr") {
|
|
3022
|
+
workflowOptions.prNumber = context.identifier;
|
|
3023
|
+
}
|
|
3024
|
+
return this.claudeService.launchForWorkflow(workflowOptions);
|
|
3025
|
+
}
|
|
3026
|
+
};
|
|
3027
|
+
|
|
3028
|
+
// src/utils/index.ts
|
|
3029
|
+
init_logger();
|
|
3030
|
+
export {
|
|
3031
|
+
ClaudeContextManager,
|
|
3032
|
+
DatabaseManager,
|
|
3033
|
+
EnvironmentManager,
|
|
3034
|
+
GitHubService,
|
|
3035
|
+
GitWorktreeManager,
|
|
3036
|
+
WorkspaceManager,
|
|
3037
|
+
branchExists,
|
|
3038
|
+
createLogger,
|
|
3039
|
+
ensureRepositoryHasCommits,
|
|
3040
|
+
executeGitCommand,
|
|
3041
|
+
extractPRNumber,
|
|
3042
|
+
findAllBranchesForIssue,
|
|
3043
|
+
findMainWorktreePath,
|
|
3044
|
+
findMainWorktreePathWithSettings,
|
|
3045
|
+
generateWorktreePath,
|
|
3046
|
+
getCurrentBranch,
|
|
3047
|
+
getDefaultBranch,
|
|
3048
|
+
getRepoRoot,
|
|
3049
|
+
hasUncommittedChanges,
|
|
3050
|
+
isEmptyRepository,
|
|
3051
|
+
isPRBranch,
|
|
3052
|
+
isValidGitRepo,
|
|
3053
|
+
isWorktreePath,
|
|
3054
|
+
logger,
|
|
3055
|
+
parseWorktreeList,
|
|
3056
|
+
pushBranchToRemote
|
|
3057
|
+
};
|
|
3058
|
+
//# sourceMappingURL=index.js.map
|