@iloom/cli 0.2.0 → 0.3.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +234 -667
- package/dist/BranchNamingService-OMWKUYMM.js +13 -0
- package/dist/ClaudeContextManager-3VXA6UPR.js +13 -0
- package/dist/ClaudeService-6CPK43N4.js +12 -0
- package/dist/GitHubService-EBOETDIW.js +11 -0
- package/dist/{LoomLauncher-CTSWJL35.js → LoomLauncher-JF7JZMTZ.js} +63 -32
- package/dist/LoomLauncher-JF7JZMTZ.js.map +1 -0
- package/dist/ProjectCapabilityDetector-34LU7JJ4.js +9 -0
- package/dist/{PromptTemplateManager-WII75TKH.js → PromptTemplateManager-A52RUAMS.js} +2 -2
- package/dist/README.md +234 -667
- package/dist/{SettingsManager-XOYCLH3D.js → SettingsManager-ZCWJ56WP.js} +12 -4
- package/dist/SettingsMigrationManager-AGIIIPDQ.js +10 -0
- package/dist/agents/iloom-issue-analyze-and-plan.md +125 -35
- package/dist/agents/iloom-issue-analyzer.md +284 -32
- package/dist/agents/iloom-issue-complexity-evaluator.md +40 -21
- package/dist/agents/iloom-issue-enhancer.md +69 -48
- package/dist/agents/iloom-issue-implementer.md +36 -25
- package/dist/agents/iloom-issue-planner.md +35 -24
- package/dist/agents/iloom-issue-reviewer.md +62 -9
- package/dist/{chunk-SWCRXDZC.js → chunk-3RUPPQRG.js} +1 -18
- package/dist/chunk-3RUPPQRG.js.map +1 -0
- package/dist/{chunk-HBVFXN7R.js → chunk-4BGK7T6X.js} +26 -3
- package/dist/chunk-4BGK7T6X.js.map +1 -0
- package/dist/{chunk-6LEQW46Y.js → chunk-4E4LD3QR.js} +72 -2
- package/dist/{chunk-6LEQW46Y.js.map → chunk-4E4LD3QR.js.map} +1 -1
- package/dist/{chunk-CWR2SANQ.js → chunk-EBISESAP.js} +1 -1
- package/dist/{chunk-TS6DL67T.js → chunk-G2IEYOLQ.js} +11 -38
- package/dist/chunk-G2IEYOLQ.js.map +1 -0
- package/dist/chunk-HBYZH6GD.js +1989 -0
- package/dist/chunk-HBYZH6GD.js.map +1 -0
- package/dist/chunk-INW24J2W.js +55 -0
- package/dist/chunk-INW24J2W.js.map +1 -0
- package/dist/{chunk-ZMNQBJUI.js → chunk-IP7SMKIF.js} +61 -22
- package/dist/chunk-IP7SMKIF.js.map +1 -0
- package/dist/{chunk-4IV6W4U5.js → chunk-IXKLYTWO.js} +12 -12
- package/dist/chunk-IXKLYTWO.js.map +1 -0
- package/dist/{chunk-JNKJ7NJV.js → chunk-JKXJ7BGL.js} +6 -2
- package/dist/{chunk-JNKJ7NJV.js.map → chunk-JKXJ7BGL.js.map} +1 -1
- package/dist/{chunk-LAPY6NAE.js → chunk-JQFO7QQN.js} +68 -12
- package/dist/{chunk-LAPY6NAE.js.map → chunk-JQFO7QQN.js.map} +1 -1
- package/dist/{SettingsMigrationManager-MTQIMI54.js → chunk-KLBYVHPK.js} +3 -2
- package/dist/{chunk-USVVV3FP.js → chunk-MKWYLDFK.js} +5 -5
- package/dist/chunk-O5OH5MRX.js +396 -0
- package/dist/chunk-O5OH5MRX.js.map +1 -0
- package/dist/{chunk-DJUGYNQE.js → chunk-PA6Q6AWM.js} +16 -3
- package/dist/chunk-PA6Q6AWM.js.map +1 -0
- package/dist/chunk-RO26VS3W.js +444 -0
- package/dist/chunk-RO26VS3W.js.map +1 -0
- package/dist/{chunk-VETG35MF.js → chunk-TSKY3JI7.js} +3 -3
- package/dist/{chunk-VETG35MF.js.map → chunk-TSKY3JI7.js.map} +1 -1
- package/dist/{chunk-LHP6ROUM.js → chunk-U5QDY7ZD.js} +4 -16
- package/dist/chunk-U5QDY7ZD.js.map +1 -0
- package/dist/{chunk-SPYPLHMK.js → chunk-VU3QMIP2.js} +34 -2
- package/dist/chunk-VU3QMIP2.js.map +1 -0
- package/dist/{chunk-PVAVNJKS.js → chunk-WEN5C5DM.js} +10 -1
- package/dist/chunk-WEN5C5DM.js.map +1 -0
- package/dist/{chunk-2PLUQT6J.js → chunk-XPKDPZ5D.js} +2 -2
- package/dist/{chunk-RF2YI2XJ.js → chunk-ZBQVSHVT.js} +5 -5
- package/dist/chunk-ZBQVSHVT.js.map +1 -0
- package/dist/{chunk-GZP4UGGM.js → chunk-ZM3CFL5L.js} +2 -2
- package/dist/{chunk-BLCTGFZN.js → chunk-ZT3YZB4K.js} +3 -4
- package/dist/chunk-ZT3YZB4K.js.map +1 -0
- package/dist/{chunk-MFU53H6J.js → chunk-ZWFBBPJI.js} +6 -6
- package/dist/{chunk-MFU53H6J.js.map → chunk-ZWFBBPJI.js.map} +1 -1
- package/dist/{claude-ZIWDG4XG.js → claude-LUZ35IMK.js} +2 -2
- package/dist/{cleanup-FEIVZSIV.js → cleanup-3MONU4PU.js} +88 -27
- package/dist/cleanup-3MONU4PU.js.map +1 -0
- package/dist/cli.js +2511 -62
- package/dist/cli.js.map +1 -1
- package/dist/{contribute-EMZKCAC6.js → contribute-UWJAGIG7.js} +6 -6
- package/dist/{feedback-LFNMQBAZ.js → feedback-W3BXTGIM.js} +15 -14
- package/dist/{feedback-LFNMQBAZ.js.map → feedback-W3BXTGIM.js.map} +1 -1
- package/dist/{git-WC6HZLOT.js → git-34Z6QVDS.js} +4 -2
- package/dist/{ignite-MQWVJEAB.js → ignite-KVJEFXNO.js} +32 -27
- package/dist/ignite-KVJEFXNO.js.map +1 -0
- package/dist/index.d.ts +359 -45
- package/dist/index.js +1267 -503
- package/dist/index.js.map +1 -1
- package/dist/{init-GJDYN2IK.js → init-L55Q73H4.js} +104 -40
- package/dist/init-L55Q73H4.js.map +1 -0
- package/dist/mcp/issue-management-server.js +934 -0
- package/dist/mcp/issue-management-server.js.map +1 -0
- package/dist/{neon-helpers-ZVIRPKCI.js → neon-helpers-WPUACUVC.js} +3 -3
- package/dist/neon-helpers-WPUACUVC.js.map +1 -0
- package/dist/{open-NXSN7XOC.js → open-LNRZL3UU.js} +39 -36
- package/dist/open-LNRZL3UU.js.map +1 -0
- package/dist/{prompt-ANTQWHUF.js → prompt-7INJ7YRU.js} +4 -2
- package/dist/prompt-7INJ7YRU.js.map +1 -0
- package/dist/prompts/init-prompt.txt +541 -98
- package/dist/prompts/issue-prompt.txt +27 -27
- package/dist/{rebase-DUNFOJVS.js → rebase-C4WNCVGM.js} +6 -6
- package/dist/{remote-ZCXJVVNW.js → remote-VUNCQZ6J.js} +3 -2
- package/dist/remote-VUNCQZ6J.js.map +1 -0
- package/dist/{run-O7ZK7CKA.js → run-IOGNIOYN.js} +39 -36
- package/dist/run-IOGNIOYN.js.map +1 -0
- package/dist/schema/settings.schema.json +59 -3
- package/dist/{test-git-T76HOTIA.js → test-git-J7I5MFYH.js} +3 -3
- package/dist/{test-prefix-6HJUVQMH.js → test-prefix-ZCONBCBX.js} +3 -3
- package/dist/{test-webserver-M2I3EV4J.js → test-webserver-DAHONWCS.js} +4 -4
- package/dist/test-webserver-DAHONWCS.js.map +1 -0
- package/package.json +3 -2
- package/dist/ClaudeContextManager-LVCYRM6Q.js +0 -13
- package/dist/ClaudeService-WVTWB3DK.js +0 -12
- package/dist/GitHubService-7E2S5NNZ.js +0 -11
- package/dist/LoomLauncher-CTSWJL35.js.map +0 -1
- package/dist/add-issue-OBI325W7.js +0 -69
- package/dist/add-issue-OBI325W7.js.map +0 -1
- package/dist/chunk-4IV6W4U5.js.map +0 -1
- package/dist/chunk-BLCTGFZN.js.map +0 -1
- package/dist/chunk-CVLAZRNB.js +0 -54
- package/dist/chunk-CVLAZRNB.js.map +0 -1
- package/dist/chunk-DJUGYNQE.js.map +0 -1
- package/dist/chunk-H4E4THUZ.js +0 -55
- package/dist/chunk-H4E4THUZ.js.map +0 -1
- package/dist/chunk-H5LDRGVK.js +0 -642
- package/dist/chunk-H5LDRGVK.js.map +0 -1
- package/dist/chunk-HBVFXN7R.js.map +0 -1
- package/dist/chunk-LHP6ROUM.js.map +0 -1
- package/dist/chunk-PVAVNJKS.js.map +0 -1
- package/dist/chunk-RF2YI2XJ.js.map +0 -1
- package/dist/chunk-SPYPLHMK.js.map +0 -1
- package/dist/chunk-SWCRXDZC.js.map +0 -1
- package/dist/chunk-SYOSCMIT.js +0 -545
- package/dist/chunk-SYOSCMIT.js.map +0 -1
- package/dist/chunk-T3KEIB4D.js +0 -243
- package/dist/chunk-T3KEIB4D.js.map +0 -1
- package/dist/chunk-TS6DL67T.js.map +0 -1
- package/dist/chunk-ZMNQBJUI.js.map +0 -1
- package/dist/cleanup-FEIVZSIV.js.map +0 -1
- package/dist/enhance-MNA4ZGXW.js +0 -176
- package/dist/enhance-MNA4ZGXW.js.map +0 -1
- package/dist/finish-TX5CJICB.js +0 -1749
- package/dist/finish-TX5CJICB.js.map +0 -1
- package/dist/ignite-MQWVJEAB.js.map +0 -1
- package/dist/init-GJDYN2IK.js.map +0 -1
- package/dist/mcp/chunk-6SDFJ42P.js +0 -62
- package/dist/mcp/chunk-6SDFJ42P.js.map +0 -1
- package/dist/mcp/claude-NDFOCQQQ.js +0 -249
- package/dist/mcp/claude-NDFOCQQQ.js.map +0 -1
- package/dist/mcp/color-QS5BFCNN.js +0 -168
- package/dist/mcp/color-QS5BFCNN.js.map +0 -1
- package/dist/mcp/github-comment-server.js +0 -168
- package/dist/mcp/github-comment-server.js.map +0 -1
- package/dist/mcp/terminal-OMNRFWB3.js +0 -227
- package/dist/mcp/terminal-OMNRFWB3.js.map +0 -1
- package/dist/open-NXSN7XOC.js.map +0 -1
- package/dist/run-O7ZK7CKA.js.map +0 -1
- package/dist/start-73I5W7WW.js +0 -983
- package/dist/start-73I5W7WW.js.map +0 -1
- package/dist/test-webserver-M2I3EV4J.js.map +0 -1
- /package/dist/{ClaudeContextManager-LVCYRM6Q.js.map → BranchNamingService-OMWKUYMM.js.map} +0 -0
- /package/dist/{ClaudeService-WVTWB3DK.js.map → ClaudeContextManager-3VXA6UPR.js.map} +0 -0
- /package/dist/{GitHubService-7E2S5NNZ.js.map → ClaudeService-6CPK43N4.js.map} +0 -0
- /package/dist/{PromptTemplateManager-WII75TKH.js.map → GitHubService-EBOETDIW.js.map} +0 -0
- /package/dist/{SettingsManager-XOYCLH3D.js.map → ProjectCapabilityDetector-34LU7JJ4.js.map} +0 -0
- /package/dist/{claude-ZIWDG4XG.js.map → PromptTemplateManager-A52RUAMS.js.map} +0 -0
- /package/dist/{git-WC6HZLOT.js.map → SettingsManager-ZCWJ56WP.js.map} +0 -0
- /package/dist/{neon-helpers-ZVIRPKCI.js.map → SettingsMigrationManager-AGIIIPDQ.js.map} +0 -0
- /package/dist/{chunk-CWR2SANQ.js.map → chunk-EBISESAP.js.map} +0 -0
- /package/dist/{SettingsMigrationManager-MTQIMI54.js.map → chunk-KLBYVHPK.js.map} +0 -0
- /package/dist/{chunk-USVVV3FP.js.map → chunk-MKWYLDFK.js.map} +0 -0
- /package/dist/{chunk-2PLUQT6J.js.map → chunk-XPKDPZ5D.js.map} +0 -0
- /package/dist/{chunk-GZP4UGGM.js.map → chunk-ZM3CFL5L.js.map} +0 -0
- /package/dist/{prompt-ANTQWHUF.js.map → claude-LUZ35IMK.js.map} +0 -0
- /package/dist/{contribute-EMZKCAC6.js.map → contribute-UWJAGIG7.js.map} +0 -0
- /package/dist/{remote-ZCXJVVNW.js.map → git-34Z6QVDS.js.map} +0 -0
- /package/dist/{rebase-DUNFOJVS.js.map → rebase-C4WNCVGM.js.map} +0 -0
- /package/dist/{test-git-T76HOTIA.js.map → test-git-J7I5MFYH.js.map} +0 -0
- /package/dist/{test-prefix-6HJUVQMH.js.map → test-prefix-ZCONBCBX.js.map} +0 -0
package/dist/index.js
CHANGED
|
@@ -147,18 +147,23 @@ var SettingsManager_exports = {};
|
|
|
147
147
|
__export(SettingsManager_exports, {
|
|
148
148
|
AgentSettingsSchema: () => AgentSettingsSchema,
|
|
149
149
|
CapabilitiesSettingsSchema: () => CapabilitiesSettingsSchema,
|
|
150
|
+
CapabilitiesSettingsSchemaNoDefaults: () => CapabilitiesSettingsSchemaNoDefaults,
|
|
150
151
|
DatabaseProvidersSettingsSchema: () => DatabaseProvidersSettingsSchema,
|
|
151
152
|
IloomSettingsSchema: () => IloomSettingsSchema,
|
|
153
|
+
IloomSettingsSchemaNoDefaults: () => IloomSettingsSchemaNoDefaults,
|
|
152
154
|
NeonSettingsSchema: () => NeonSettingsSchema,
|
|
153
155
|
SettingsManager: () => SettingsManager,
|
|
154
156
|
WorkflowPermissionSchema: () => WorkflowPermissionSchema,
|
|
155
|
-
|
|
157
|
+
WorkflowPermissionSchemaNoDefaults: () => WorkflowPermissionSchemaNoDefaults,
|
|
158
|
+
WorkflowsSettingsSchema: () => WorkflowsSettingsSchema,
|
|
159
|
+
WorkflowsSettingsSchemaNoDefaults: () => WorkflowsSettingsSchemaNoDefaults
|
|
156
160
|
});
|
|
157
161
|
import { readFile } from "fs/promises";
|
|
158
162
|
import path from "path";
|
|
163
|
+
import os from "os";
|
|
159
164
|
import { z } from "zod";
|
|
160
165
|
import deepmerge from "deepmerge";
|
|
161
|
-
var AgentSettingsSchema, WorkflowPermissionSchema, WorkflowsSettingsSchema, CapabilitiesSettingsSchema, NeonSettingsSchema, DatabaseProvidersSettingsSchema, IloomSettingsSchema, SettingsManager;
|
|
166
|
+
var AgentSettingsSchema, WorkflowPermissionSchema, WorkflowPermissionSchemaNoDefaults, WorkflowsSettingsSchema, WorkflowsSettingsSchemaNoDefaults, CapabilitiesSettingsSchema, CapabilitiesSettingsSchemaNoDefaults, NeonSettingsSchema, DatabaseProvidersSettingsSchema, IloomSettingsSchema, IloomSettingsSchemaNoDefaults, SettingsManager;
|
|
162
167
|
var init_SettingsManager = __esm({
|
|
163
168
|
"src/lib/SettingsManager.ts"() {
|
|
164
169
|
"use strict";
|
|
@@ -172,14 +177,27 @@ var init_SettingsManager = __esm({
|
|
|
172
177
|
noVerify: z.boolean().optional().describe("Skip pre-commit hooks (--no-verify) when committing during finish workflow"),
|
|
173
178
|
startIde: z.boolean().default(true).describe("Launch IDE (code) when starting this workflow type"),
|
|
174
179
|
startDevServer: z.boolean().default(true).describe("Launch development server when starting this workflow type"),
|
|
175
|
-
startAiAgent: z.boolean().default(true).describe("Launch Claude
|
|
180
|
+
startAiAgent: z.boolean().default(true).describe("Launch Claude Code agent when starting this workflow type"),
|
|
176
181
|
startTerminal: z.boolean().default(false).describe("Launch terminal window without dev server when starting this workflow type")
|
|
177
182
|
});
|
|
183
|
+
WorkflowPermissionSchemaNoDefaults = z.object({
|
|
184
|
+
permissionMode: z.enum(["plan", "acceptEdits", "bypassPermissions", "default"]).optional().describe("Permission mode for Claude CLI in this workflow type"),
|
|
185
|
+
noVerify: z.boolean().optional().describe("Skip pre-commit hooks (--no-verify) when committing during finish workflow"),
|
|
186
|
+
startIde: z.boolean().optional().describe("Launch IDE (code) when starting this workflow type"),
|
|
187
|
+
startDevServer: z.boolean().optional().describe("Launch development server when starting this workflow type"),
|
|
188
|
+
startAiAgent: z.boolean().optional().describe("Launch Claude Code agent when starting this workflow type"),
|
|
189
|
+
startTerminal: z.boolean().optional().describe("Launch terminal window without dev server when starting this workflow type")
|
|
190
|
+
});
|
|
178
191
|
WorkflowsSettingsSchema = z.object({
|
|
179
192
|
issue: WorkflowPermissionSchema.optional(),
|
|
180
193
|
pr: WorkflowPermissionSchema.optional(),
|
|
181
194
|
regular: WorkflowPermissionSchema.optional()
|
|
182
195
|
}).optional();
|
|
196
|
+
WorkflowsSettingsSchemaNoDefaults = z.object({
|
|
197
|
+
issue: WorkflowPermissionSchemaNoDefaults.optional(),
|
|
198
|
+
pr: WorkflowPermissionSchemaNoDefaults.optional(),
|
|
199
|
+
regular: WorkflowPermissionSchemaNoDefaults.optional()
|
|
200
|
+
}).optional();
|
|
183
201
|
CapabilitiesSettingsSchema = z.object({
|
|
184
202
|
web: z.object({
|
|
185
203
|
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)")
|
|
@@ -188,6 +206,14 @@ var init_SettingsManager = __esm({
|
|
|
188
206
|
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")
|
|
189
207
|
}).optional()
|
|
190
208
|
}).optional();
|
|
209
|
+
CapabilitiesSettingsSchemaNoDefaults = z.object({
|
|
210
|
+
web: z.object({
|
|
211
|
+
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)")
|
|
212
|
+
}).optional(),
|
|
213
|
+
database: z.object({
|
|
214
|
+
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().describe("Name of environment variable for database connection URL")
|
|
215
|
+
}).optional()
|
|
216
|
+
}).optional();
|
|
191
217
|
NeonSettingsSchema = z.object({
|
|
192
218
|
projectId: z.string().min(1).regex(/^[a-zA-Z0-9-]+$/, "Neon project ID must contain only letters, numbers, and hyphens").describe('Neon project ID found in your project URL (e.g., "fantastic-fox-3566354")'),
|
|
193
219
|
parentBranch: z.string().min(1).describe("Branch from which new database branches are created")
|
|
@@ -199,6 +225,9 @@ var init_SettingsManager = __esm({
|
|
|
199
225
|
}).optional();
|
|
200
226
|
IloomSettingsSchema = z.object({
|
|
201
227
|
mainBranch: z.string().min(1, "Settings 'mainBranch' cannot be empty").optional().describe("Name of the main/primary branch for the repository"),
|
|
228
|
+
sourceEnvOnStart: z.boolean().default(false).describe(
|
|
229
|
+
"Source .env file when launching terminal processes (Claude, dev server, terminal). NOTE TO AI AGENTS: Before making changes to settings files, YOU MUST OFFER to check .env compatibility. WARNING: Unquoted values in .env files may break sourcing in bash/zsh shells. Before enabling, verify your .env file does not contain unquoted special characters (e.g., database URLs with ?, &, or other shell metacharacters). Shell compatibility issues may cause processes to fail or behave unexpectedly."
|
|
230
|
+
),
|
|
202
231
|
worktreePrefix: z.string().optional().refine(
|
|
203
232
|
(val) => {
|
|
204
233
|
if (val === void 0) return true;
|
|
@@ -228,32 +257,107 @@ var init_SettingsManager = __esm({
|
|
|
228
257
|
capabilities: CapabilitiesSettingsSchema.describe("Project capability configurations"),
|
|
229
258
|
databaseProviders: DatabaseProvidersSettingsSchema.describe("Database provider configurations"),
|
|
230
259
|
issueManagement: z.object({
|
|
260
|
+
provider: z.enum(["github", "linear"]).optional().default("github").describe("Issue tracker provider (github, linear)"),
|
|
231
261
|
github: z.object({
|
|
232
262
|
remote: z.string().min(1, "Remote name cannot be empty").describe("Git remote name to use for GitHub operations")
|
|
263
|
+
}).optional(),
|
|
264
|
+
linear: z.object({
|
|
265
|
+
teamId: z.string().min(1, "Team ID cannot be empty").describe('Linear team identifier (e.g., "ENG", "PLAT")'),
|
|
266
|
+
branchFormat: z.string().optional().describe("Branch naming template for Linear issues"),
|
|
267
|
+
apiToken: z.string().optional().describe("Linear API token (lin_api_...). SECURITY: Store in settings.local.json only, never commit to source control.")
|
|
233
268
|
}).optional()
|
|
234
269
|
}).optional().describe("Issue management configuration"),
|
|
235
270
|
mergeBehavior: z.object({
|
|
236
271
|
mode: z.enum(["local", "github-pr"]).default("local"),
|
|
237
272
|
remote: z.string().optional()
|
|
238
|
-
}).optional().describe("Merge behavior configuration: local (merge locally) or github-pr (create PR)")
|
|
273
|
+
}).optional().describe("Merge behavior configuration: local (merge locally) or github-pr (create PR)"),
|
|
274
|
+
ide: z.object({
|
|
275
|
+
type: z.enum(["vscode", "cursor", "webstorm", "sublime", "intellij", "windsurf"]).default("vscode").describe(
|
|
276
|
+
"IDE to launch when starting a loom. Options: vscode (Visual Studio Code), cursor (Cursor AI editor), webstorm (JetBrains WebStorm), sublime (Sublime Text), intellij (JetBrains IntelliJ IDEA), windsurf (Windsurf editor)."
|
|
277
|
+
)
|
|
278
|
+
}).optional().describe(
|
|
279
|
+
"IDE configuration for workspace launches. Controls which editor opens when you start a loom. Supports VSCode, Cursor, WebStorm, Sublime Text, IntelliJ, and Windsurf. Note: Color synchronization (title bar colors) only works with VSCode-compatible editors (vscode, cursor, windsurf)."
|
|
280
|
+
)
|
|
281
|
+
});
|
|
282
|
+
IloomSettingsSchemaNoDefaults = z.object({
|
|
283
|
+
mainBranch: z.string().min(1, "Settings 'mainBranch' cannot be empty").optional().describe("Name of the main/primary branch for the repository"),
|
|
284
|
+
sourceEnvOnStart: z.boolean().optional().describe(
|
|
285
|
+
"Source .env file when launching terminal processes (Claude, dev server, terminal). NOTE TO AI AGENTS: Before making changes to settings files, YOU MUST OFFER to check .env compatibility. WARNING: Unquoted values in .env files may break sourcing in bash/zsh shells. Before enabling, verify your .env file does not contain unquoted special characters (e.g., database URLs with ?, &, or other shell metacharacters). Shell compatibility issues may cause processes to fail or behave unexpectedly."
|
|
286
|
+
),
|
|
287
|
+
worktreePrefix: z.string().optional().refine(
|
|
288
|
+
(val) => {
|
|
289
|
+
if (val === void 0) return true;
|
|
290
|
+
if (val === "") return true;
|
|
291
|
+
const allowedChars = /^[a-zA-Z0-9\-_/]+$/;
|
|
292
|
+
if (!allowedChars.test(val)) return false;
|
|
293
|
+
if (/^[-_/]+$/.test(val)) return false;
|
|
294
|
+
const segments = val.split("/");
|
|
295
|
+
for (const segment of segments) {
|
|
296
|
+
if (segment && /^[-_]+$/.test(segment)) {
|
|
297
|
+
return false;
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
return true;
|
|
301
|
+
},
|
|
302
|
+
{
|
|
303
|
+
message: "worktreePrefix contains invalid characters. Only alphanumeric characters, hyphens (-), underscores (_), and forward slashes (/) are allowed. Use forward slashes for nested directories."
|
|
304
|
+
}
|
|
305
|
+
).describe(
|
|
306
|
+
"Prefix for worktree directories. Empty string disables prefix. Defaults to <repo-name>-looms if not set."
|
|
307
|
+
),
|
|
308
|
+
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"])'),
|
|
309
|
+
workflows: WorkflowsSettingsSchemaNoDefaults.describe("Per-workflow-type permission configurations"),
|
|
310
|
+
agents: z.record(z.string(), AgentSettingsSchema).optional().nullable().describe(
|
|
311
|
+
"Per-agent configuration overrides. Available agents: iloom-issue-analyzer (analyzes issues), iloom-issue-planner (creates implementation plans), iloom-issue-analyze-and-plan (combined analysis and planning), iloom-issue-complexity-evaluator (evaluates complexity), iloom-issue-enhancer (enhances issue descriptions), iloom-issue-implementer (implements code changes), iloom-issue-reviewer (reviews code changes against requirements)"
|
|
312
|
+
),
|
|
313
|
+
capabilities: CapabilitiesSettingsSchemaNoDefaults.describe("Project capability configurations"),
|
|
314
|
+
databaseProviders: DatabaseProvidersSettingsSchema.describe("Database provider configurations"),
|
|
315
|
+
issueManagement: z.object({
|
|
316
|
+
provider: z.enum(["github", "linear"]).optional().describe("Issue tracker provider (github, linear)"),
|
|
317
|
+
github: z.object({
|
|
318
|
+
remote: z.string().min(1, "Remote name cannot be empty").describe("Git remote name to use for GitHub operations")
|
|
319
|
+
}).optional(),
|
|
320
|
+
linear: z.object({
|
|
321
|
+
teamId: z.string().min(1, "Team ID cannot be empty").describe('Linear team identifier (e.g., "ENG", "PLAT")'),
|
|
322
|
+
branchFormat: z.string().optional().describe("Branch naming template for Linear issues"),
|
|
323
|
+
apiToken: z.string().optional().describe("Linear API token (lin_api_...). SECURITY: Store in settings.local.json only, never commit to source control.")
|
|
324
|
+
}).optional()
|
|
325
|
+
}).optional().describe("Issue management configuration"),
|
|
326
|
+
mergeBehavior: z.object({
|
|
327
|
+
mode: z.enum(["local", "github-pr"]).optional(),
|
|
328
|
+
remote: z.string().optional()
|
|
329
|
+
}).optional().describe("Merge behavior configuration: local (merge locally) or github-pr (create PR)"),
|
|
330
|
+
ide: z.object({
|
|
331
|
+
type: z.enum(["vscode", "cursor", "webstorm", "sublime", "intellij", "windsurf"]).optional().describe(
|
|
332
|
+
"IDE to launch when starting a loom. Options: vscode (Visual Studio Code), cursor (Cursor AI editor), webstorm (JetBrains WebStorm), sublime (Sublime Text), intellij (JetBrains IntelliJ IDEA), windsurf (Windsurf editor)."
|
|
333
|
+
)
|
|
334
|
+
}).optional().describe(
|
|
335
|
+
"IDE configuration for workspace launches. Controls which editor opens when you start a loom. Supports VSCode, Cursor, WebStorm, Sublime Text, IntelliJ, and Windsurf. Note: Color synchronization (title bar colors) only works with VSCode-compatible editors (vscode, cursor, windsurf)."
|
|
336
|
+
)
|
|
239
337
|
});
|
|
240
338
|
SettingsManager = class {
|
|
241
339
|
/**
|
|
242
|
-
* Load settings from
|
|
243
|
-
*
|
|
244
|
-
*
|
|
245
|
-
*
|
|
340
|
+
* Load settings from global, project, and local sources with proper precedence
|
|
341
|
+
* Merge hierarchy (lowest to highest priority):
|
|
342
|
+
* 1. Global settings (~/.config/iloom-ai/settings.json)
|
|
343
|
+
* 2. Project settings (<PROJECT_ROOT>/.iloom/settings.json)
|
|
344
|
+
* 3. Local settings (<PROJECT_ROOT>/.iloom/settings.local.json)
|
|
345
|
+
* 4. CLI overrides (--set flags)
|
|
346
|
+
* Returns empty object if all files don't exist (not an error)
|
|
246
347
|
*/
|
|
247
348
|
async loadSettings(projectRoot, cliOverrides) {
|
|
248
349
|
const root = this.getProjectRoot(projectRoot);
|
|
350
|
+
const globalSettings = await this.loadGlobalSettingsFile();
|
|
351
|
+
const globalSettingsPath = this.getGlobalSettingsPath();
|
|
352
|
+
logger.debug(`\u{1F30D} Global settings from ${globalSettingsPath}:`, JSON.stringify(globalSettings, null, 2));
|
|
249
353
|
const baseSettings = await this.loadSettingsFile(root, "settings.json");
|
|
250
354
|
const baseSettingsPath = path.join(root, ".iloom", "settings.json");
|
|
251
355
|
logger.debug(`\u{1F4C4} Base settings from ${baseSettingsPath}:`, JSON.stringify(baseSettings, null, 2));
|
|
252
356
|
const localSettings = await this.loadSettingsFile(root, "settings.local.json");
|
|
253
357
|
const localSettingsPath = path.join(root, ".iloom", "settings.local.json");
|
|
254
358
|
logger.debug(`\u{1F4C4} Local settings from ${localSettingsPath}:`, JSON.stringify(localSettings, null, 2));
|
|
255
|
-
let merged = this.mergeSettings(baseSettings, localSettings);
|
|
256
|
-
logger.debug("\u{1F504} After merging base + local settings:", JSON.stringify(merged, null, 2));
|
|
359
|
+
let merged = this.mergeSettings(this.mergeSettings(globalSettings, baseSettings), localSettings);
|
|
360
|
+
logger.debug("\u{1F504} After merging global + base + local settings:", JSON.stringify(merged, null, 2));
|
|
257
361
|
if (cliOverrides && Object.keys(cliOverrides).length > 0) {
|
|
258
362
|
logger.debug("\u2699\uFE0F CLI overrides to apply:", JSON.stringify(cliOverrides, null, 2));
|
|
259
363
|
merged = this.mergeSettings(merged, cliOverrides);
|
|
@@ -285,6 +389,7 @@ Note: CLI overrides were applied. Check your --set arguments.`);
|
|
|
285
389
|
/**
|
|
286
390
|
* Load and parse a single settings file
|
|
287
391
|
* Returns empty object if file doesn't exist (not an error)
|
|
392
|
+
* Uses non-defaulting schema to prevent polluting partial settings with defaults before merge
|
|
288
393
|
*/
|
|
289
394
|
async loadSettingsFile(projectRoot, filename) {
|
|
290
395
|
const settingsPath = path.join(projectRoot, ".iloom", filename);
|
|
@@ -298,16 +403,13 @@ Note: CLI overrides were applied. Check your --set arguments.`);
|
|
|
298
403
|
`Failed to parse settings file at ${settingsPath}: ${error instanceof Error ? error.message : "Invalid JSON"}`
|
|
299
404
|
);
|
|
300
405
|
}
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
const errorMsg = this.formatAllZodErrors(error, filename);
|
|
307
|
-
throw errorMsg;
|
|
308
|
-
}
|
|
309
|
-
throw error;
|
|
406
|
+
if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) {
|
|
407
|
+
throw new Error(
|
|
408
|
+
`Settings validation failed at ${filename}:
|
|
409
|
+
- root: Expected object, received ${typeof parsed}`
|
|
410
|
+
);
|
|
310
411
|
}
|
|
412
|
+
return parsed;
|
|
311
413
|
} catch (error) {
|
|
312
414
|
if (error.code === "ENOENT") {
|
|
313
415
|
logger.debug(`No settings file found at ${settingsPath}, using defaults`);
|
|
@@ -361,6 +463,57 @@ ${errorMessages.join("\n")}`
|
|
|
361
463
|
getProjectRoot(projectRoot) {
|
|
362
464
|
return projectRoot ?? process.cwd();
|
|
363
465
|
}
|
|
466
|
+
/**
|
|
467
|
+
* Get global config directory path (~/.config/iloom-ai)
|
|
468
|
+
*/
|
|
469
|
+
getGlobalConfigDir() {
|
|
470
|
+
return path.join(os.homedir(), ".config", "iloom-ai");
|
|
471
|
+
}
|
|
472
|
+
/**
|
|
473
|
+
* Get global settings file path (~/.config/iloom-ai/settings.json)
|
|
474
|
+
*/
|
|
475
|
+
getGlobalSettingsPath() {
|
|
476
|
+
return path.join(this.getGlobalConfigDir(), "settings.json");
|
|
477
|
+
}
|
|
478
|
+
/**
|
|
479
|
+
* Load and parse global settings file
|
|
480
|
+
* Returns empty object if file doesn't exist (not an error)
|
|
481
|
+
* Warns but returns empty object on validation/parse errors (graceful degradation)
|
|
482
|
+
*/
|
|
483
|
+
async loadGlobalSettingsFile() {
|
|
484
|
+
const settingsPath = this.getGlobalSettingsPath();
|
|
485
|
+
try {
|
|
486
|
+
const content = await readFile(settingsPath, "utf-8");
|
|
487
|
+
let parsed;
|
|
488
|
+
try {
|
|
489
|
+
parsed = JSON.parse(content);
|
|
490
|
+
} catch (error) {
|
|
491
|
+
logger.warn(
|
|
492
|
+
`Failed to parse global settings file at ${settingsPath}: ${error instanceof Error ? error.message : "Invalid JSON"}. Ignoring global settings.`
|
|
493
|
+
);
|
|
494
|
+
return {};
|
|
495
|
+
}
|
|
496
|
+
try {
|
|
497
|
+
const validated = IloomSettingsSchemaNoDefaults.strict().parse(parsed);
|
|
498
|
+
return validated;
|
|
499
|
+
} catch (error) {
|
|
500
|
+
if (error instanceof z.ZodError) {
|
|
501
|
+
const errorMsg = this.formatAllZodErrors(error, "global settings");
|
|
502
|
+
logger.warn(`${errorMsg.message}. Ignoring global settings.`);
|
|
503
|
+
} else {
|
|
504
|
+
logger.warn(`Validation error in global settings: ${error instanceof Error ? error.message : "Unknown error"}. Ignoring global settings.`);
|
|
505
|
+
}
|
|
506
|
+
return {};
|
|
507
|
+
}
|
|
508
|
+
} catch (error) {
|
|
509
|
+
if (error.code === "ENOENT") {
|
|
510
|
+
logger.debug(`No global settings file found at ${settingsPath}`);
|
|
511
|
+
return {};
|
|
512
|
+
}
|
|
513
|
+
logger.warn(`Error reading global settings file at ${settingsPath}: ${error instanceof Error ? error.message : "Unknown error"}. Ignoring global settings.`);
|
|
514
|
+
return {};
|
|
515
|
+
}
|
|
516
|
+
}
|
|
364
517
|
/**
|
|
365
518
|
* Get effective protected branches list with mainBranch always included
|
|
366
519
|
*
|
|
@@ -396,7 +549,7 @@ __export(terminal_exports, {
|
|
|
396
549
|
openMultipleTerminalWindows: () => openMultipleTerminalWindows,
|
|
397
550
|
openTerminalWindow: () => openTerminalWindow
|
|
398
551
|
});
|
|
399
|
-
import { execa as
|
|
552
|
+
import { execa as execa3 } from "execa";
|
|
400
553
|
import { existsSync } from "fs";
|
|
401
554
|
function detectPlatform() {
|
|
402
555
|
const platform = process.platform;
|
|
@@ -420,9 +573,9 @@ async function openTerminalWindow(options) {
|
|
|
420
573
|
const hasITerm2 = await detectITerm2();
|
|
421
574
|
const applescript = hasITerm2 ? buildITerm2SingleTabScript(options) : buildAppleScript(options);
|
|
422
575
|
try {
|
|
423
|
-
await
|
|
576
|
+
await execa3("osascript", ["-e", applescript]);
|
|
424
577
|
if (!hasITerm2) {
|
|
425
|
-
await
|
|
578
|
+
await execa3("osascript", ["-e", 'tell application "Terminal" to activate']);
|
|
426
579
|
}
|
|
427
580
|
} catch (error) {
|
|
428
581
|
throw new Error(
|
|
@@ -590,7 +743,7 @@ async function openMultipleTerminalWindows(optionsArray) {
|
|
|
590
743
|
if (hasITerm2) {
|
|
591
744
|
const applescript = buildITerm2MultiTabScript(optionsArray);
|
|
592
745
|
try {
|
|
593
|
-
await
|
|
746
|
+
await execa3("osascript", ["-e", applescript]);
|
|
594
747
|
} catch (error) {
|
|
595
748
|
throw new Error(
|
|
596
749
|
`Failed to open iTerm2 window: ${error instanceof Error ? error.message : "Unknown error"}`
|
|
@@ -629,7 +782,7 @@ __export(color_exports, {
|
|
|
629
782
|
rgbToHex: () => rgbToHex,
|
|
630
783
|
saturateColor: () => saturateColor
|
|
631
784
|
});
|
|
632
|
-
import { createHash } from "crypto";
|
|
785
|
+
import { createHash as createHash2 } from "crypto";
|
|
633
786
|
function getColorPalette() {
|
|
634
787
|
return [
|
|
635
788
|
// First 10 colors preserved for backward compatibility
|
|
@@ -736,7 +889,7 @@ function hexToRgb(hex) {
|
|
|
736
889
|
return { r, g, b };
|
|
737
890
|
}
|
|
738
891
|
function generateColorFromBranchName(branchName) {
|
|
739
|
-
const hash =
|
|
892
|
+
const hash = createHash2("sha256").update(branchName).digest("hex");
|
|
740
893
|
const hashPrefix = hash.slice(0, 8);
|
|
741
894
|
const palette = getColorPalette();
|
|
742
895
|
const hashAsInt = parseInt(hashPrefix, 16);
|
|
@@ -788,388 +941,159 @@ var init_color = __esm({
|
|
|
788
941
|
}
|
|
789
942
|
});
|
|
790
943
|
|
|
791
|
-
// src/
|
|
792
|
-
var
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
import
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
await execa3("command", ["-v", "claude"], {
|
|
806
|
-
shell: true,
|
|
807
|
-
timeout: 5e3
|
|
808
|
-
});
|
|
809
|
-
return true;
|
|
810
|
-
} catch (error) {
|
|
811
|
-
logger.debug("Claude CLI not available", { error });
|
|
812
|
-
return false;
|
|
813
|
-
}
|
|
814
|
-
}
|
|
815
|
-
async function getClaudeVersion() {
|
|
944
|
+
// src/lib/WorkspaceManager.ts
|
|
945
|
+
var WorkspaceManager = class {
|
|
946
|
+
// TODO: Implement in Issue #6
|
|
947
|
+
};
|
|
948
|
+
|
|
949
|
+
// src/lib/GitWorktreeManager.ts
|
|
950
|
+
import path3 from "path";
|
|
951
|
+
import fs from "fs-extra";
|
|
952
|
+
|
|
953
|
+
// src/utils/git.ts
|
|
954
|
+
init_logger();
|
|
955
|
+
import path2 from "path";
|
|
956
|
+
import { execa } from "execa";
|
|
957
|
+
async function executeGitCommand(args, options) {
|
|
816
958
|
try {
|
|
817
|
-
const result = await
|
|
818
|
-
|
|
959
|
+
const result = await execa("git", args, {
|
|
960
|
+
cwd: (options == null ? void 0 : options.cwd) ?? process.cwd(),
|
|
961
|
+
timeout: (options == null ? void 0 : options.timeout) ?? 3e4,
|
|
962
|
+
encoding: "utf8",
|
|
963
|
+
stdio: (options == null ? void 0 : options.stdio) ?? "pipe",
|
|
964
|
+
verbose: logger.isDebugEnabled()
|
|
819
965
|
});
|
|
820
|
-
return result.stdout
|
|
966
|
+
return result.stdout;
|
|
821
967
|
} catch (error) {
|
|
822
|
-
|
|
823
|
-
|
|
968
|
+
const execaError = error;
|
|
969
|
+
const stderr = execaError.stderr ?? execaError.message ?? "Unknown Git error";
|
|
970
|
+
throw new Error(`Git command failed: ${stderr}`);
|
|
824
971
|
}
|
|
825
972
|
}
|
|
826
|
-
function
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
973
|
+
function parseWorktreeList(output, defaultBranch) {
|
|
974
|
+
var _a, _b;
|
|
975
|
+
const worktrees = [];
|
|
976
|
+
const lines = output.trim().split("\n");
|
|
977
|
+
let i = 0;
|
|
978
|
+
while (i < lines.length) {
|
|
979
|
+
const pathLine = lines[i];
|
|
980
|
+
if (!(pathLine == null ? void 0 : pathLine.startsWith("worktree "))) {
|
|
981
|
+
i++;
|
|
982
|
+
continue;
|
|
983
|
+
}
|
|
984
|
+
const pathMatch = pathLine.match(/^worktree (.+)$/);
|
|
985
|
+
if (!pathMatch) {
|
|
986
|
+
i++;
|
|
987
|
+
continue;
|
|
988
|
+
}
|
|
989
|
+
let branch = "";
|
|
990
|
+
let commit = "";
|
|
991
|
+
let detached = false;
|
|
992
|
+
let bare = false;
|
|
993
|
+
let locked = false;
|
|
994
|
+
let lockReason;
|
|
995
|
+
i++;
|
|
996
|
+
while (i < lines.length && !((_a = lines[i]) == null ? void 0 : _a.startsWith("worktree "))) {
|
|
997
|
+
const line = (_b = lines[i]) == null ? void 0 : _b.trim();
|
|
998
|
+
if (!line) {
|
|
999
|
+
i++;
|
|
837
1000
|
continue;
|
|
838
1001
|
}
|
|
1002
|
+
if (line === "bare") {
|
|
1003
|
+
bare = true;
|
|
1004
|
+
branch = defaultBranch ?? "main";
|
|
1005
|
+
} else if (line === "detached") {
|
|
1006
|
+
detached = true;
|
|
1007
|
+
branch = "HEAD";
|
|
1008
|
+
} else if (line.startsWith("locked")) {
|
|
1009
|
+
locked = true;
|
|
1010
|
+
const lockMatch = line.match(/^locked (.+)$/);
|
|
1011
|
+
lockReason = lockMatch == null ? void 0 : lockMatch[1];
|
|
1012
|
+
branch = branch || "unknown";
|
|
1013
|
+
} else if (line.startsWith("HEAD ")) {
|
|
1014
|
+
const commitMatch = line.match(/^HEAD ([a-f0-9]+)/);
|
|
1015
|
+
if (commitMatch) {
|
|
1016
|
+
commit = commitMatch[1] ?? "";
|
|
1017
|
+
}
|
|
1018
|
+
} else if (line.startsWith("branch ")) {
|
|
1019
|
+
const branchMatch = line.match(/^branch refs\/heads\/(.+)$/);
|
|
1020
|
+
branch = (branchMatch == null ? void 0 : branchMatch[1]) ?? line.replace("branch ", "");
|
|
1021
|
+
}
|
|
1022
|
+
i++;
|
|
839
1023
|
}
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
1024
|
+
const worktree = {
|
|
1025
|
+
path: pathMatch[1] ?? "",
|
|
1026
|
+
branch,
|
|
1027
|
+
commit,
|
|
1028
|
+
bare,
|
|
1029
|
+
detached,
|
|
1030
|
+
locked
|
|
1031
|
+
};
|
|
1032
|
+
if (lockReason !== void 0) {
|
|
1033
|
+
worktree.lockReason = lockReason;
|
|
1034
|
+
}
|
|
1035
|
+
worktrees.push(worktree);
|
|
843
1036
|
}
|
|
1037
|
+
return worktrees;
|
|
844
1038
|
}
|
|
845
|
-
|
|
846
|
-
const
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
1039
|
+
function isPRBranch(branchName) {
|
|
1040
|
+
const prPatterns = [
|
|
1041
|
+
/^pr\/\d+/i,
|
|
1042
|
+
// pr/123, pr/123-feature-name
|
|
1043
|
+
/^pull\/\d+/i,
|
|
1044
|
+
// pull/123
|
|
1045
|
+
/^\d+[-_]/,
|
|
1046
|
+
// 123-feature-name, 123_feature_name
|
|
1047
|
+
/^feature\/pr[-_]?\d+/i,
|
|
1048
|
+
// feature/pr123, feature/pr-123
|
|
1049
|
+
/^hotfix\/pr[-_]?\d+/i
|
|
1050
|
+
// hotfix/pr123
|
|
1051
|
+
];
|
|
1052
|
+
return prPatterns.some((pattern) => pattern.test(branchName));
|
|
1053
|
+
}
|
|
1054
|
+
function extractPRNumber(branchName) {
|
|
1055
|
+
const patterns = [
|
|
1056
|
+
/^pr\/(\d+)/i,
|
|
1057
|
+
// pr/123
|
|
1058
|
+
/^pull\/(\d+)/i,
|
|
1059
|
+
// pull/123
|
|
1060
|
+
/^(\d+)[-_]/,
|
|
1061
|
+
// 123-feature-name
|
|
1062
|
+
/^feature\/pr[-_]?(\d+)/i,
|
|
1063
|
+
// feature/pr123
|
|
1064
|
+
/^hotfix\/pr[-_]?(\d+)/i,
|
|
1065
|
+
// hotfix/pr123
|
|
1066
|
+
/pr[-_]?(\d+)/i
|
|
1067
|
+
// anywhere with pr123 or pr-123
|
|
1068
|
+
];
|
|
1069
|
+
for (const pattern of patterns) {
|
|
1070
|
+
const match = branchName.match(pattern);
|
|
1071
|
+
if (match == null ? void 0 : match[1]) {
|
|
1072
|
+
const num = parseInt(match[1], 10);
|
|
1073
|
+
if (!isNaN(num)) return num;
|
|
1074
|
+
}
|
|
852
1075
|
}
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
}
|
|
871
|
-
if (allowedTools && allowedTools.length > 0) {
|
|
872
|
-
args.push("--allowed-tools", ...allowedTools);
|
|
873
|
-
}
|
|
874
|
-
if (disallowedTools && disallowedTools.length > 0) {
|
|
875
|
-
args.push("--disallowed-tools", ...disallowedTools);
|
|
876
|
-
}
|
|
877
|
-
if (agents) {
|
|
878
|
-
args.push("--agents", JSON.stringify(agents));
|
|
879
|
-
}
|
|
880
|
-
try {
|
|
881
|
-
if (headless) {
|
|
882
|
-
const isDebugMode = logger.isDebugEnabled();
|
|
883
|
-
const execaOptions = {
|
|
884
|
-
input: prompt,
|
|
885
|
-
timeout: 0,
|
|
886
|
-
// Disable timeout for long responses
|
|
887
|
-
...addDir && { cwd: addDir },
|
|
888
|
-
// Run Claude in the worktree directory
|
|
889
|
-
verbose: isDebugMode,
|
|
890
|
-
...isDebugMode && { stdio: ["pipe", "pipe", "pipe"] }
|
|
891
|
-
// Enable streaming in debug mode
|
|
892
|
-
};
|
|
893
|
-
const subprocess = execa3("claude", args, execaOptions);
|
|
894
|
-
const isJsonStreamFormat = args.includes("--output-format") && args.includes("stream-json");
|
|
895
|
-
let outputBuffer = "";
|
|
896
|
-
let isStreaming = false;
|
|
897
|
-
let isFirstProgress = true;
|
|
898
|
-
if (subprocess.stdout && typeof subprocess.stdout.on === "function") {
|
|
899
|
-
isStreaming = true;
|
|
900
|
-
subprocess.stdout.on("data", (chunk) => {
|
|
901
|
-
const text = chunk.toString();
|
|
902
|
-
outputBuffer += text;
|
|
903
|
-
if (isDebugMode) {
|
|
904
|
-
process.stdout.write(text);
|
|
905
|
-
} else {
|
|
906
|
-
if (isFirstProgress) {
|
|
907
|
-
process.stdout.write("\u{1F916} .");
|
|
908
|
-
isFirstProgress = false;
|
|
909
|
-
} else {
|
|
910
|
-
process.stdout.write(".");
|
|
911
|
-
}
|
|
912
|
-
}
|
|
913
|
-
});
|
|
914
|
-
}
|
|
915
|
-
const result = await subprocess;
|
|
916
|
-
if (isStreaming) {
|
|
917
|
-
const rawOutput = outputBuffer.trim();
|
|
918
|
-
if (!isDebugMode) {
|
|
919
|
-
process.stdout.write("\n");
|
|
920
|
-
}
|
|
921
|
-
return isJsonStreamFormat ? parseJsonStreamOutput(rawOutput) : rawOutput;
|
|
922
|
-
} else {
|
|
923
|
-
if (isDebugMode) {
|
|
924
|
-
process.stdout.write(result.stdout);
|
|
925
|
-
if (result.stdout && !result.stdout.endsWith("\n")) {
|
|
926
|
-
process.stdout.write("\n");
|
|
927
|
-
}
|
|
928
|
-
} else {
|
|
929
|
-
process.stdout.write("\u{1F916} .");
|
|
930
|
-
process.stdout.write("\n");
|
|
931
|
-
}
|
|
932
|
-
const rawOutput = result.stdout.trim();
|
|
933
|
-
return isJsonStreamFormat ? parseJsonStreamOutput(rawOutput) : rawOutput;
|
|
934
|
-
}
|
|
935
|
-
} else {
|
|
936
|
-
await execa3("claude", [...args, "--", prompt], {
|
|
937
|
-
...addDir && { cwd: addDir },
|
|
938
|
-
stdio: "inherit",
|
|
939
|
-
// Let user interact directly in current terminal
|
|
940
|
-
timeout: 0,
|
|
941
|
-
// Disable timeout
|
|
942
|
-
verbose: logger.isDebugEnabled()
|
|
943
|
-
});
|
|
944
|
-
return;
|
|
945
|
-
}
|
|
946
|
-
} catch (error) {
|
|
947
|
-
const execaError = error;
|
|
948
|
-
const errorMessage = execaError.stderr ?? execaError.message ?? "Unknown Claude CLI error";
|
|
949
|
-
throw new Error(`Claude CLI error: ${errorMessage}`);
|
|
950
|
-
}
|
|
951
|
-
}
|
|
952
|
-
async function launchClaudeInNewTerminalWindow(_prompt, options) {
|
|
953
|
-
const { workspacePath, branchName, oneShot = "default", port, setArguments, executablePath } = options;
|
|
954
|
-
if (!workspacePath) {
|
|
955
|
-
throw new Error("workspacePath is required for terminal window launch");
|
|
956
|
-
}
|
|
957
|
-
const { openTerminalWindow: openTerminalWindow2 } = await Promise.resolve().then(() => (init_terminal(), terminal_exports));
|
|
958
|
-
const executable = executablePath ?? "iloom";
|
|
959
|
-
let launchCommand = `${executable} spin`;
|
|
960
|
-
if (oneShot !== "default") {
|
|
961
|
-
launchCommand += ` --one-shot=${oneShot}`;
|
|
962
|
-
}
|
|
963
|
-
if (setArguments && setArguments.length > 0) {
|
|
964
|
-
for (const setArg of setArguments) {
|
|
965
|
-
launchCommand += ` --set ${setArg}`;
|
|
966
|
-
}
|
|
967
|
-
}
|
|
968
|
-
let backgroundColor;
|
|
969
|
-
if (branchName) {
|
|
970
|
-
try {
|
|
971
|
-
const { generateColorFromBranchName: generateColorFromBranchName2 } = await Promise.resolve().then(() => (init_color(), color_exports));
|
|
972
|
-
const colorData = generateColorFromBranchName2(branchName);
|
|
973
|
-
backgroundColor = colorData.rgb;
|
|
974
|
-
} catch (error) {
|
|
975
|
-
logger.warn(
|
|
976
|
-
`Failed to generate terminal color: ${error instanceof Error ? error.message : "Unknown error"}`
|
|
977
|
-
);
|
|
978
|
-
}
|
|
979
|
-
}
|
|
980
|
-
const hasEnvFile = existsSync2(join(workspacePath, ".env"));
|
|
981
|
-
await openTerminalWindow2({
|
|
982
|
-
workspacePath,
|
|
983
|
-
command: launchCommand,
|
|
984
|
-
...backgroundColor && { backgroundColor },
|
|
985
|
-
includeEnvSetup: hasEnvFile,
|
|
986
|
-
// source .env only if it exists
|
|
987
|
-
...port !== void 0 && { port, includePortExport: true }
|
|
988
|
-
});
|
|
989
|
-
}
|
|
990
|
-
async function generateBranchName(issueTitle, issueNumber, model = "haiku") {
|
|
991
|
-
try {
|
|
992
|
-
const isAvailable = await detectClaudeCli();
|
|
993
|
-
if (!isAvailable) {
|
|
994
|
-
logger.warn("Claude CLI not available, using fallback branch name");
|
|
995
|
-
return `feat/issue-${issueNumber}`;
|
|
996
|
-
}
|
|
997
|
-
logger.debug("Generating branch name with Claude", { issueNumber, issueTitle });
|
|
998
|
-
const prompt = `<Task>
|
|
999
|
-
Generate a git branch name for the following issue:
|
|
1000
|
-
<Issue>
|
|
1001
|
-
<IssueNumber>${issueNumber}</IssueNumber>
|
|
1002
|
-
<IssueTitle>${issueTitle}</IssueTitle>
|
|
1003
|
-
</Issue>
|
|
1004
|
-
|
|
1005
|
-
<Requirements>
|
|
1006
|
-
<IssueNumber>Must use this exact issue number: ${issueNumber}</IssueNumber>
|
|
1007
|
-
<Format>Format must be: {prefix}/issue-${issueNumber}-{description}</Format>
|
|
1008
|
-
<Prefix>Prefix must be one of: feat, fix, docs, refactor, test, chore</Prefix>
|
|
1009
|
-
<MaxLength>Maximum 50 characters total</MaxLength>
|
|
1010
|
-
<Characters>Only lowercase letters, numbers, and hyphens allowed</Characters>
|
|
1011
|
-
<Output>Reply with ONLY the branch name, nothing else</Output>
|
|
1012
|
-
</Requirements>
|
|
1013
|
-
</Task>`;
|
|
1014
|
-
logger.debug("Sending prompt to Claude", { prompt });
|
|
1015
|
-
const result = await launchClaude(prompt, {
|
|
1016
|
-
model,
|
|
1017
|
-
headless: true
|
|
1018
|
-
});
|
|
1019
|
-
const branchName = result.trim();
|
|
1020
|
-
logger.debug("Claude returned branch name", { branchName, issueNumber });
|
|
1021
|
-
if (!branchName || !isValidBranchName(branchName, issueNumber)) {
|
|
1022
|
-
logger.warn("Invalid branch name from Claude, using fallback", { branchName });
|
|
1023
|
-
return `feat/issue-${issueNumber}`;
|
|
1024
|
-
}
|
|
1025
|
-
return branchName;
|
|
1026
|
-
} catch (error) {
|
|
1027
|
-
logger.warn("Failed to generate branch name with Claude", { error });
|
|
1028
|
-
return `feat/issue-${issueNumber}`;
|
|
1029
|
-
}
|
|
1030
|
-
}
|
|
1031
|
-
function isValidBranchName(name, issueNumber) {
|
|
1032
|
-
const pattern = new RegExp(`^(feat|fix|docs|refactor|test|chore)/issue-${issueNumber}-[a-z0-9-]+$`);
|
|
1033
|
-
return pattern.test(name) && name.length <= 50;
|
|
1034
|
-
}
|
|
1035
|
-
var init_claude = __esm({
|
|
1036
|
-
"src/utils/claude.ts"() {
|
|
1037
|
-
"use strict";
|
|
1038
|
-
init_logger();
|
|
1039
|
-
}
|
|
1040
|
-
});
|
|
1041
|
-
|
|
1042
|
-
// src/lib/WorkspaceManager.ts
|
|
1043
|
-
var WorkspaceManager = class {
|
|
1044
|
-
// TODO: Implement in Issue #6
|
|
1045
|
-
};
|
|
1046
|
-
|
|
1047
|
-
// src/lib/GitWorktreeManager.ts
|
|
1048
|
-
import path3 from "path";
|
|
1049
|
-
import fs from "fs-extra";
|
|
1050
|
-
|
|
1051
|
-
// src/utils/git.ts
|
|
1052
|
-
init_logger();
|
|
1053
|
-
import path2 from "path";
|
|
1054
|
-
import { execa } from "execa";
|
|
1055
|
-
async function executeGitCommand(args, options) {
|
|
1056
|
-
try {
|
|
1057
|
-
const result = await execa("git", args, {
|
|
1058
|
-
cwd: (options == null ? void 0 : options.cwd) ?? process.cwd(),
|
|
1059
|
-
timeout: (options == null ? void 0 : options.timeout) ?? 3e4,
|
|
1060
|
-
encoding: "utf8",
|
|
1061
|
-
stdio: (options == null ? void 0 : options.stdio) ?? "pipe",
|
|
1062
|
-
verbose: logger.isDebugEnabled()
|
|
1063
|
-
});
|
|
1064
|
-
return result.stdout;
|
|
1065
|
-
} catch (error) {
|
|
1066
|
-
const execaError = error;
|
|
1067
|
-
const stderr = execaError.stderr ?? execaError.message ?? "Unknown Git error";
|
|
1068
|
-
throw new Error(`Git command failed: ${stderr}`);
|
|
1069
|
-
}
|
|
1070
|
-
}
|
|
1071
|
-
function parseWorktreeList(output, defaultBranch) {
|
|
1072
|
-
var _a, _b;
|
|
1073
|
-
const worktrees = [];
|
|
1074
|
-
const lines = output.trim().split("\n");
|
|
1075
|
-
let i = 0;
|
|
1076
|
-
while (i < lines.length) {
|
|
1077
|
-
const pathLine = lines[i];
|
|
1078
|
-
if (!(pathLine == null ? void 0 : pathLine.startsWith("worktree "))) {
|
|
1079
|
-
i++;
|
|
1080
|
-
continue;
|
|
1081
|
-
}
|
|
1082
|
-
const pathMatch = pathLine.match(/^worktree (.+)$/);
|
|
1083
|
-
if (!pathMatch) {
|
|
1084
|
-
i++;
|
|
1085
|
-
continue;
|
|
1086
|
-
}
|
|
1087
|
-
let branch = "";
|
|
1088
|
-
let commit = "";
|
|
1089
|
-
let detached = false;
|
|
1090
|
-
let bare = false;
|
|
1091
|
-
let locked = false;
|
|
1092
|
-
let lockReason;
|
|
1093
|
-
i++;
|
|
1094
|
-
while (i < lines.length && !((_a = lines[i]) == null ? void 0 : _a.startsWith("worktree "))) {
|
|
1095
|
-
const line = (_b = lines[i]) == null ? void 0 : _b.trim();
|
|
1096
|
-
if (!line) {
|
|
1097
|
-
i++;
|
|
1098
|
-
continue;
|
|
1099
|
-
}
|
|
1100
|
-
if (line === "bare") {
|
|
1101
|
-
bare = true;
|
|
1102
|
-
branch = defaultBranch ?? "main";
|
|
1103
|
-
} else if (line === "detached") {
|
|
1104
|
-
detached = true;
|
|
1105
|
-
branch = "HEAD";
|
|
1106
|
-
} else if (line.startsWith("locked")) {
|
|
1107
|
-
locked = true;
|
|
1108
|
-
const lockMatch = line.match(/^locked (.+)$/);
|
|
1109
|
-
lockReason = lockMatch == null ? void 0 : lockMatch[1];
|
|
1110
|
-
branch = branch || "unknown";
|
|
1111
|
-
} else if (line.startsWith("HEAD ")) {
|
|
1112
|
-
const commitMatch = line.match(/^HEAD ([a-f0-9]+)/);
|
|
1113
|
-
if (commitMatch) {
|
|
1114
|
-
commit = commitMatch[1] ?? "";
|
|
1115
|
-
}
|
|
1116
|
-
} else if (line.startsWith("branch ")) {
|
|
1117
|
-
const branchMatch = line.match(/^branch refs\/heads\/(.+)$/);
|
|
1118
|
-
branch = (branchMatch == null ? void 0 : branchMatch[1]) ?? line.replace("branch ", "");
|
|
1119
|
-
}
|
|
1120
|
-
i++;
|
|
1121
|
-
}
|
|
1122
|
-
const worktree = {
|
|
1123
|
-
path: pathMatch[1] ?? "",
|
|
1124
|
-
branch,
|
|
1125
|
-
commit,
|
|
1126
|
-
bare,
|
|
1127
|
-
detached,
|
|
1128
|
-
locked
|
|
1129
|
-
};
|
|
1130
|
-
if (lockReason !== void 0) {
|
|
1131
|
-
worktree.lockReason = lockReason;
|
|
1132
|
-
}
|
|
1133
|
-
worktrees.push(worktree);
|
|
1134
|
-
}
|
|
1135
|
-
return worktrees;
|
|
1136
|
-
}
|
|
1137
|
-
function isPRBranch(branchName) {
|
|
1138
|
-
const prPatterns = [
|
|
1139
|
-
/^pr\/\d+/i,
|
|
1140
|
-
// pr/123, pr/123-feature-name
|
|
1141
|
-
/^pull\/\d+/i,
|
|
1142
|
-
// pull/123
|
|
1143
|
-
/^\d+[-_]/,
|
|
1144
|
-
// 123-feature-name, 123_feature_name
|
|
1145
|
-
/^feature\/pr[-_]?\d+/i,
|
|
1146
|
-
// feature/pr123, feature/pr-123
|
|
1147
|
-
/^hotfix\/pr[-_]?\d+/i
|
|
1148
|
-
// hotfix/pr123
|
|
1149
|
-
];
|
|
1150
|
-
return prPatterns.some((pattern) => pattern.test(branchName));
|
|
1151
|
-
}
|
|
1152
|
-
function extractPRNumber(branchName) {
|
|
1153
|
-
const patterns = [
|
|
1154
|
-
/^pr\/(\d+)/i,
|
|
1155
|
-
// pr/123
|
|
1156
|
-
/^pull\/(\d+)/i,
|
|
1157
|
-
// pull/123
|
|
1158
|
-
/^(\d+)[-_]/,
|
|
1159
|
-
// 123-feature-name
|
|
1160
|
-
/^feature\/pr[-_]?(\d+)/i,
|
|
1161
|
-
// feature/pr123
|
|
1162
|
-
/^hotfix\/pr[-_]?(\d+)/i,
|
|
1163
|
-
// hotfix/pr123
|
|
1164
|
-
/pr[-_]?(\d+)/i
|
|
1165
|
-
// anywhere with pr123 or pr-123
|
|
1076
|
+
return null;
|
|
1077
|
+
}
|
|
1078
|
+
function extractIssueNumber(branchName) {
|
|
1079
|
+
const newFormatPattern = /issue-([^_]+)__/i;
|
|
1080
|
+
const newMatch = branchName.match(newFormatPattern);
|
|
1081
|
+
if (newMatch == null ? void 0 : newMatch[1]) return newMatch[1];
|
|
1082
|
+
const oldFormatPattern = /issue-(\d+)(?:-|$)/i;
|
|
1083
|
+
const oldMatch = branchName.match(oldFormatPattern);
|
|
1084
|
+
if (oldMatch == null ? void 0 : oldMatch[1]) return oldMatch[1];
|
|
1085
|
+
const alphanumericEndPattern = /issue-([^_\s/]+)$/i;
|
|
1086
|
+
const alphanumericMatch = branchName.match(alphanumericEndPattern);
|
|
1087
|
+
if (alphanumericMatch == null ? void 0 : alphanumericMatch[1]) return alphanumericMatch[1];
|
|
1088
|
+
const legacyPatterns = [
|
|
1089
|
+
/issue_(\d+)/i,
|
|
1090
|
+
// issue_42
|
|
1091
|
+
/^(\d+)-/
|
|
1092
|
+
// 42-feature-name
|
|
1166
1093
|
];
|
|
1167
|
-
for (const pattern of
|
|
1094
|
+
for (const pattern of legacyPatterns) {
|
|
1168
1095
|
const match = branchName.match(pattern);
|
|
1169
|
-
if (match == null ? void 0 : match[1])
|
|
1170
|
-
const num = parseInt(match[1], 10);
|
|
1171
|
-
if (!isNaN(num)) return num;
|
|
1172
|
-
}
|
|
1096
|
+
if (match == null ? void 0 : match[1]) return match[1];
|
|
1173
1097
|
}
|
|
1174
1098
|
return null;
|
|
1175
1099
|
}
|
|
@@ -1766,7 +1690,7 @@ var GitWorktreeManager = class {
|
|
|
1766
1690
|
*/
|
|
1767
1691
|
async findWorktreeForIssue(issueNumber) {
|
|
1768
1692
|
const worktrees = await this.listWorktrees({ porcelain: true });
|
|
1769
|
-
const pattern = new RegExp(`(?:^|[/_-])issue-${issueNumber}(
|
|
1693
|
+
const pattern = new RegExp(`(?:^|[/_-])issue-${issueNumber}(?:-|__|$)`, "i");
|
|
1770
1694
|
return worktrees.find((wt) => pattern.test(wt.branch)) ?? null;
|
|
1771
1695
|
}
|
|
1772
1696
|
/**
|
|
@@ -1840,9 +1764,9 @@ var GitHubError = class extends Error {
|
|
|
1840
1764
|
|
|
1841
1765
|
// src/utils/github.ts
|
|
1842
1766
|
init_logger();
|
|
1843
|
-
import { execa as
|
|
1767
|
+
import { execa as execa2 } from "execa";
|
|
1844
1768
|
async function executeGhCommand(args, options) {
|
|
1845
|
-
const result = await
|
|
1769
|
+
const result = await execa2("gh", args, {
|
|
1846
1770
|
cwd: (options == null ? void 0 : options.cwd) ?? process.cwd(),
|
|
1847
1771
|
timeout: (options == null ? void 0 : options.timeout) ?? 3e4,
|
|
1848
1772
|
encoding: "utf8"
|
|
@@ -1984,21 +1908,6 @@ async function updateProjectItemField(itemId, projectId, fieldId, optionId) {
|
|
|
1984
1908
|
"json"
|
|
1985
1909
|
]);
|
|
1986
1910
|
}
|
|
1987
|
-
var SimpleBranchNameStrategy = class {
|
|
1988
|
-
async generate(issueNumber, title) {
|
|
1989
|
-
const slug = title.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "").substring(0, 20);
|
|
1990
|
-
return `feat/issue-${issueNumber}-${slug}`;
|
|
1991
|
-
}
|
|
1992
|
-
};
|
|
1993
|
-
var ClaudeBranchNameStrategy = class {
|
|
1994
|
-
constructor(claudeModel = "haiku") {
|
|
1995
|
-
this.claudeModel = claudeModel;
|
|
1996
|
-
}
|
|
1997
|
-
async generate(issueNumber, title) {
|
|
1998
|
-
const { generateBranchName: generateBranchName2 } = await Promise.resolve().then(() => (init_claude(), claude_exports));
|
|
1999
|
-
return generateBranchName2(title, issueNumber, this.claudeModel);
|
|
2000
|
-
}
|
|
2001
|
-
};
|
|
2002
1911
|
async function createIssue(title, body, options) {
|
|
2003
1912
|
const { repo, labels } = options ?? {};
|
|
2004
1913
|
logger.debug("Creating GitHub issue", { title, repo, labels });
|
|
@@ -2023,7 +1932,7 @@ async function createIssue(title, body, options) {
|
|
|
2023
1932
|
if (!repo) {
|
|
2024
1933
|
execaOptions.cwd = process.cwd();
|
|
2025
1934
|
}
|
|
2026
|
-
const result = await
|
|
1935
|
+
const result = await execa2("gh", args, execaOptions);
|
|
2027
1936
|
const urlMatch = result.stdout.trim().match(/https:\/\/github\.com\/[^/]+\/[^/]+\/issues\/(\d+)/);
|
|
2028
1937
|
if (!(urlMatch == null ? void 0 : urlMatch[1])) {
|
|
2029
1938
|
throw new Error(`Failed to parse issue URL from gh output: ${result.stdout}`);
|
|
@@ -2077,35 +1986,29 @@ async function promptConfirmation(message, defaultValue = false) {
|
|
|
2077
1986
|
// src/lib/GitHubService.ts
|
|
2078
1987
|
var GitHubService = class {
|
|
2079
1988
|
constructor(options) {
|
|
2080
|
-
|
|
2081
|
-
|
|
2082
|
-
|
|
2083
|
-
this.defaultBranchNameStrategy = new ClaudeBranchNameStrategy(
|
|
2084
|
-
options == null ? void 0 : options.claudeModel
|
|
2085
|
-
);
|
|
2086
|
-
} else {
|
|
2087
|
-
this.defaultBranchNameStrategy = new SimpleBranchNameStrategy();
|
|
2088
|
-
}
|
|
1989
|
+
// IssueTracker interface implementation
|
|
1990
|
+
this.providerName = "github";
|
|
1991
|
+
this.supportsPullRequests = true;
|
|
2089
1992
|
this.prompter = (options == null ? void 0 : options.prompter) ?? promptConfirmation;
|
|
2090
1993
|
}
|
|
2091
|
-
// Input detection
|
|
1994
|
+
// Input detection - IssueTracker interface implementation
|
|
2092
1995
|
async detectInputType(input, repo) {
|
|
2093
1996
|
const numberMatch = input.match(/^#?(\d+)$/);
|
|
2094
1997
|
if (!(numberMatch == null ? void 0 : numberMatch[1])) {
|
|
2095
|
-
return { type: "unknown",
|
|
1998
|
+
return { type: "unknown", identifier: null, rawInput: input };
|
|
2096
1999
|
}
|
|
2097
2000
|
const number = parseInt(numberMatch[1], 10);
|
|
2098
2001
|
logger.debug("Checking if input is a PR", { number });
|
|
2099
2002
|
const pr = await this.isValidPR(number, repo);
|
|
2100
2003
|
if (pr) {
|
|
2101
|
-
return { type: "pr", number, rawInput: input };
|
|
2004
|
+
return { type: "pr", identifier: number.toString(), rawInput: input };
|
|
2102
2005
|
}
|
|
2103
2006
|
logger.debug("Checking if input is an issue", { number });
|
|
2104
2007
|
const issue = await this.isValidIssue(number, repo);
|
|
2105
2008
|
if (issue) {
|
|
2106
|
-
return { type: "issue", number, rawInput: input };
|
|
2009
|
+
return { type: "issue", identifier: number.toString(), rawInput: input };
|
|
2107
2010
|
}
|
|
2108
|
-
return { type: "unknown",
|
|
2011
|
+
return { type: "unknown", identifier: null, rawInput: input };
|
|
2109
2012
|
}
|
|
2110
2013
|
// Issue fetching with validation
|
|
2111
2014
|
async fetchIssue(issueNumber, repo) {
|
|
@@ -2199,17 +2102,6 @@ var GitHubService = class {
|
|
|
2199
2102
|
}
|
|
2200
2103
|
}
|
|
2201
2104
|
}
|
|
2202
|
-
// Branch name generation using strategy pattern
|
|
2203
|
-
async generateBranchName(options) {
|
|
2204
|
-
const { issueNumber, title, strategy } = options;
|
|
2205
|
-
const nameStrategy = strategy ?? this.defaultBranchNameStrategy;
|
|
2206
|
-
logger.debug("Generating branch name", {
|
|
2207
|
-
issueNumber,
|
|
2208
|
-
title,
|
|
2209
|
-
strategy: nameStrategy.constructor.name
|
|
2210
|
-
});
|
|
2211
|
-
return nameStrategy.generate(issueNumber, title);
|
|
2212
|
-
}
|
|
2213
2105
|
// Issue creation
|
|
2214
2106
|
async createIssue(title, body, repository, labels) {
|
|
2215
2107
|
return createIssue(title, body, { repo: repository, labels });
|
|
@@ -2344,13 +2236,372 @@ State: ${entity.state}`;
|
|
|
2344
2236
|
async promptUserConfirmation(message) {
|
|
2345
2237
|
return this.prompter(message);
|
|
2346
2238
|
}
|
|
2347
|
-
|
|
2348
|
-
|
|
2349
|
-
|
|
2239
|
+
};
|
|
2240
|
+
|
|
2241
|
+
// src/types/linear.ts
|
|
2242
|
+
var LinearServiceError = class _LinearServiceError extends Error {
|
|
2243
|
+
constructor(code, message, details) {
|
|
2244
|
+
super(message);
|
|
2245
|
+
this.code = code;
|
|
2246
|
+
this.details = details;
|
|
2247
|
+
this.name = "LinearServiceError";
|
|
2248
|
+
if (Error.captureStackTrace) {
|
|
2249
|
+
Error.captureStackTrace(this, _LinearServiceError);
|
|
2250
|
+
}
|
|
2251
|
+
}
|
|
2252
|
+
};
|
|
2253
|
+
|
|
2254
|
+
// src/utils/linear.ts
|
|
2255
|
+
import { LinearClient } from "@linear/sdk";
|
|
2256
|
+
init_logger();
|
|
2257
|
+
function slugifyTitle(title, maxLength = 50) {
|
|
2258
|
+
const slug = title.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "");
|
|
2259
|
+
if (slug.length <= maxLength) {
|
|
2260
|
+
return slug;
|
|
2261
|
+
}
|
|
2262
|
+
const parts = slug.split("-");
|
|
2263
|
+
let result = "";
|
|
2264
|
+
for (const part of parts) {
|
|
2265
|
+
const candidate = result ? `${result}-${part}` : part;
|
|
2266
|
+
if (candidate.length > maxLength) {
|
|
2267
|
+
break;
|
|
2268
|
+
}
|
|
2269
|
+
result = candidate;
|
|
2270
|
+
}
|
|
2271
|
+
return result || slug.slice(0, maxLength);
|
|
2272
|
+
}
|
|
2273
|
+
function buildLinearIssueUrl(identifier, title) {
|
|
2274
|
+
const base = `https://linear.app/issue/${identifier}`;
|
|
2275
|
+
if (title) {
|
|
2276
|
+
const slug = slugifyTitle(title);
|
|
2277
|
+
return slug ? `${base}/${slug}` : base;
|
|
2278
|
+
}
|
|
2279
|
+
return base;
|
|
2280
|
+
}
|
|
2281
|
+
function getLinearApiToken() {
|
|
2282
|
+
const token = process.env.LINEAR_API_TOKEN;
|
|
2283
|
+
if (!token) {
|
|
2284
|
+
throw new LinearServiceError(
|
|
2285
|
+
"UNAUTHORIZED",
|
|
2286
|
+
"LINEAR_API_TOKEN not set. Configure in settings.local.json or set environment variable."
|
|
2287
|
+
);
|
|
2288
|
+
}
|
|
2289
|
+
return token;
|
|
2290
|
+
}
|
|
2291
|
+
function createLinearClient() {
|
|
2292
|
+
return new LinearClient({ apiKey: getLinearApiToken() });
|
|
2293
|
+
}
|
|
2294
|
+
function handleLinearError(error, context) {
|
|
2295
|
+
logger.debug(`${context}: Handling error`, { error });
|
|
2296
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
2297
|
+
if (errorMessage.includes("not found") || errorMessage.includes("Not found")) {
|
|
2298
|
+
throw new LinearServiceError("NOT_FOUND", "Linear issue or resource not found", { error });
|
|
2299
|
+
}
|
|
2300
|
+
if (errorMessage.includes("unauthorized") || errorMessage.includes("Unauthorized") || errorMessage.includes("Invalid API key")) {
|
|
2301
|
+
throw new LinearServiceError(
|
|
2302
|
+
"UNAUTHORIZED",
|
|
2303
|
+
"Linear authentication failed. Check LINEAR_API_TOKEN.",
|
|
2304
|
+
{ error }
|
|
2305
|
+
);
|
|
2306
|
+
}
|
|
2307
|
+
if (errorMessage.includes("rate limit")) {
|
|
2308
|
+
throw new LinearServiceError("RATE_LIMITED", "Linear API rate limit exceeded", { error });
|
|
2309
|
+
}
|
|
2310
|
+
throw new LinearServiceError("CLI_ERROR", `Linear SDK error: ${errorMessage}`, { error });
|
|
2311
|
+
}
|
|
2312
|
+
async function fetchLinearIssue(identifier) {
|
|
2313
|
+
try {
|
|
2314
|
+
logger.debug(`Fetching Linear issue: ${identifier}`);
|
|
2315
|
+
const client = createLinearClient();
|
|
2316
|
+
const issue = await client.issue(identifier);
|
|
2317
|
+
if (!issue) {
|
|
2318
|
+
throw new LinearServiceError("NOT_FOUND", `Linear issue ${identifier} not found`);
|
|
2319
|
+
}
|
|
2320
|
+
const result = {
|
|
2321
|
+
id: issue.id,
|
|
2322
|
+
identifier: issue.identifier,
|
|
2323
|
+
title: issue.title,
|
|
2324
|
+
url: issue.url,
|
|
2325
|
+
createdAt: issue.createdAt.toISOString(),
|
|
2326
|
+
updatedAt: issue.updatedAt.toISOString()
|
|
2327
|
+
};
|
|
2328
|
+
if (issue.description) {
|
|
2329
|
+
result.description = issue.description;
|
|
2330
|
+
}
|
|
2331
|
+
if (issue.state) {
|
|
2332
|
+
const state = await issue.state;
|
|
2333
|
+
if (state == null ? void 0 : state.name) {
|
|
2334
|
+
result.state = state.name;
|
|
2335
|
+
}
|
|
2336
|
+
}
|
|
2337
|
+
return result;
|
|
2338
|
+
} catch (error) {
|
|
2339
|
+
if (error instanceof LinearServiceError) {
|
|
2340
|
+
throw error;
|
|
2341
|
+
}
|
|
2342
|
+
handleLinearError(error, "fetchLinearIssue");
|
|
2343
|
+
}
|
|
2344
|
+
}
|
|
2345
|
+
async function createLinearIssue(title, body, teamKey, _labels) {
|
|
2346
|
+
try {
|
|
2347
|
+
logger.debug(`Creating Linear issue in team ${teamKey}: ${title}`);
|
|
2348
|
+
const client = createLinearClient();
|
|
2349
|
+
const teams = await client.teams();
|
|
2350
|
+
const team = teams.nodes.find((t) => t.key === teamKey);
|
|
2351
|
+
if (!team) {
|
|
2352
|
+
throw new LinearServiceError("NOT_FOUND", `Linear team ${teamKey} not found`);
|
|
2353
|
+
}
|
|
2354
|
+
const issueInput = {
|
|
2355
|
+
teamId: team.id,
|
|
2356
|
+
title
|
|
2357
|
+
};
|
|
2358
|
+
if (body) {
|
|
2359
|
+
issueInput.description = body;
|
|
2360
|
+
}
|
|
2361
|
+
const payload = await client.createIssue(issueInput);
|
|
2362
|
+
const issue = await payload.issue;
|
|
2363
|
+
if (!issue) {
|
|
2364
|
+
throw new LinearServiceError("CLI_ERROR", "Failed to create Linear issue");
|
|
2365
|
+
}
|
|
2366
|
+
const url = issue.url ?? buildLinearIssueUrl(issue.identifier, title);
|
|
2367
|
+
return {
|
|
2368
|
+
identifier: issue.identifier,
|
|
2369
|
+
url
|
|
2370
|
+
};
|
|
2371
|
+
} catch (error) {
|
|
2372
|
+
if (error instanceof LinearServiceError) {
|
|
2373
|
+
throw error;
|
|
2374
|
+
}
|
|
2375
|
+
handleLinearError(error, "createLinearIssue");
|
|
2376
|
+
}
|
|
2377
|
+
}
|
|
2378
|
+
async function updateLinearIssueState(identifier, stateName) {
|
|
2379
|
+
try {
|
|
2380
|
+
logger.debug(`Updating Linear issue ${identifier} state to: ${stateName}`);
|
|
2381
|
+
const client = createLinearClient();
|
|
2382
|
+
const issue = await client.issue(identifier);
|
|
2383
|
+
if (!issue) {
|
|
2384
|
+
throw new LinearServiceError("NOT_FOUND", `Linear issue ${identifier} not found`);
|
|
2385
|
+
}
|
|
2386
|
+
const team = await issue.team;
|
|
2387
|
+
if (!team) {
|
|
2388
|
+
throw new LinearServiceError("CLI_ERROR", "Issue has no team");
|
|
2389
|
+
}
|
|
2390
|
+
const states = await team.states();
|
|
2391
|
+
const state = states.nodes.find((s) => s.name === stateName);
|
|
2392
|
+
if (!state) {
|
|
2393
|
+
throw new LinearServiceError(
|
|
2394
|
+
"NOT_FOUND",
|
|
2395
|
+
`State "${stateName}" not found in team ${team.key}`
|
|
2396
|
+
);
|
|
2397
|
+
}
|
|
2398
|
+
await client.updateIssue(issue.id, {
|
|
2399
|
+
stateId: state.id
|
|
2400
|
+
});
|
|
2401
|
+
} catch (error) {
|
|
2402
|
+
if (error instanceof LinearServiceError) {
|
|
2403
|
+
throw error;
|
|
2404
|
+
}
|
|
2405
|
+
handleLinearError(error, "updateLinearIssueState");
|
|
2406
|
+
}
|
|
2407
|
+
}
|
|
2408
|
+
|
|
2409
|
+
// src/lib/LinearService.ts
|
|
2410
|
+
init_logger();
|
|
2411
|
+
var LinearService = class {
|
|
2412
|
+
constructor(config, options) {
|
|
2413
|
+
// IssueTracker interface implementation
|
|
2414
|
+
this.providerName = "linear";
|
|
2415
|
+
this.supportsPullRequests = false;
|
|
2416
|
+
this.config = config ?? {};
|
|
2417
|
+
this.prompter = (options == null ? void 0 : options.prompter) ?? promptConfirmation;
|
|
2418
|
+
}
|
|
2419
|
+
/**
|
|
2420
|
+
* Detect if input matches Linear identifier format (TEAM-NUMBER)
|
|
2421
|
+
* @param input - User input string
|
|
2422
|
+
* @param _repo - Repository (unused for Linear)
|
|
2423
|
+
* @returns Detection result with type and identifier
|
|
2424
|
+
*/
|
|
2425
|
+
async detectInputType(input, _repo) {
|
|
2426
|
+
logger.debug(`LinearService.detectInputType called with input: "${input}"`);
|
|
2427
|
+
const linearPattern = /^([A-Z]{2,}-\d+)$/i;
|
|
2428
|
+
const match = input.match(linearPattern);
|
|
2429
|
+
if (!(match == null ? void 0 : match[1])) {
|
|
2430
|
+
logger.debug(`LinearService: Input "${input}" does not match Linear pattern`);
|
|
2431
|
+
return { type: "unknown", identifier: null, rawInput: input };
|
|
2432
|
+
}
|
|
2433
|
+
const identifier = match[1].toUpperCase();
|
|
2434
|
+
logger.debug(`LinearService: Matched Linear identifier: ${identifier}`);
|
|
2435
|
+
logger.debug(`LinearService: Checking if ${identifier} is a valid Linear issue via SDK`);
|
|
2436
|
+
const issue = await this.isValidIssue(identifier);
|
|
2437
|
+
if (issue) {
|
|
2438
|
+
logger.debug(`LinearService: Issue ${identifier} found: "${issue.title}"`);
|
|
2439
|
+
return { type: "issue", identifier, rawInput: input };
|
|
2440
|
+
}
|
|
2441
|
+
logger.debug(`LinearService: Issue ${identifier} NOT found by SDK`);
|
|
2442
|
+
return { type: "unknown", identifier: null, rawInput: input };
|
|
2443
|
+
}
|
|
2444
|
+
/**
|
|
2445
|
+
* Fetch a Linear issue by identifier
|
|
2446
|
+
* @param identifier - Linear issue identifier (string or number)
|
|
2447
|
+
* @param _repo - Repository (unused for Linear)
|
|
2448
|
+
* @returns Generic Issue type
|
|
2449
|
+
* @throws LinearServiceError if issue not found
|
|
2450
|
+
*/
|
|
2451
|
+
async fetchIssue(identifier, _repo) {
|
|
2452
|
+
const linearIssue = await fetchLinearIssue(String(identifier));
|
|
2453
|
+
return this.mapLinearIssueToIssue(linearIssue);
|
|
2454
|
+
}
|
|
2455
|
+
/**
|
|
2456
|
+
* Check if an issue identifier is valid (silent validation)
|
|
2457
|
+
* @param identifier - Linear issue identifier
|
|
2458
|
+
* @param _repo - Repository (unused for Linear)
|
|
2459
|
+
* @returns Issue if valid, false if not found
|
|
2460
|
+
*/
|
|
2461
|
+
async isValidIssue(identifier, _repo) {
|
|
2462
|
+
try {
|
|
2463
|
+
return await this.fetchIssue(identifier);
|
|
2464
|
+
} catch (error) {
|
|
2465
|
+
if (error instanceof LinearServiceError && error.code === "NOT_FOUND") {
|
|
2466
|
+
return false;
|
|
2467
|
+
}
|
|
2468
|
+
throw error;
|
|
2469
|
+
}
|
|
2470
|
+
}
|
|
2471
|
+
/**
|
|
2472
|
+
* Validate issue state and prompt user if closed
|
|
2473
|
+
* @param issue - Issue to validate
|
|
2474
|
+
* @throws LinearServiceError if user cancels due to closed issue
|
|
2475
|
+
*/
|
|
2476
|
+
async validateIssueState(issue) {
|
|
2477
|
+
if (issue.state === "closed") {
|
|
2478
|
+
const shouldContinue = await this.prompter(
|
|
2479
|
+
`Issue ${issue.number} is closed. Continue anyway?`
|
|
2480
|
+
);
|
|
2481
|
+
if (!shouldContinue) {
|
|
2482
|
+
throw new LinearServiceError("INVALID_STATE", "User cancelled due to closed issue");
|
|
2483
|
+
}
|
|
2484
|
+
}
|
|
2485
|
+
}
|
|
2486
|
+
/**
|
|
2487
|
+
* Create a new Linear issue
|
|
2488
|
+
* @param title - Issue title
|
|
2489
|
+
* @param body - Issue description (markdown)
|
|
2490
|
+
* @param _repository - Repository (unused for Linear)
|
|
2491
|
+
* @param labels - Optional label names
|
|
2492
|
+
* @returns Created issue identifier and URL
|
|
2493
|
+
* @throws LinearServiceError if teamId not configured or creation fails
|
|
2494
|
+
*/
|
|
2495
|
+
async createIssue(title, body, _repository, labels) {
|
|
2496
|
+
if (!this.config.teamId) {
|
|
2497
|
+
throw new LinearServiceError(
|
|
2498
|
+
"INVALID_STATE",
|
|
2499
|
+
"Linear teamId not configured. Run `il init` to configure Linear settings."
|
|
2500
|
+
);
|
|
2501
|
+
}
|
|
2502
|
+
logger.info(`Creating Linear issue in team ${this.config.teamId}: ${title}`);
|
|
2503
|
+
const result = await createLinearIssue(title, body, this.config.teamId, labels);
|
|
2504
|
+
return {
|
|
2505
|
+
number: result.identifier,
|
|
2506
|
+
url: result.url
|
|
2507
|
+
};
|
|
2508
|
+
}
|
|
2509
|
+
/**
|
|
2510
|
+
* Get the web URL for a Linear issue
|
|
2511
|
+
* @param identifier - Linear issue identifier
|
|
2512
|
+
* @param _repo - Repository (unused for Linear)
|
|
2513
|
+
* @returns Issue URL
|
|
2514
|
+
*/
|
|
2515
|
+
async getIssueUrl(identifier, _repo) {
|
|
2516
|
+
const issue = await this.fetchIssue(identifier);
|
|
2517
|
+
return issue.url;
|
|
2518
|
+
}
|
|
2519
|
+
/**
|
|
2520
|
+
* Move a Linear issue to "In Progress" state
|
|
2521
|
+
* @param identifier - Linear issue identifier
|
|
2522
|
+
* @throws LinearServiceError if state update fails
|
|
2523
|
+
*/
|
|
2524
|
+
async moveIssueToInProgress(identifier) {
|
|
2525
|
+
logger.info(`Moving Linear issue ${identifier} to In Progress`);
|
|
2526
|
+
await updateLinearIssueState(String(identifier), "In Progress");
|
|
2527
|
+
}
|
|
2528
|
+
/**
|
|
2529
|
+
* Extract issue context for AI prompts
|
|
2530
|
+
* @param entity - Issue (Linear doesn't have PRs)
|
|
2531
|
+
* @returns Formatted context string
|
|
2532
|
+
*/
|
|
2533
|
+
extractContext(entity) {
|
|
2534
|
+
const issue = entity;
|
|
2535
|
+
return `Linear Issue ${issue.number}: ${issue.title}
|
|
2536
|
+
State: ${issue.state}
|
|
2537
|
+
|
|
2538
|
+
${issue.body}`;
|
|
2539
|
+
}
|
|
2540
|
+
/**
|
|
2541
|
+
* Map Linear API issue to generic Issue type
|
|
2542
|
+
* @param linear - Linear issue from SDK
|
|
2543
|
+
* @returns Generic Issue type
|
|
2544
|
+
*/
|
|
2545
|
+
mapLinearIssueToIssue(linear) {
|
|
2546
|
+
return {
|
|
2547
|
+
number: linear.identifier,
|
|
2548
|
+
// Keep as string (e.g., "ENG-123")
|
|
2549
|
+
title: linear.title,
|
|
2550
|
+
body: linear.description ?? "",
|
|
2551
|
+
state: linear.state ? linear.state.toLowerCase().includes("done") || linear.state.toLowerCase().includes("completed") || linear.state.toLowerCase().includes("canceled") ? "closed" : "open" : "open",
|
|
2552
|
+
labels: [],
|
|
2553
|
+
assignees: [],
|
|
2554
|
+
url: linear.url
|
|
2555
|
+
};
|
|
2556
|
+
}
|
|
2557
|
+
};
|
|
2558
|
+
|
|
2559
|
+
// src/lib/IssueTrackerFactory.ts
|
|
2560
|
+
init_logger();
|
|
2561
|
+
var IssueTrackerFactory = class {
|
|
2562
|
+
/**
|
|
2563
|
+
* Create an IssueTracker instance based on settings configuration
|
|
2564
|
+
* Defaults to GitHub if no provider specified
|
|
2565
|
+
*
|
|
2566
|
+
* @param settings - iloom settings containing issueManagement.provider
|
|
2567
|
+
* @returns IssueTracker instance configured for the specified provider
|
|
2568
|
+
* @throws Error if provider type is not supported
|
|
2569
|
+
*/
|
|
2570
|
+
static create(settings) {
|
|
2571
|
+
var _a, _b;
|
|
2572
|
+
const provider = ((_a = settings.issueManagement) == null ? void 0 : _a.provider) ?? "github";
|
|
2573
|
+
logger.debug(`IssueTrackerFactory: Creating tracker for provider "${provider}"`);
|
|
2574
|
+
logger.debug(`IssueTrackerFactory: issueManagement settings:`, JSON.stringify(settings.issueManagement, null, 2));
|
|
2575
|
+
switch (provider) {
|
|
2576
|
+
case "github":
|
|
2577
|
+
logger.debug("IssueTrackerFactory: Creating GitHubService");
|
|
2578
|
+
return new GitHubService();
|
|
2579
|
+
case "linear": {
|
|
2580
|
+
const linearSettings = (_b = settings.issueManagement) == null ? void 0 : _b.linear;
|
|
2581
|
+
const linearConfig = {};
|
|
2582
|
+
if (linearSettings == null ? void 0 : linearSettings.teamId) {
|
|
2583
|
+
linearConfig.teamId = linearSettings.teamId;
|
|
2584
|
+
}
|
|
2585
|
+
if (linearSettings == null ? void 0 : linearSettings.branchFormat) {
|
|
2586
|
+
linearConfig.branchFormat = linearSettings.branchFormat;
|
|
2587
|
+
}
|
|
2588
|
+
logger.debug(`IssueTrackerFactory: Creating LinearService with config:`, JSON.stringify(linearConfig, null, 2));
|
|
2589
|
+
return new LinearService(linearConfig);
|
|
2590
|
+
}
|
|
2591
|
+
default:
|
|
2592
|
+
throw new Error(`Unsupported issue tracker provider: ${provider}`);
|
|
2593
|
+
}
|
|
2350
2594
|
}
|
|
2351
|
-
|
|
2352
|
-
|
|
2353
|
-
|
|
2595
|
+
/**
|
|
2596
|
+
* Get the configured provider name from settings
|
|
2597
|
+
* Defaults to 'github' if not configured
|
|
2598
|
+
*
|
|
2599
|
+
* @param settings - iloom settings
|
|
2600
|
+
* @returns Provider type string
|
|
2601
|
+
*/
|
|
2602
|
+
static getProviderName(settings) {
|
|
2603
|
+
var _a;
|
|
2604
|
+
return ((_a = settings.issueManagement) == null ? void 0 : _a.provider) ?? "github";
|
|
2354
2605
|
}
|
|
2355
2606
|
};
|
|
2356
2607
|
|
|
@@ -2415,12 +2666,12 @@ function isValidEnvKey(key) {
|
|
|
2415
2666
|
}
|
|
2416
2667
|
|
|
2417
2668
|
// src/utils/port.ts
|
|
2418
|
-
import { createHash
|
|
2669
|
+
import { createHash } from "crypto";
|
|
2419
2670
|
function generatePortOffsetFromBranchName(branchName) {
|
|
2420
2671
|
if (!branchName || branchName.trim().length === 0) {
|
|
2421
2672
|
throw new Error("Branch name cannot be empty");
|
|
2422
2673
|
}
|
|
2423
|
-
const hash =
|
|
2674
|
+
const hash = createHash("sha256").update(branchName).digest("hex");
|
|
2424
2675
|
const hashPrefix = hash.slice(0, 8);
|
|
2425
2676
|
const hashAsInt = parseInt(hashPrefix, 16);
|
|
2426
2677
|
const portOffset = hashAsInt % 999 + 1;
|
|
@@ -2515,6 +2766,14 @@ var EnvironmentManager = class {
|
|
|
2515
2766
|
return /* @__PURE__ */ new Map();
|
|
2516
2767
|
}
|
|
2517
2768
|
}
|
|
2769
|
+
/**
|
|
2770
|
+
* Get a specific environment variable from a .env file
|
|
2771
|
+
* Returns null if file doesn't exist or variable is not found
|
|
2772
|
+
*/
|
|
2773
|
+
async getEnvVariable(filePath, variableName) {
|
|
2774
|
+
const envVars = await this.readEnvFile(filePath);
|
|
2775
|
+
return envVars.get(variableName) ?? null;
|
|
2776
|
+
}
|
|
2518
2777
|
/**
|
|
2519
2778
|
* Generic file copy helper that only copies if source exists
|
|
2520
2779
|
* Does not throw if source file doesn't exist - just logs and returns
|
|
@@ -2538,13 +2797,17 @@ var EnvironmentManager = class {
|
|
|
2538
2797
|
calculatePort(options) {
|
|
2539
2798
|
const basePort = options.basePort ?? 3e3;
|
|
2540
2799
|
if (options.issueNumber !== void 0) {
|
|
2541
|
-
const
|
|
2542
|
-
if (
|
|
2543
|
-
|
|
2544
|
-
|
|
2545
|
-
|
|
2800
|
+
const numericIssue = typeof options.issueNumber === "number" ? options.issueNumber : parseInt(String(options.issueNumber), 10);
|
|
2801
|
+
if (!isNaN(numericIssue) && String(numericIssue) === String(options.issueNumber)) {
|
|
2802
|
+
const port = basePort + numericIssue;
|
|
2803
|
+
if (port > 65535) {
|
|
2804
|
+
throw new Error(
|
|
2805
|
+
`Calculated port ${port} exceeds maximum (65535). Use a lower base port or issue number.`
|
|
2806
|
+
);
|
|
2807
|
+
}
|
|
2808
|
+
return port;
|
|
2546
2809
|
}
|
|
2547
|
-
return
|
|
2810
|
+
return calculatePortForBranch(String(options.issueNumber), basePort);
|
|
2548
2811
|
}
|
|
2549
2812
|
if (options.prNumber !== void 0) {
|
|
2550
2813
|
const port = basePort + options.prNumber;
|
|
@@ -2664,8 +2927,9 @@ var DatabaseManager = class {
|
|
|
2664
2927
|
* @param branchName - Name of the branch to create
|
|
2665
2928
|
* @param envFilePath - Path to .env file for configuration checks
|
|
2666
2929
|
* @param cwd - Optional working directory to run commands from
|
|
2930
|
+
* @param fromBranch - Optional parent branch to create from (for child looms)
|
|
2667
2931
|
*/
|
|
2668
|
-
async createBranchIfConfigured(branchName, envFilePath, cwd) {
|
|
2932
|
+
async createBranchIfConfigured(branchName, envFilePath, cwd, fromBranch) {
|
|
2669
2933
|
if (!await this.shouldUseDatabaseBranching(envFilePath)) {
|
|
2670
2934
|
return null;
|
|
2671
2935
|
}
|
|
@@ -2687,7 +2951,7 @@ var DatabaseManager = class {
|
|
|
2687
2951
|
throw error;
|
|
2688
2952
|
}
|
|
2689
2953
|
try {
|
|
2690
|
-
const connectionString = await this.provider.createBranch(branchName,
|
|
2954
|
+
const connectionString = await this.provider.createBranch(branchName, fromBranch, cwd);
|
|
2691
2955
|
logger3.success(`Database branch ready: ${this.provider.sanitizeBranchName(branchName)}`);
|
|
2692
2956
|
return connectionString;
|
|
2693
2957
|
} catch (error) {
|
|
@@ -2774,51 +3038,248 @@ var DatabaseManager = class {
|
|
|
2774
3038
|
};
|
|
2775
3039
|
}
|
|
2776
3040
|
}
|
|
2777
|
-
/**
|
|
2778
|
-
*
|
|
2779
|
-
*
|
|
2780
|
-
*
|
|
2781
|
-
|
|
2782
|
-
|
|
2783
|
-
|
|
2784
|
-
|
|
2785
|
-
|
|
2786
|
-
|
|
2787
|
-
|
|
2788
|
-
|
|
3041
|
+
/**
|
|
3042
|
+
* Get database branch name from connection string (reverse lookup)
|
|
3043
|
+
* Returns branch name if provider supports reverse lookup, null otherwise
|
|
3044
|
+
*
|
|
3045
|
+
* @param connectionString - Database connection string
|
|
3046
|
+
* @param cwd - Optional working directory to run commands from
|
|
3047
|
+
*/
|
|
3048
|
+
async getBranchNameFromConnectionString(connectionString, cwd) {
|
|
3049
|
+
if (!this.provider.isConfigured()) {
|
|
3050
|
+
logger3.debug("Provider not configured, skipping reverse lookup");
|
|
3051
|
+
return null;
|
|
3052
|
+
}
|
|
3053
|
+
if ("getBranchNameFromConnectionString" in this.provider && typeof this.provider.getBranchNameFromConnectionString === "function") {
|
|
3054
|
+
return this.provider.getBranchNameFromConnectionString(connectionString, cwd);
|
|
3055
|
+
}
|
|
3056
|
+
logger3.debug("Provider does not support reverse lookup");
|
|
3057
|
+
return null;
|
|
3058
|
+
}
|
|
3059
|
+
/**
|
|
3060
|
+
* Check if .env has the configured database URL variable
|
|
3061
|
+
* CRITICAL: If user explicitly configured a custom variable name (not default),
|
|
3062
|
+
* throw an error if it's missing from .env
|
|
3063
|
+
*/
|
|
3064
|
+
async hasDatabaseUrlInEnv(envFilePath) {
|
|
3065
|
+
try {
|
|
3066
|
+
const envMap = await this.environment.readEnvFile(envFilePath);
|
|
3067
|
+
if (this.databaseUrlEnvVarName !== "DATABASE_URL") {
|
|
3068
|
+
logger3.debug(`Looking for custom database URL variable: ${this.databaseUrlEnvVarName}`);
|
|
3069
|
+
} else {
|
|
3070
|
+
logger3.debug("Looking for default database URL variable: DATABASE_URL");
|
|
3071
|
+
}
|
|
3072
|
+
if (envMap.has(this.databaseUrlEnvVarName)) {
|
|
3073
|
+
if (this.databaseUrlEnvVarName !== "DATABASE_URL") {
|
|
3074
|
+
logger3.debug(`\u2705 Found custom database URL variable: ${this.databaseUrlEnvVarName}`);
|
|
3075
|
+
} else {
|
|
3076
|
+
logger3.debug(`\u2705 Found default database URL variable: DATABASE_URL`);
|
|
3077
|
+
}
|
|
3078
|
+
return true;
|
|
3079
|
+
}
|
|
3080
|
+
if (this.databaseUrlEnvVarName !== "DATABASE_URL") {
|
|
3081
|
+
logger3.debug(`\u274C Custom database URL variable '${this.databaseUrlEnvVarName}' not found in .env file`);
|
|
3082
|
+
throw new Error(
|
|
3083
|
+
`Configured database URL environment variable '${this.databaseUrlEnvVarName}' not found in .env file. Please add it to your .env file or update your iloom configuration.`
|
|
3084
|
+
);
|
|
3085
|
+
}
|
|
3086
|
+
const hasDefaultVar = envMap.has("DATABASE_URL");
|
|
3087
|
+
if (hasDefaultVar) {
|
|
3088
|
+
logger3.debug("\u2705 Found fallback DATABASE_URL variable");
|
|
3089
|
+
} else {
|
|
3090
|
+
logger3.debug("\u274C No DATABASE_URL variable found in .env file");
|
|
3091
|
+
}
|
|
3092
|
+
return hasDefaultVar;
|
|
3093
|
+
} catch (error) {
|
|
3094
|
+
if (error instanceof Error && error.message.includes("not found in .env")) {
|
|
3095
|
+
throw error;
|
|
3096
|
+
}
|
|
3097
|
+
return false;
|
|
3098
|
+
}
|
|
3099
|
+
}
|
|
3100
|
+
};
|
|
3101
|
+
|
|
3102
|
+
// src/utils/claude.ts
|
|
3103
|
+
init_logger();
|
|
3104
|
+
import { execa as execa4 } from "execa";
|
|
3105
|
+
import { existsSync as existsSync2 } from "fs";
|
|
3106
|
+
import { join } from "path";
|
|
3107
|
+
async function detectClaudeCli() {
|
|
3108
|
+
try {
|
|
3109
|
+
await execa4("command", ["-v", "claude"], {
|
|
3110
|
+
shell: true,
|
|
3111
|
+
timeout: 5e3
|
|
3112
|
+
});
|
|
3113
|
+
return true;
|
|
3114
|
+
} catch (error) {
|
|
3115
|
+
logger.debug("Claude CLI not available", { error });
|
|
3116
|
+
return false;
|
|
3117
|
+
}
|
|
3118
|
+
}
|
|
3119
|
+
function parseJsonStreamOutput(output) {
|
|
3120
|
+
try {
|
|
3121
|
+
const lines = output.split("\n").filter((line) => line.trim());
|
|
3122
|
+
let lastResult = "";
|
|
3123
|
+
for (const line of lines) {
|
|
3124
|
+
try {
|
|
3125
|
+
const jsonObj = JSON.parse(line);
|
|
3126
|
+
if (jsonObj && typeof jsonObj === "object" && jsonObj.type === "result" && "result" in jsonObj) {
|
|
3127
|
+
lastResult = jsonObj.result;
|
|
3128
|
+
}
|
|
3129
|
+
} catch {
|
|
3130
|
+
continue;
|
|
3131
|
+
}
|
|
3132
|
+
}
|
|
3133
|
+
return lastResult || output;
|
|
3134
|
+
} catch {
|
|
3135
|
+
return output;
|
|
3136
|
+
}
|
|
3137
|
+
}
|
|
3138
|
+
async function launchClaude(prompt, options = {}) {
|
|
3139
|
+
const { model, permissionMode, addDir, headless = false, appendSystemPrompt, mcpConfig, allowedTools, disallowedTools, agents } = options;
|
|
3140
|
+
const args = [];
|
|
3141
|
+
if (headless) {
|
|
3142
|
+
args.push("-p");
|
|
3143
|
+
args.push("--output-format", "stream-json");
|
|
3144
|
+
args.push("--verbose");
|
|
3145
|
+
}
|
|
3146
|
+
if (model) {
|
|
3147
|
+
args.push("--model", model);
|
|
3148
|
+
}
|
|
3149
|
+
if (permissionMode && permissionMode !== "default") {
|
|
3150
|
+
args.push("--permission-mode", permissionMode);
|
|
3151
|
+
}
|
|
3152
|
+
if (addDir) {
|
|
3153
|
+
args.push("--add-dir", addDir);
|
|
3154
|
+
}
|
|
3155
|
+
args.push("--add-dir", "/tmp");
|
|
3156
|
+
if (appendSystemPrompt) {
|
|
3157
|
+
args.push("--append-system-prompt", appendSystemPrompt);
|
|
3158
|
+
}
|
|
3159
|
+
if (mcpConfig && mcpConfig.length > 0) {
|
|
3160
|
+
for (const config of mcpConfig) {
|
|
3161
|
+
args.push("--mcp-config", JSON.stringify(config));
|
|
3162
|
+
}
|
|
3163
|
+
}
|
|
3164
|
+
if (allowedTools && allowedTools.length > 0) {
|
|
3165
|
+
args.push("--allowed-tools", ...allowedTools);
|
|
3166
|
+
}
|
|
3167
|
+
if (disallowedTools && disallowedTools.length > 0) {
|
|
3168
|
+
args.push("--disallowed-tools", ...disallowedTools);
|
|
3169
|
+
}
|
|
3170
|
+
if (agents) {
|
|
3171
|
+
args.push("--agents", JSON.stringify(agents));
|
|
3172
|
+
}
|
|
3173
|
+
try {
|
|
3174
|
+
if (headless) {
|
|
3175
|
+
const isDebugMode = logger.isDebugEnabled();
|
|
3176
|
+
const execaOptions = {
|
|
3177
|
+
input: prompt,
|
|
3178
|
+
timeout: 0,
|
|
3179
|
+
// Disable timeout for long responses
|
|
3180
|
+
...addDir && { cwd: addDir },
|
|
3181
|
+
// Run Claude in the worktree directory
|
|
3182
|
+
verbose: isDebugMode,
|
|
3183
|
+
...isDebugMode && { stdio: ["pipe", "pipe", "pipe"] }
|
|
3184
|
+
// Enable streaming in debug mode
|
|
3185
|
+
};
|
|
3186
|
+
const subprocess = execa4("claude", args, execaOptions);
|
|
3187
|
+
const isJsonStreamFormat = args.includes("--output-format") && args.includes("stream-json");
|
|
3188
|
+
let outputBuffer = "";
|
|
3189
|
+
let isStreaming = false;
|
|
3190
|
+
let isFirstProgress = true;
|
|
3191
|
+
if (subprocess.stdout && typeof subprocess.stdout.on === "function") {
|
|
3192
|
+
isStreaming = true;
|
|
3193
|
+
subprocess.stdout.on("data", (chunk) => {
|
|
3194
|
+
const text = chunk.toString();
|
|
3195
|
+
outputBuffer += text;
|
|
3196
|
+
if (isDebugMode) {
|
|
3197
|
+
process.stdout.write(text);
|
|
3198
|
+
} else {
|
|
3199
|
+
if (isFirstProgress) {
|
|
3200
|
+
process.stdout.write("\u{1F916} .");
|
|
3201
|
+
isFirstProgress = false;
|
|
3202
|
+
} else {
|
|
3203
|
+
process.stdout.write(".");
|
|
3204
|
+
}
|
|
3205
|
+
}
|
|
3206
|
+
});
|
|
2789
3207
|
}
|
|
2790
|
-
|
|
2791
|
-
|
|
2792
|
-
|
|
2793
|
-
|
|
2794
|
-
|
|
3208
|
+
const result = await subprocess;
|
|
3209
|
+
if (isStreaming) {
|
|
3210
|
+
const rawOutput = outputBuffer.trim();
|
|
3211
|
+
if (!isDebugMode) {
|
|
3212
|
+
process.stdout.write("\n");
|
|
2795
3213
|
}
|
|
2796
|
-
return
|
|
2797
|
-
}
|
|
2798
|
-
if (this.databaseUrlEnvVarName !== "DATABASE_URL") {
|
|
2799
|
-
logger3.debug(`\u274C Custom database URL variable '${this.databaseUrlEnvVarName}' not found in .env file`);
|
|
2800
|
-
throw new Error(
|
|
2801
|
-
`Configured database URL environment variable '${this.databaseUrlEnvVarName}' not found in .env file. Please add it to your .env file or update your iloom configuration.`
|
|
2802
|
-
);
|
|
2803
|
-
}
|
|
2804
|
-
const hasDefaultVar = envMap.has("DATABASE_URL");
|
|
2805
|
-
if (hasDefaultVar) {
|
|
2806
|
-
logger3.debug("\u2705 Found fallback DATABASE_URL variable");
|
|
3214
|
+
return isJsonStreamFormat ? parseJsonStreamOutput(rawOutput) : rawOutput;
|
|
2807
3215
|
} else {
|
|
2808
|
-
|
|
3216
|
+
if (isDebugMode) {
|
|
3217
|
+
process.stdout.write(result.stdout);
|
|
3218
|
+
if (result.stdout && !result.stdout.endsWith("\n")) {
|
|
3219
|
+
process.stdout.write("\n");
|
|
3220
|
+
}
|
|
3221
|
+
} else {
|
|
3222
|
+
process.stdout.write("\u{1F916} .");
|
|
3223
|
+
process.stdout.write("\n");
|
|
3224
|
+
}
|
|
3225
|
+
const rawOutput = result.stdout.trim();
|
|
3226
|
+
return isJsonStreamFormat ? parseJsonStreamOutput(rawOutput) : rawOutput;
|
|
2809
3227
|
}
|
|
2810
|
-
|
|
3228
|
+
} else {
|
|
3229
|
+
await execa4("claude", [...args, "--", prompt], {
|
|
3230
|
+
...addDir && { cwd: addDir },
|
|
3231
|
+
stdio: "inherit",
|
|
3232
|
+
// Let user interact directly in current terminal
|
|
3233
|
+
timeout: 0,
|
|
3234
|
+
// Disable timeout
|
|
3235
|
+
verbose: logger.isDebugEnabled()
|
|
3236
|
+
});
|
|
3237
|
+
return;
|
|
3238
|
+
}
|
|
3239
|
+
} catch (error) {
|
|
3240
|
+
const execaError = error;
|
|
3241
|
+
const errorMessage = execaError.stderr ?? execaError.message ?? "Unknown Claude CLI error";
|
|
3242
|
+
throw new Error(`Claude CLI error: ${errorMessage}`);
|
|
3243
|
+
}
|
|
3244
|
+
}
|
|
3245
|
+
async function launchClaudeInNewTerminalWindow(_prompt, options) {
|
|
3246
|
+
const { workspacePath, branchName, oneShot = "default", port, setArguments, executablePath } = options;
|
|
3247
|
+
if (!workspacePath) {
|
|
3248
|
+
throw new Error("workspacePath is required for terminal window launch");
|
|
3249
|
+
}
|
|
3250
|
+
const { openTerminalWindow: openTerminalWindow2 } = await Promise.resolve().then(() => (init_terminal(), terminal_exports));
|
|
3251
|
+
const executable = executablePath ?? "iloom";
|
|
3252
|
+
let launchCommand = `${executable} spin`;
|
|
3253
|
+
if (oneShot !== "default") {
|
|
3254
|
+
launchCommand += ` --one-shot=${oneShot}`;
|
|
3255
|
+
}
|
|
3256
|
+
if (setArguments && setArguments.length > 0) {
|
|
3257
|
+
for (const setArg of setArguments) {
|
|
3258
|
+
launchCommand += ` --set ${setArg}`;
|
|
3259
|
+
}
|
|
3260
|
+
}
|
|
3261
|
+
let backgroundColor;
|
|
3262
|
+
if (branchName) {
|
|
3263
|
+
try {
|
|
3264
|
+
const { generateColorFromBranchName: generateColorFromBranchName2 } = await Promise.resolve().then(() => (init_color(), color_exports));
|
|
3265
|
+
const colorData = generateColorFromBranchName2(branchName);
|
|
3266
|
+
backgroundColor = colorData.rgb;
|
|
2811
3267
|
} catch (error) {
|
|
2812
|
-
|
|
2813
|
-
|
|
2814
|
-
|
|
2815
|
-
return false;
|
|
3268
|
+
logger.warn(
|
|
3269
|
+
`Failed to generate terminal color: ${error instanceof Error ? error.message : "Unknown error"}`
|
|
3270
|
+
);
|
|
2816
3271
|
}
|
|
2817
3272
|
}
|
|
2818
|
-
|
|
2819
|
-
|
|
2820
|
-
|
|
2821
|
-
|
|
3273
|
+
const hasEnvFile = existsSync2(join(workspacePath, ".env"));
|
|
3274
|
+
await openTerminalWindow2({
|
|
3275
|
+
workspacePath,
|
|
3276
|
+
command: launchCommand,
|
|
3277
|
+
...backgroundColor && { backgroundColor },
|
|
3278
|
+
includeEnvSetup: hasEnvFile,
|
|
3279
|
+
// source .env only if it exists
|
|
3280
|
+
...port !== void 0 && { port, includePortExport: true }
|
|
3281
|
+
});
|
|
3282
|
+
}
|
|
2822
3283
|
|
|
2823
3284
|
// src/lib/PromptTemplateManager.ts
|
|
2824
3285
|
init_logger();
|
|
@@ -2898,6 +3359,9 @@ var PromptTemplateManager = class {
|
|
|
2898
3359
|
if (variables.SETTINGS_SCHEMA !== void 0) {
|
|
2899
3360
|
result = result.replace(/SETTINGS_SCHEMA/g, variables.SETTINGS_SCHEMA);
|
|
2900
3361
|
}
|
|
3362
|
+
if (variables.SETTINGS_GLOBAL_JSON !== void 0) {
|
|
3363
|
+
result = result.replace(/SETTINGS_GLOBAL_JSON/g, variables.SETTINGS_GLOBAL_JSON);
|
|
3364
|
+
}
|
|
2901
3365
|
if (variables.SETTINGS_JSON !== void 0) {
|
|
2902
3366
|
result = result.replace(/SETTINGS_JSON/g, variables.SETTINGS_JSON);
|
|
2903
3367
|
}
|
|
@@ -2959,6 +3423,12 @@ var PromptTemplateManager = class {
|
|
|
2959
3423
|
} else {
|
|
2960
3424
|
result = result.replace(settingsJsonRegex, "");
|
|
2961
3425
|
}
|
|
3426
|
+
const settingsGlobalJsonRegex = /\{\{#IF SETTINGS_GLOBAL_JSON\}\}(.*?)\{\{\/IF SETTINGS_GLOBAL_JSON\}\}/gs;
|
|
3427
|
+
if (variables.SETTINGS_GLOBAL_JSON !== void 0 && variables.SETTINGS_GLOBAL_JSON !== "") {
|
|
3428
|
+
result = result.replace(settingsGlobalJsonRegex, "$1");
|
|
3429
|
+
} else {
|
|
3430
|
+
result = result.replace(settingsGlobalJsonRegex, "");
|
|
3431
|
+
}
|
|
2962
3432
|
const settingsLocalJsonRegex = /\{\{#IF SETTINGS_LOCAL_JSON\}\}(.*?)\{\{\/IF SETTINGS_LOCAL_JSON\}\}/gs;
|
|
2963
3433
|
if (variables.SETTINGS_LOCAL_JSON !== void 0 && variables.SETTINGS_LOCAL_JSON !== "") {
|
|
2964
3434
|
result = result.replace(settingsLocalJsonRegex, "$1");
|
|
@@ -3119,17 +3589,6 @@ var ClaudeService = class {
|
|
|
3119
3589
|
throw error;
|
|
3120
3590
|
}
|
|
3121
3591
|
}
|
|
3122
|
-
/**
|
|
3123
|
-
* Generate branch name with Claude, with fallback on failure
|
|
3124
|
-
*/
|
|
3125
|
-
async generateBranchNameWithFallback(issueTitle, issueNumber) {
|
|
3126
|
-
try {
|
|
3127
|
-
return await generateBranchName(issueTitle, issueNumber);
|
|
3128
|
-
} catch (error) {
|
|
3129
|
-
logger.warn("Claude branch name generation failed, using fallback", { error });
|
|
3130
|
-
return `feat/issue-${issueNumber}`;
|
|
3131
|
-
}
|
|
3132
|
-
}
|
|
3133
3592
|
};
|
|
3134
3593
|
|
|
3135
3594
|
// src/lib/ClaudeContextManager.ts
|
|
@@ -3189,17 +3648,322 @@ var ClaudeContextManager = class {
|
|
|
3189
3648
|
|
|
3190
3649
|
// src/utils/index.ts
|
|
3191
3650
|
init_logger();
|
|
3651
|
+
|
|
3652
|
+
// src/utils/linear-markup-converter.ts
|
|
3653
|
+
import { appendFileSync } from "fs";
|
|
3654
|
+
import { join as join2, dirname, basename, extname } from "path";
|
|
3655
|
+
var LinearMarkupConverter = class {
|
|
3656
|
+
/**
|
|
3657
|
+
* Convert HTML details/summary blocks to Linear's collapsible format
|
|
3658
|
+
* Handles nested details blocks recursively
|
|
3659
|
+
*
|
|
3660
|
+
* @param text - Text containing HTML details/summary blocks
|
|
3661
|
+
* @returns Text with details/summary converted to Linear format
|
|
3662
|
+
*/
|
|
3663
|
+
static convertDetailsToLinear(text) {
|
|
3664
|
+
if (!text) {
|
|
3665
|
+
return text;
|
|
3666
|
+
}
|
|
3667
|
+
let previousText = "";
|
|
3668
|
+
let currentText = text;
|
|
3669
|
+
while (previousText !== currentText) {
|
|
3670
|
+
previousText = currentText;
|
|
3671
|
+
currentText = this.convertSinglePass(currentText);
|
|
3672
|
+
}
|
|
3673
|
+
return currentText;
|
|
3674
|
+
}
|
|
3675
|
+
/**
|
|
3676
|
+
* Perform a single pass of details block conversion
|
|
3677
|
+
* Converts the innermost details blocks first
|
|
3678
|
+
*/
|
|
3679
|
+
static convertSinglePass(text) {
|
|
3680
|
+
const detailsRegex = /<details[^>]*>\s*<summary[^>]*>(.*?)<\/summary>\s*(.*?)\s*<\/details>/gis;
|
|
3681
|
+
return text.replace(detailsRegex, (_match, summary, content) => {
|
|
3682
|
+
const cleanSummary = this.cleanText(summary);
|
|
3683
|
+
const cleanContent = this.cleanContent(content);
|
|
3684
|
+
if (cleanContent) {
|
|
3685
|
+
return `+++ ${cleanSummary}
|
|
3686
|
+
|
|
3687
|
+
${cleanContent}
|
|
3688
|
+
|
|
3689
|
+
+++`;
|
|
3690
|
+
} else {
|
|
3691
|
+
return `+++ ${cleanSummary}
|
|
3692
|
+
|
|
3693
|
+
+++`;
|
|
3694
|
+
}
|
|
3695
|
+
});
|
|
3696
|
+
}
|
|
3697
|
+
/**
|
|
3698
|
+
* Clean text by trimming whitespace and decoding common HTML entities
|
|
3699
|
+
*/
|
|
3700
|
+
static cleanText(text) {
|
|
3701
|
+
return text.trim().replace(/</g, "<").replace(/>/g, ">").replace(/&/g, "&").replace(/"/g, '"').replace(/'/g, "'");
|
|
3702
|
+
}
|
|
3703
|
+
/**
|
|
3704
|
+
* Clean content while preserving internal structure
|
|
3705
|
+
* - Removes leading/trailing whitespace
|
|
3706
|
+
* - Normalizes internal blank lines (max 2 consecutive newlines)
|
|
3707
|
+
* - Preserves code blocks and other formatting
|
|
3708
|
+
*/
|
|
3709
|
+
static cleanContent(content) {
|
|
3710
|
+
if (!content) {
|
|
3711
|
+
return "";
|
|
3712
|
+
}
|
|
3713
|
+
let cleaned = content.trim();
|
|
3714
|
+
cleaned = cleaned.replace(/\n{3,}/g, "\n\n");
|
|
3715
|
+
return cleaned;
|
|
3716
|
+
}
|
|
3717
|
+
/**
|
|
3718
|
+
* Check if text contains HTML details/summary blocks
|
|
3719
|
+
* Useful for conditional conversion
|
|
3720
|
+
*/
|
|
3721
|
+
static hasDetailsBlocks(text) {
|
|
3722
|
+
if (!text) {
|
|
3723
|
+
return false;
|
|
3724
|
+
}
|
|
3725
|
+
const detailsRegex = /<details[^>]*>.*?<summary[^>]*>.*?<\/summary>.*?<\/details>/is;
|
|
3726
|
+
return detailsRegex.test(text);
|
|
3727
|
+
}
|
|
3728
|
+
/**
|
|
3729
|
+
* Remove wrapper tags from code sample details blocks
|
|
3730
|
+
* Identifies details blocks where summary contains "X lines" pattern
|
|
3731
|
+
* and removes the details/summary tags while preserving the content
|
|
3732
|
+
*
|
|
3733
|
+
* @param text - Text containing potential code sample details blocks
|
|
3734
|
+
* @returns Text with code sample wrappers removed
|
|
3735
|
+
*/
|
|
3736
|
+
static removeCodeSampleWrappers(text) {
|
|
3737
|
+
if (!text) {
|
|
3738
|
+
return text;
|
|
3739
|
+
}
|
|
3740
|
+
const codeSampleRegex = /<details[^>]*>\s*<summary[^>]*>([^<]*\d+\s+lines[^<]*)<\/summary>\s*([\s\S]*?)<\/details>/gi;
|
|
3741
|
+
return text.replace(codeSampleRegex, (_match, _summary, content) => {
|
|
3742
|
+
return content.trim();
|
|
3743
|
+
});
|
|
3744
|
+
}
|
|
3745
|
+
/**
|
|
3746
|
+
* Convert text for Linear - applies all necessary conversions
|
|
3747
|
+
* Currently only converts details/summary blocks, but can be extended
|
|
3748
|
+
* for other HTML to Linear markdown conversions
|
|
3749
|
+
*/
|
|
3750
|
+
static convertToLinear(text) {
|
|
3751
|
+
if (!text) {
|
|
3752
|
+
return text;
|
|
3753
|
+
}
|
|
3754
|
+
this.logConversion("INPUT", text);
|
|
3755
|
+
let converted = text;
|
|
3756
|
+
converted = this.removeCodeSampleWrappers(converted);
|
|
3757
|
+
converted = this.convertDetailsToLinear(converted);
|
|
3758
|
+
this.logConversion("OUTPUT", converted);
|
|
3759
|
+
return converted;
|
|
3760
|
+
}
|
|
3761
|
+
/**
|
|
3762
|
+
* Log conversion input/output if LINEAR_MARKDOWN_LOG_FILE is set
|
|
3763
|
+
*/
|
|
3764
|
+
static logConversion(label, content) {
|
|
3765
|
+
const logFilePath = process.env.LINEAR_MARKDOWN_LOG_FILE;
|
|
3766
|
+
if (!logFilePath) {
|
|
3767
|
+
return;
|
|
3768
|
+
}
|
|
3769
|
+
try {
|
|
3770
|
+
const timestampedPath = this.getTimestampedLogPath(logFilePath);
|
|
3771
|
+
const timestamp = (/* @__PURE__ */ new Date()).toISOString();
|
|
3772
|
+
const separator = "================================";
|
|
3773
|
+
const logEntry = `${separator}
|
|
3774
|
+
[${timestamp}] CONVERSION ${label}
|
|
3775
|
+
${separator}
|
|
3776
|
+
${label}:
|
|
3777
|
+
${content}
|
|
3778
|
+
|
|
3779
|
+
`;
|
|
3780
|
+
appendFileSync(timestampedPath, logEntry, "utf-8");
|
|
3781
|
+
} catch {
|
|
3782
|
+
}
|
|
3783
|
+
}
|
|
3784
|
+
/**
|
|
3785
|
+
* Generate timestamped log file path
|
|
3786
|
+
* Example: debug.log -> debug-20231202-161234.log
|
|
3787
|
+
*/
|
|
3788
|
+
static getTimestampedLogPath(logFilePath) {
|
|
3789
|
+
const dir = dirname(logFilePath);
|
|
3790
|
+
const ext = extname(logFilePath);
|
|
3791
|
+
const base = basename(logFilePath, ext);
|
|
3792
|
+
const now = /* @__PURE__ */ new Date();
|
|
3793
|
+
const timestamp = [
|
|
3794
|
+
now.getFullYear(),
|
|
3795
|
+
String(now.getMonth() + 1).padStart(2, "0"),
|
|
3796
|
+
String(now.getDate()).padStart(2, "0")
|
|
3797
|
+
].join("") + "-" + [
|
|
3798
|
+
String(now.getHours()).padStart(2, "0"),
|
|
3799
|
+
String(now.getMinutes()).padStart(2, "0"),
|
|
3800
|
+
String(now.getSeconds()).padStart(2, "0")
|
|
3801
|
+
].join("");
|
|
3802
|
+
return join2(dir, `${base}-${timestamp}${ext}`);
|
|
3803
|
+
}
|
|
3804
|
+
};
|
|
3805
|
+
|
|
3806
|
+
// src/utils/table-formatter.ts
|
|
3807
|
+
var TableFormatter = class {
|
|
3808
|
+
/**
|
|
3809
|
+
* Pad table headers to achieve equal column widths
|
|
3810
|
+
* @param headers Array of header text
|
|
3811
|
+
* @param options Formatting options
|
|
3812
|
+
* @returns Array of padded headers
|
|
3813
|
+
*/
|
|
3814
|
+
static padHeaders(headers, options = {}) {
|
|
3815
|
+
if (!headers || headers.length === 0) {
|
|
3816
|
+
throw new Error("Headers array cannot be empty");
|
|
3817
|
+
}
|
|
3818
|
+
const {
|
|
3819
|
+
targetTotalWidth = this.DEFAULT_TARGET_WIDTH,
|
|
3820
|
+
paddingChar = this.DEFAULT_PADDING_CHAR,
|
|
3821
|
+
maxPadding = this.DEFAULT_MAX_PADDING
|
|
3822
|
+
} = options;
|
|
3823
|
+
const columnCount = headers.length;
|
|
3824
|
+
const targetWidthPerColumn = Math.floor(targetTotalWidth / columnCount);
|
|
3825
|
+
return headers.map((header) => {
|
|
3826
|
+
const currentLength = header.length;
|
|
3827
|
+
const paddingNeeded = Math.max(0, targetWidthPerColumn - currentLength);
|
|
3828
|
+
const cappedPadding = Math.min(paddingNeeded, maxPadding);
|
|
3829
|
+
const padding = paddingChar.repeat(cappedPadding);
|
|
3830
|
+
return header + padding;
|
|
3831
|
+
});
|
|
3832
|
+
}
|
|
3833
|
+
/**
|
|
3834
|
+
* Generate a complete markdown table with padded headers
|
|
3835
|
+
* @param options Table generation options
|
|
3836
|
+
* @returns Complete markdown table string
|
|
3837
|
+
*/
|
|
3838
|
+
static generateTable(options) {
|
|
3839
|
+
var _a;
|
|
3840
|
+
const { headers, rows, ...paddingOptions } = options;
|
|
3841
|
+
if (!rows || rows.length === 0) {
|
|
3842
|
+
throw new Error("Table must have at least one row");
|
|
3843
|
+
}
|
|
3844
|
+
const invalidRows = rows.filter((row) => row.length !== headers.length);
|
|
3845
|
+
if (invalidRows.length > 0) {
|
|
3846
|
+
throw new Error(
|
|
3847
|
+
`All rows must have ${headers.length} columns. Found rows with ${(_a = invalidRows[0]) == null ? void 0 : _a.length} columns.`
|
|
3848
|
+
);
|
|
3849
|
+
}
|
|
3850
|
+
const paddedHeaders = this.padHeaders(headers, paddingOptions);
|
|
3851
|
+
const headerRow = `| ${paddedHeaders.join(" | ")} |`;
|
|
3852
|
+
const separators = headers.map(() => "---");
|
|
3853
|
+
const separatorRow = `| ${separators.join(" | ")} |`;
|
|
3854
|
+
const dataRows = rows.map((row) => `| ${row.join(" | ")} |`).join("\n");
|
|
3855
|
+
return [headerRow, separatorRow, dataRows].join("\n");
|
|
3856
|
+
}
|
|
3857
|
+
/**
|
|
3858
|
+
* Calculate the optimal width distribution for given headers
|
|
3859
|
+
* @param headers Array of header text
|
|
3860
|
+
* @param targetTotalWidth Target total width
|
|
3861
|
+
* @param maxPadding Maximum padding entities per column
|
|
3862
|
+
* @returns Width distribution information
|
|
3863
|
+
*/
|
|
3864
|
+
static calculateWidthDistribution(headers, targetTotalWidth = this.DEFAULT_TARGET_WIDTH, maxPadding = this.DEFAULT_MAX_PADDING) {
|
|
3865
|
+
if (!headers || headers.length === 0) {
|
|
3866
|
+
throw new Error("Headers array cannot be empty");
|
|
3867
|
+
}
|
|
3868
|
+
const columnCount = headers.length;
|
|
3869
|
+
const widthPerColumn = Math.floor(targetTotalWidth / columnCount);
|
|
3870
|
+
const headerInfo = headers.map((header) => {
|
|
3871
|
+
const paddingNeeded = Math.max(0, widthPerColumn - header.length);
|
|
3872
|
+
const paddingUsed = Math.min(paddingNeeded, maxPadding);
|
|
3873
|
+
return {
|
|
3874
|
+
text: header,
|
|
3875
|
+
currentLength: header.length,
|
|
3876
|
+
targetLength: widthPerColumn,
|
|
3877
|
+
paddingNeeded,
|
|
3878
|
+
paddingUsed
|
|
3879
|
+
};
|
|
3880
|
+
});
|
|
3881
|
+
return {
|
|
3882
|
+
totalWidth: targetTotalWidth,
|
|
3883
|
+
widthPerColumn,
|
|
3884
|
+
headers: headerInfo
|
|
3885
|
+
};
|
|
3886
|
+
}
|
|
3887
|
+
/**
|
|
3888
|
+
* Create a simple two-column assessment table (common pattern)
|
|
3889
|
+
* @param assessmentData Array of [question, answer] pairs
|
|
3890
|
+
* @param options Formatting options
|
|
3891
|
+
* @returns Formatted markdown table
|
|
3892
|
+
*/
|
|
3893
|
+
static createAssessmentTable(assessmentData, options = {}) {
|
|
3894
|
+
const headers = ["Question", "Answer"];
|
|
3895
|
+
const rows = assessmentData;
|
|
3896
|
+
return this.generateTable({
|
|
3897
|
+
headers,
|
|
3898
|
+
rows,
|
|
3899
|
+
...options
|
|
3900
|
+
});
|
|
3901
|
+
}
|
|
3902
|
+
/**
|
|
3903
|
+
* Create a three-column status table (common pattern)
|
|
3904
|
+
* @param statusData Array of [task, status, assignee] tuples
|
|
3905
|
+
* @param options Formatting options
|
|
3906
|
+
* @returns Formatted markdown table
|
|
3907
|
+
*/
|
|
3908
|
+
static createStatusTable(statusData, options = {}) {
|
|
3909
|
+
const headers = ["Task", "Status", "Assignee"];
|
|
3910
|
+
const rows = statusData;
|
|
3911
|
+
return this.generateTable({
|
|
3912
|
+
headers,
|
|
3913
|
+
rows,
|
|
3914
|
+
...options
|
|
3915
|
+
});
|
|
3916
|
+
}
|
|
3917
|
+
/**
|
|
3918
|
+
* Preview table formatting without generating full markdown
|
|
3919
|
+
* Useful for debugging and development
|
|
3920
|
+
* @param headers Array of header text
|
|
3921
|
+
* @param options Formatting options
|
|
3922
|
+
* @returns Human-readable formatting preview
|
|
3923
|
+
*/
|
|
3924
|
+
static previewFormatting(headers, options = {}) {
|
|
3925
|
+
const { maxPadding = this.DEFAULT_MAX_PADDING } = options;
|
|
3926
|
+
const distribution = this.calculateWidthDistribution(
|
|
3927
|
+
headers,
|
|
3928
|
+
options.targetTotalWidth,
|
|
3929
|
+
maxPadding
|
|
3930
|
+
);
|
|
3931
|
+
const lines = [
|
|
3932
|
+
`Table Formatting Preview`,
|
|
3933
|
+
`========================`,
|
|
3934
|
+
`Target total width: ${distribution.totalWidth} characters`,
|
|
3935
|
+
`Columns: ${headers.length}`,
|
|
3936
|
+
`Width per column: ${distribution.widthPerColumn} characters`,
|
|
3937
|
+
`Max padding per column: ${maxPadding} entities`,
|
|
3938
|
+
``
|
|
3939
|
+
];
|
|
3940
|
+
distribution.headers.forEach((header, index) => {
|
|
3941
|
+
const cappedIndicator = header.paddingUsed < header.paddingNeeded ? " (capped)" : "";
|
|
3942
|
+
lines.push(
|
|
3943
|
+
`Column ${index + 1}: "${header.text}" (${header.currentLength} chars) + ${header.paddingUsed} padding${cappedIndicator} (target: ${header.paddingNeeded}) = ${header.currentLength + header.paddingUsed * 6} total string length`
|
|
3944
|
+
);
|
|
3945
|
+
});
|
|
3946
|
+
return lines.join("\n");
|
|
3947
|
+
}
|
|
3948
|
+
};
|
|
3949
|
+
TableFormatter.DEFAULT_TARGET_WIDTH = 140;
|
|
3950
|
+
TableFormatter.DEFAULT_PADDING_CHAR = " ";
|
|
3951
|
+
TableFormatter.DEFAULT_MAX_PADDING = 16;
|
|
3192
3952
|
export {
|
|
3193
3953
|
ClaudeContextManager,
|
|
3194
3954
|
DatabaseManager,
|
|
3195
3955
|
EnvironmentManager,
|
|
3196
3956
|
GitHubService,
|
|
3197
3957
|
GitWorktreeManager,
|
|
3958
|
+
IssueTrackerFactory,
|
|
3959
|
+
LinearMarkupConverter,
|
|
3960
|
+
TableFormatter,
|
|
3198
3961
|
WorkspaceManager,
|
|
3199
3962
|
branchExists,
|
|
3200
3963
|
createLogger,
|
|
3201
3964
|
ensureRepositoryHasCommits,
|
|
3202
3965
|
executeGitCommand,
|
|
3966
|
+
extractIssueNumber,
|
|
3203
3967
|
extractPRNumber,
|
|
3204
3968
|
findAllBranchesForIssue,
|
|
3205
3969
|
findMainWorktreePath,
|