@iloom/cli 0.1.19 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +290 -30
- package/dist/BranchNamingService-3OQPRSWT.js +13 -0
- package/dist/ClaudeContextManager-MUQSDY2E.js +13 -0
- package/dist/ClaudeService-HG4VQ7AW.js +12 -0
- package/dist/GitHubService-EBOETDIW.js +11 -0
- package/dist/{LoomLauncher-UMMLPIZO.js → LoomLauncher-FLEMBCSQ.js} +64 -33
- package/dist/LoomLauncher-FLEMBCSQ.js.map +1 -0
- package/dist/ProjectCapabilityDetector-34LU7JJ4.js +9 -0
- package/dist/{PromptTemplateManager-WII75TKH.js → PromptTemplateManager-A52RUAMS.js} +2 -2
- package/dist/README.md +290 -30
- package/dist/{SettingsManager-SKLUVE3K.js → SettingsManager-WHHFGSL7.js} +12 -4
- package/dist/SettingsMigrationManager-AGIIIPDQ.js +10 -0
- package/dist/agents/iloom-issue-analyze-and-plan.md +125 -35
- package/dist/agents/iloom-issue-analyzer.md +284 -32
- package/dist/agents/iloom-issue-complexity-evaluator.md +40 -21
- package/dist/agents/iloom-issue-enhancer.md +69 -48
- package/dist/agents/iloom-issue-implementer.md +36 -25
- package/dist/agents/iloom-issue-planner.md +35 -24
- package/dist/agents/iloom-issue-reviewer.md +62 -9
- package/dist/chunk-3KATJIKO.js +55 -0
- package/dist/chunk-3KATJIKO.js.map +1 -0
- package/dist/{chunk-JXQXSC45.js → chunk-3RUPPQRG.js} +1 -18
- package/dist/chunk-3RUPPQRG.js.map +1 -0
- package/dist/{chunk-PR7FKQBG.js → chunk-47KSHUCR.js} +3 -3
- package/dist/chunk-47KSHUCR.js.map +1 -0
- package/dist/{chunk-IO4WFTL2.js → chunk-4HHRTA7Q.js} +3 -3
- package/dist/{chunk-IO4WFTL2.js.map → chunk-4HHRTA7Q.js.map} +1 -1
- package/dist/chunk-5EF7Z346.js +1987 -0
- package/dist/chunk-5EF7Z346.js.map +1 -0
- package/dist/{chunk-VVH3ANF2.js → chunk-AWOFAD5O.js} +12 -12
- package/dist/chunk-AWOFAD5O.js.map +1 -0
- package/dist/{chunk-DEPYQRRB.js → chunk-C5QCTEQK.js} +2 -2
- package/dist/{chunk-CWR2SANQ.js → chunk-EBISESAP.js} +1 -1
- package/dist/{chunk-ELFT36PV.js → chunk-FIAT22G7.js} +4 -16
- package/dist/chunk-FIAT22G7.js.map +1 -0
- package/dist/{chunk-ZWXJBSUW.js → chunk-G2IEYOLQ.js} +11 -38
- package/dist/chunk-G2IEYOLQ.js.map +1 -0
- package/dist/{chunk-ZMNQBJUI.js → chunk-IP7SMKIF.js} +61 -22
- package/dist/chunk-IP7SMKIF.js.map +1 -0
- package/dist/{chunk-JNKJ7NJV.js → chunk-JKXJ7BGL.js} +6 -2
- package/dist/{chunk-JNKJ7NJV.js.map → chunk-JKXJ7BGL.js.map} +1 -1
- package/dist/{chunk-LAPY6NAE.js → chunk-JQFO7QQN.js} +68 -12
- package/dist/{chunk-LAPY6NAE.js.map → chunk-JQFO7QQN.js.map} +1 -1
- package/dist/{SettingsMigrationManager-MTQIMI54.js → chunk-KLBYVHPK.js} +3 -2
- package/dist/{chunk-KOCQAD2E.js → chunk-MAVL6PJF.js} +26 -3
- package/dist/chunk-MAVL6PJF.js.map +1 -0
- package/dist/{chunk-USVVV3FP.js → chunk-MKWYLDFK.js} +5 -5
- package/dist/chunk-ML3NRPNB.js +396 -0
- package/dist/chunk-ML3NRPNB.js.map +1 -0
- package/dist/{chunk-FXV24OYZ.js → chunk-PA6Q6AWM.js} +24 -4
- package/dist/chunk-PA6Q6AWM.js.map +1 -0
- package/dist/chunk-RO26VS3W.js +444 -0
- package/dist/chunk-RO26VS3W.js.map +1 -0
- package/dist/{chunk-PV3GAXQO.js → chunk-VAYCCUXW.js} +72 -2
- package/dist/{chunk-PV3GAXQO.js.map → chunk-VAYCCUXW.js.map} +1 -1
- package/dist/{chunk-SPYPLHMK.js → chunk-VU3QMIP2.js} +34 -2
- package/dist/chunk-VU3QMIP2.js.map +1 -0
- package/dist/{chunk-PVAVNJKS.js → chunk-WEN5C5DM.js} +10 -1
- package/dist/chunk-WEN5C5DM.js.map +1 -0
- package/dist/{chunk-PXZBAC2M.js → chunk-XXV3UFZL.js} +4 -4
- package/dist/{chunk-PXZBAC2M.js.map → chunk-XXV3UFZL.js.map} +1 -1
- package/dist/{chunk-RSRO7564.js → chunk-ZE74H5BR.js} +28 -3
- package/dist/chunk-ZE74H5BR.js.map +1 -0
- package/dist/{chunk-GZP4UGGM.js → chunk-ZM3CFL5L.js} +2 -2
- package/dist/{chunk-BLCTGFZN.js → chunk-ZT3YZB4K.js} +3 -4
- package/dist/chunk-ZT3YZB4K.js.map +1 -0
- package/dist/{claude-7LUVDZZ4.js → claude-GOP6PFC7.js} +2 -2
- package/dist/{cleanup-ZHROIBSQ.js → cleanup-7RWLBSLE.js} +86 -25
- package/dist/cleanup-7RWLBSLE.js.map +1 -0
- package/dist/cli.js +2513 -64
- package/dist/cli.js.map +1 -1
- package/dist/{contribute-3MQJ3XAQ.js → contribute-BS2L4FZR.js} +9 -6
- package/dist/{contribute-3MQJ3XAQ.js.map → contribute-BS2L4FZR.js.map} +1 -1
- package/dist/{feedback-ZOUCCHN4.js → feedback-N4ECWIPF.js} +15 -14
- package/dist/{feedback-ZOUCCHN4.js.map → feedback-N4ECWIPF.js.map} +1 -1
- package/dist/{git-OUYMVYJX.js → git-TDXKRTXM.js} +4 -2
- package/dist/{ignite-HICLZEYU.js → ignite-VM64QO3J.js} +32 -27
- package/dist/ignite-VM64QO3J.js.map +1 -0
- package/dist/index.d.ts +379 -45
- package/dist/index.js +1241 -448
- package/dist/index.js.map +1 -1
- package/dist/{init-UMKNHNV5.js → init-G3T64SC4.js} +104 -40
- package/dist/init-G3T64SC4.js.map +1 -0
- package/dist/mcp/issue-management-server.js +934 -0
- package/dist/mcp/issue-management-server.js.map +1 -0
- package/dist/{neon-helpers-ZVIRPKCI.js → neon-helpers-WPUACUVC.js} +3 -3
- package/dist/{open-ETZUFSE4.js → open-KXDXEKRZ.js} +39 -36
- package/dist/open-KXDXEKRZ.js.map +1 -0
- package/dist/{prompt-ANTQWHUF.js → prompt-7INJ7YRU.js} +4 -2
- package/dist/prompt-7INJ7YRU.js.map +1 -0
- package/dist/prompts/init-prompt.txt +563 -91
- package/dist/prompts/issue-prompt.txt +27 -27
- package/dist/{rebase-KBWFDZCN.js → rebase-Q7GMM7EI.js} +6 -6
- package/dist/{remote-GJEZWRCC.js → remote-VUNCQZ6J.js} +5 -2
- package/dist/remote-VUNCQZ6J.js.map +1 -0
- package/dist/{run-4SVQ3WEU.js → run-PAWJJCSX.js} +39 -36
- package/dist/run-PAWJJCSX.js.map +1 -0
- package/dist/schema/settings.schema.json +74 -0
- package/dist/{terminal-3D6TUAKJ.js → terminal-BIRBZ4AZ.js} +2 -2
- package/dist/terminal-BIRBZ4AZ.js.map +1 -0
- package/dist/{test-git-MKZATGZN.js → test-git-3WDLNQCA.js} +3 -3
- package/dist/{test-prefix-ZNLWDI3K.js → test-prefix-EVGAWAJW.js} +3 -3
- package/dist/{test-tabs-JRKY3QMM.js → test-tabs-RXDBZ6J7.js} +2 -2
- package/dist/{test-webserver-M2I3EV4J.js → test-webserver-DAHONWCS.js} +4 -4
- package/dist/test-webserver-DAHONWCS.js.map +1 -0
- package/package.json +2 -1
- package/dist/ClaudeContextManager-JKR4WGNU.js +0 -13
- package/dist/ClaudeService-55DQGB7T.js +0 -12
- package/dist/GitHubService-LWP4GKGH.js +0 -11
- package/dist/LoomLauncher-UMMLPIZO.js.map +0 -1
- package/dist/add-issue-X56V3XPB.js +0 -69
- package/dist/add-issue-X56V3XPB.js.map +0 -1
- package/dist/chunk-BLCTGFZN.js.map +0 -1
- package/dist/chunk-ELFT36PV.js.map +0 -1
- package/dist/chunk-FXV24OYZ.js.map +0 -1
- package/dist/chunk-H4E4THUZ.js +0 -55
- package/dist/chunk-H4E4THUZ.js.map +0 -1
- package/dist/chunk-H5LDRGVK.js +0 -642
- package/dist/chunk-H5LDRGVK.js.map +0 -1
- package/dist/chunk-JXQXSC45.js.map +0 -1
- package/dist/chunk-KOCQAD2E.js.map +0 -1
- package/dist/chunk-PR7FKQBG.js.map +0 -1
- package/dist/chunk-PVAVNJKS.js.map +0 -1
- package/dist/chunk-Q2KYPAH2.js +0 -545
- package/dist/chunk-Q2KYPAH2.js.map +0 -1
- package/dist/chunk-RSRO7564.js.map +0 -1
- package/dist/chunk-SPYPLHMK.js.map +0 -1
- package/dist/chunk-VCMMAFXQ.js +0 -54
- package/dist/chunk-VCMMAFXQ.js.map +0 -1
- package/dist/chunk-VVH3ANF2.js.map +0 -1
- package/dist/chunk-VYQLLHZ7.js +0 -239
- package/dist/chunk-VYQLLHZ7.js.map +0 -1
- package/dist/chunk-ZMNQBJUI.js.map +0 -1
- package/dist/chunk-ZWXJBSUW.js.map +0 -1
- package/dist/cleanup-ZHROIBSQ.js.map +0 -1
- package/dist/enhance-VGWUX474.js +0 -176
- package/dist/enhance-VGWUX474.js.map +0 -1
- package/dist/finish-QJSK6Z7J.js +0 -1355
- package/dist/finish-QJSK6Z7J.js.map +0 -1
- package/dist/ignite-HICLZEYU.js.map +0 -1
- package/dist/init-UMKNHNV5.js.map +0 -1
- package/dist/mcp/chunk-6SDFJ42P.js +0 -62
- package/dist/mcp/chunk-6SDFJ42P.js.map +0 -1
- package/dist/mcp/claude-YHHHLSXH.js +0 -249
- package/dist/mcp/claude-YHHHLSXH.js.map +0 -1
- package/dist/mcp/color-QS5BFCNN.js +0 -168
- package/dist/mcp/color-QS5BFCNN.js.map +0 -1
- package/dist/mcp/github-comment-server.js +0 -168
- package/dist/mcp/github-comment-server.js.map +0 -1
- package/dist/mcp/terminal-SDCMDVD7.js +0 -202
- package/dist/mcp/terminal-SDCMDVD7.js.map +0 -1
- package/dist/open-ETZUFSE4.js.map +0 -1
- package/dist/run-4SVQ3WEU.js.map +0 -1
- package/dist/start-CT2ZEFP2.js +0 -983
- package/dist/start-CT2ZEFP2.js.map +0 -1
- package/dist/test-webserver-M2I3EV4J.js.map +0 -1
- /package/dist/{ClaudeContextManager-JKR4WGNU.js.map → BranchNamingService-3OQPRSWT.js.map} +0 -0
- /package/dist/{ClaudeService-55DQGB7T.js.map → ClaudeContextManager-MUQSDY2E.js.map} +0 -0
- /package/dist/{GitHubService-LWP4GKGH.js.map → ClaudeService-HG4VQ7AW.js.map} +0 -0
- /package/dist/{PromptTemplateManager-WII75TKH.js.map → GitHubService-EBOETDIW.js.map} +0 -0
- /package/dist/{SettingsManager-SKLUVE3K.js.map → ProjectCapabilityDetector-34LU7JJ4.js.map} +0 -0
- /package/dist/{claude-7LUVDZZ4.js.map → PromptTemplateManager-A52RUAMS.js.map} +0 -0
- /package/dist/{git-OUYMVYJX.js.map → SettingsManager-WHHFGSL7.js.map} +0 -0
- /package/dist/{neon-helpers-ZVIRPKCI.js.map → SettingsMigrationManager-AGIIIPDQ.js.map} +0 -0
- /package/dist/{chunk-DEPYQRRB.js.map → chunk-C5QCTEQK.js.map} +0 -0
- /package/dist/{chunk-CWR2SANQ.js.map → chunk-EBISESAP.js.map} +0 -0
- /package/dist/{SettingsMigrationManager-MTQIMI54.js.map → chunk-KLBYVHPK.js.map} +0 -0
- /package/dist/{chunk-USVVV3FP.js.map → chunk-MKWYLDFK.js.map} +0 -0
- /package/dist/{chunk-GZP4UGGM.js.map → chunk-ZM3CFL5L.js.map} +0 -0
- /package/dist/{prompt-ANTQWHUF.js.map → claude-GOP6PFC7.js.map} +0 -0
- /package/dist/{remote-GJEZWRCC.js.map → git-TDXKRTXM.js.map} +0 -0
- /package/dist/{terminal-3D6TUAKJ.js.map → neon-helpers-WPUACUVC.js.map} +0 -0
- /package/dist/{rebase-KBWFDZCN.js.map → rebase-Q7GMM7EI.js.map} +0 -0
- /package/dist/{test-git-MKZATGZN.js.map → test-git-3WDLNQCA.js.map} +0 -0
- /package/dist/{test-prefix-ZNLWDI3K.js.map → test-prefix-EVGAWAJW.js.map} +0 -0
- /package/dist/{test-tabs-JRKY3QMM.js.map → test-tabs-RXDBZ6J7.js.map} +0 -0
package/dist/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";
|
|
@@ -175,11 +180,24 @@ var init_SettingsManager = __esm({
|
|
|
175
180
|
startAiAgent: z.boolean().default(true).describe("Launch Claude AI 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 AI 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,28 +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)"),
|
|
261
|
+
github: z.object({
|
|
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.")
|
|
268
|
+
}).optional()
|
|
269
|
+
}).optional().describe("Issue management configuration"),
|
|
270
|
+
mergeBehavior: z.object({
|
|
271
|
+
mode: z.enum(["local", "github-pr"]).default("local"),
|
|
272
|
+
remote: z.string().optional()
|
|
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)"),
|
|
231
317
|
github: z.object({
|
|
232
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.")
|
|
233
324
|
}).optional()
|
|
234
|
-
}).optional().describe("Issue management configuration")
|
|
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
|
+
)
|
|
235
337
|
});
|
|
236
338
|
SettingsManager = class {
|
|
237
339
|
/**
|
|
238
|
-
* Load settings from
|
|
239
|
-
*
|
|
240
|
-
*
|
|
241
|
-
*
|
|
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)
|
|
242
347
|
*/
|
|
243
348
|
async loadSettings(projectRoot, cliOverrides) {
|
|
244
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));
|
|
245
353
|
const baseSettings = await this.loadSettingsFile(root, "settings.json");
|
|
246
354
|
const baseSettingsPath = path.join(root, ".iloom", "settings.json");
|
|
247
355
|
logger.debug(`\u{1F4C4} Base settings from ${baseSettingsPath}:`, JSON.stringify(baseSettings, null, 2));
|
|
248
356
|
const localSettings = await this.loadSettingsFile(root, "settings.local.json");
|
|
249
357
|
const localSettingsPath = path.join(root, ".iloom", "settings.local.json");
|
|
250
358
|
logger.debug(`\u{1F4C4} Local settings from ${localSettingsPath}:`, JSON.stringify(localSettings, null, 2));
|
|
251
|
-
let merged = this.mergeSettings(baseSettings, localSettings);
|
|
252
|
-
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));
|
|
253
361
|
if (cliOverrides && Object.keys(cliOverrides).length > 0) {
|
|
254
362
|
logger.debug("\u2699\uFE0F CLI overrides to apply:", JSON.stringify(cliOverrides, null, 2));
|
|
255
363
|
merged = this.mergeSettings(merged, cliOverrides);
|
|
@@ -281,6 +389,7 @@ Note: CLI overrides were applied. Check your --set arguments.`);
|
|
|
281
389
|
/**
|
|
282
390
|
* Load and parse a single settings file
|
|
283
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
|
|
284
393
|
*/
|
|
285
394
|
async loadSettingsFile(projectRoot, filename) {
|
|
286
395
|
const settingsPath = path.join(projectRoot, ".iloom", filename);
|
|
@@ -294,16 +403,13 @@ Note: CLI overrides were applied. Check your --set arguments.`);
|
|
|
294
403
|
`Failed to parse settings file at ${settingsPath}: ${error instanceof Error ? error.message : "Invalid JSON"}`
|
|
295
404
|
);
|
|
296
405
|
}
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
const errorMsg = this.formatAllZodErrors(error, filename);
|
|
303
|
-
throw errorMsg;
|
|
304
|
-
}
|
|
305
|
-
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
|
+
);
|
|
306
411
|
}
|
|
412
|
+
return parsed;
|
|
307
413
|
} catch (error) {
|
|
308
414
|
if (error.code === "ENOENT") {
|
|
309
415
|
logger.debug(`No settings file found at ${settingsPath}, using defaults`);
|
|
@@ -357,6 +463,57 @@ ${errorMessages.join("\n")}`
|
|
|
357
463
|
getProjectRoot(projectRoot) {
|
|
358
464
|
return projectRoot ?? process.cwd();
|
|
359
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
|
+
}
|
|
360
517
|
/**
|
|
361
518
|
* Get effective protected branches list with mainBranch always included
|
|
362
519
|
*
|
|
@@ -392,7 +549,7 @@ __export(terminal_exports, {
|
|
|
392
549
|
openMultipleTerminalWindows: () => openMultipleTerminalWindows,
|
|
393
550
|
openTerminalWindow: () => openTerminalWindow
|
|
394
551
|
});
|
|
395
|
-
import { execa as
|
|
552
|
+
import { execa as execa3 } from "execa";
|
|
396
553
|
import { existsSync } from "fs";
|
|
397
554
|
function detectPlatform() {
|
|
398
555
|
const platform = process.platform;
|
|
@@ -413,10 +570,13 @@ async function openTerminalWindow(options) {
|
|
|
413
570
|
`Terminal window launching not yet supported on ${platform}. Currently only macOS is supported.`
|
|
414
571
|
);
|
|
415
572
|
}
|
|
416
|
-
const
|
|
573
|
+
const hasITerm2 = await detectITerm2();
|
|
574
|
+
const applescript = hasITerm2 ? buildITerm2SingleTabScript(options) : buildAppleScript(options);
|
|
417
575
|
try {
|
|
418
|
-
await
|
|
419
|
-
|
|
576
|
+
await execa3("osascript", ["-e", applescript]);
|
|
577
|
+
if (!hasITerm2) {
|
|
578
|
+
await execa3("osascript", ["-e", 'tell application "Terminal" to activate']);
|
|
579
|
+
}
|
|
420
580
|
} catch (error) {
|
|
421
581
|
throw new Error(
|
|
422
582
|
`Failed to open terminal window: ${error instanceof Error ? error.message : "Unknown error"}`
|
|
@@ -465,6 +625,28 @@ function escapePathForAppleScript(path5) {
|
|
|
465
625
|
function escapeForAppleScript(command) {
|
|
466
626
|
return command.replace(/\\/g, "\\\\").replace(/"/g, '\\"');
|
|
467
627
|
}
|
|
628
|
+
function buildITerm2SingleTabScript(options) {
|
|
629
|
+
const command = buildCommandSequence(options);
|
|
630
|
+
let script = 'tell application id "com.googlecode.iterm2"\n';
|
|
631
|
+
script += " create window with default profile\n";
|
|
632
|
+
script += " set s1 to current session of current window\n\n";
|
|
633
|
+
if (options.backgroundColor) {
|
|
634
|
+
const { r, g, b } = options.backgroundColor;
|
|
635
|
+
script += ` set background color of s1 to {${Math.round(r * 257)}, ${Math.round(g * 257)}, ${Math.round(b * 257)}}
|
|
636
|
+
`;
|
|
637
|
+
}
|
|
638
|
+
script += ` tell s1 to write text "${escapeForAppleScript(command)}"
|
|
639
|
+
|
|
640
|
+
`;
|
|
641
|
+
if (options.title) {
|
|
642
|
+
script += ` set name of s1 to "${escapeForAppleScript(options.title)}"
|
|
643
|
+
|
|
644
|
+
`;
|
|
645
|
+
}
|
|
646
|
+
script += " activate\n";
|
|
647
|
+
script += "end tell";
|
|
648
|
+
return script;
|
|
649
|
+
}
|
|
468
650
|
function buildCommandSequence(options) {
|
|
469
651
|
const {
|
|
470
652
|
workspacePath,
|
|
@@ -561,7 +743,7 @@ async function openMultipleTerminalWindows(optionsArray) {
|
|
|
561
743
|
if (hasITerm2) {
|
|
562
744
|
const applescript = buildITerm2MultiTabScript(optionsArray);
|
|
563
745
|
try {
|
|
564
|
-
await
|
|
746
|
+
await execa3("osascript", ["-e", applescript]);
|
|
565
747
|
} catch (error) {
|
|
566
748
|
throw new Error(
|
|
567
749
|
`Failed to open iTerm2 window: ${error instanceof Error ? error.message : "Unknown error"}`
|
|
@@ -600,7 +782,7 @@ __export(color_exports, {
|
|
|
600
782
|
rgbToHex: () => rgbToHex,
|
|
601
783
|
saturateColor: () => saturateColor
|
|
602
784
|
});
|
|
603
|
-
import { createHash } from "crypto";
|
|
785
|
+
import { createHash as createHash2 } from "crypto";
|
|
604
786
|
function getColorPalette() {
|
|
605
787
|
return [
|
|
606
788
|
// First 10 colors preserved for backward compatibility
|
|
@@ -707,7 +889,7 @@ function hexToRgb(hex) {
|
|
|
707
889
|
return { r, g, b };
|
|
708
890
|
}
|
|
709
891
|
function generateColorFromBranchName(branchName) {
|
|
710
|
-
const hash =
|
|
892
|
+
const hash = createHash2("sha256").update(branchName).digest("hex");
|
|
711
893
|
const hashPrefix = hash.slice(0, 8);
|
|
712
894
|
const palette = getColorPalette();
|
|
713
895
|
const hashAsInt = parseInt(hashPrefix, 16);
|
|
@@ -759,349 +941,98 @@ var init_color = __esm({
|
|
|
759
941
|
}
|
|
760
942
|
});
|
|
761
943
|
|
|
762
|
-
// src/
|
|
763
|
-
var
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
import
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
await execa3("command", ["-v", "claude"], {
|
|
777
|
-
shell: true,
|
|
778
|
-
timeout: 5e3
|
|
779
|
-
});
|
|
780
|
-
return true;
|
|
781
|
-
} catch (error) {
|
|
782
|
-
logger.debug("Claude CLI not available", { error });
|
|
783
|
-
return false;
|
|
784
|
-
}
|
|
785
|
-
}
|
|
786
|
-
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) {
|
|
787
958
|
try {
|
|
788
|
-
const result = await
|
|
789
|
-
|
|
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()
|
|
790
965
|
});
|
|
791
|
-
return result.stdout
|
|
966
|
+
return result.stdout;
|
|
792
967
|
} catch (error) {
|
|
793
|
-
|
|
794
|
-
|
|
968
|
+
const execaError = error;
|
|
969
|
+
const stderr = execaError.stderr ?? execaError.message ?? "Unknown Git error";
|
|
970
|
+
throw new Error(`Git command failed: ${stderr}`);
|
|
795
971
|
}
|
|
796
972
|
}
|
|
797
|
-
function
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
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++;
|
|
808
1000
|
continue;
|
|
809
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++;
|
|
810
1023
|
}
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
args.push("--output-format", "stream-json");
|
|
822
|
-
args.push("--verbose");
|
|
823
|
-
}
|
|
824
|
-
if (model) {
|
|
825
|
-
args.push("--model", model);
|
|
826
|
-
}
|
|
827
|
-
if (permissionMode && permissionMode !== "default") {
|
|
828
|
-
args.push("--permission-mode", permissionMode);
|
|
829
|
-
}
|
|
830
|
-
if (addDir) {
|
|
831
|
-
args.push("--add-dir", addDir);
|
|
832
|
-
}
|
|
833
|
-
args.push("--add-dir", "/tmp");
|
|
834
|
-
if (appendSystemPrompt) {
|
|
835
|
-
args.push("--append-system-prompt", appendSystemPrompt);
|
|
836
|
-
}
|
|
837
|
-
if (mcpConfig && mcpConfig.length > 0) {
|
|
838
|
-
for (const config of mcpConfig) {
|
|
839
|
-
args.push("--mcp-config", JSON.stringify(config));
|
|
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;
|
|
840
1034
|
}
|
|
841
|
-
|
|
842
|
-
if (allowedTools && allowedTools.length > 0) {
|
|
843
|
-
args.push("--allowed-tools", ...allowedTools);
|
|
844
|
-
}
|
|
845
|
-
if (disallowedTools && disallowedTools.length > 0) {
|
|
846
|
-
args.push("--disallowed-tools", ...disallowedTools);
|
|
847
|
-
}
|
|
848
|
-
if (agents) {
|
|
849
|
-
args.push("--agents", JSON.stringify(agents));
|
|
850
|
-
}
|
|
851
|
-
try {
|
|
852
|
-
if (headless) {
|
|
853
|
-
const isDebugMode = logger.isDebugEnabled();
|
|
854
|
-
const execaOptions = {
|
|
855
|
-
input: prompt,
|
|
856
|
-
timeout: 0,
|
|
857
|
-
// Disable timeout for long responses
|
|
858
|
-
...addDir && { cwd: addDir },
|
|
859
|
-
// Run Claude in the worktree directory
|
|
860
|
-
verbose: isDebugMode,
|
|
861
|
-
...isDebugMode && { stdio: ["pipe", "pipe", "pipe"] }
|
|
862
|
-
// Enable streaming in debug mode
|
|
863
|
-
};
|
|
864
|
-
const subprocess = execa3("claude", args, execaOptions);
|
|
865
|
-
const isJsonStreamFormat = args.includes("--output-format") && args.includes("stream-json");
|
|
866
|
-
let outputBuffer = "";
|
|
867
|
-
let isStreaming = false;
|
|
868
|
-
let isFirstProgress = true;
|
|
869
|
-
if (subprocess.stdout && typeof subprocess.stdout.on === "function") {
|
|
870
|
-
isStreaming = true;
|
|
871
|
-
subprocess.stdout.on("data", (chunk) => {
|
|
872
|
-
const text = chunk.toString();
|
|
873
|
-
outputBuffer += text;
|
|
874
|
-
if (isDebugMode) {
|
|
875
|
-
process.stdout.write(text);
|
|
876
|
-
} else {
|
|
877
|
-
if (isFirstProgress) {
|
|
878
|
-
process.stdout.write("\u{1F916} .");
|
|
879
|
-
isFirstProgress = false;
|
|
880
|
-
} else {
|
|
881
|
-
process.stdout.write(".");
|
|
882
|
-
}
|
|
883
|
-
}
|
|
884
|
-
});
|
|
885
|
-
}
|
|
886
|
-
const result = await subprocess;
|
|
887
|
-
if (isStreaming) {
|
|
888
|
-
const rawOutput = outputBuffer.trim();
|
|
889
|
-
if (!isDebugMode) {
|
|
890
|
-
process.stdout.write("\n");
|
|
891
|
-
}
|
|
892
|
-
return isJsonStreamFormat ? parseJsonStreamOutput(rawOutput) : rawOutput;
|
|
893
|
-
} else {
|
|
894
|
-
if (isDebugMode) {
|
|
895
|
-
process.stdout.write(result.stdout);
|
|
896
|
-
if (result.stdout && !result.stdout.endsWith("\n")) {
|
|
897
|
-
process.stdout.write("\n");
|
|
898
|
-
}
|
|
899
|
-
} else {
|
|
900
|
-
process.stdout.write("\u{1F916} .");
|
|
901
|
-
process.stdout.write("\n");
|
|
902
|
-
}
|
|
903
|
-
const rawOutput = result.stdout.trim();
|
|
904
|
-
return isJsonStreamFormat ? parseJsonStreamOutput(rawOutput) : rawOutput;
|
|
905
|
-
}
|
|
906
|
-
} else {
|
|
907
|
-
await execa3("claude", [...args, "--", prompt], {
|
|
908
|
-
...addDir && { cwd: addDir },
|
|
909
|
-
stdio: "inherit",
|
|
910
|
-
// Let user interact directly in current terminal
|
|
911
|
-
timeout: 0,
|
|
912
|
-
// Disable timeout
|
|
913
|
-
verbose: logger.isDebugEnabled()
|
|
914
|
-
});
|
|
915
|
-
return;
|
|
916
|
-
}
|
|
917
|
-
} catch (error) {
|
|
918
|
-
const execaError = error;
|
|
919
|
-
const errorMessage = execaError.stderr ?? execaError.message ?? "Unknown Claude CLI error";
|
|
920
|
-
throw new Error(`Claude CLI error: ${errorMessage}`);
|
|
921
|
-
}
|
|
922
|
-
}
|
|
923
|
-
async function launchClaudeInNewTerminalWindow(_prompt, options) {
|
|
924
|
-
const { workspacePath, branchName, oneShot = "default", port, setArguments, executablePath } = options;
|
|
925
|
-
if (!workspacePath) {
|
|
926
|
-
throw new Error("workspacePath is required for terminal window launch");
|
|
927
|
-
}
|
|
928
|
-
const { openTerminalWindow: openTerminalWindow2 } = await Promise.resolve().then(() => (init_terminal(), terminal_exports));
|
|
929
|
-
const executable = executablePath ?? "iloom";
|
|
930
|
-
let launchCommand = `${executable} spin`;
|
|
931
|
-
if (oneShot !== "default") {
|
|
932
|
-
launchCommand += ` --one-shot=${oneShot}`;
|
|
933
|
-
}
|
|
934
|
-
if (setArguments && setArguments.length > 0) {
|
|
935
|
-
for (const setArg of setArguments) {
|
|
936
|
-
launchCommand += ` --set ${setArg}`;
|
|
937
|
-
}
|
|
938
|
-
}
|
|
939
|
-
let backgroundColor;
|
|
940
|
-
if (branchName) {
|
|
941
|
-
try {
|
|
942
|
-
const { generateColorFromBranchName: generateColorFromBranchName2 } = await Promise.resolve().then(() => (init_color(), color_exports));
|
|
943
|
-
const colorData = generateColorFromBranchName2(branchName);
|
|
944
|
-
backgroundColor = colorData.rgb;
|
|
945
|
-
} catch (error) {
|
|
946
|
-
logger.warn(
|
|
947
|
-
`Failed to generate terminal color: ${error instanceof Error ? error.message : "Unknown error"}`
|
|
948
|
-
);
|
|
949
|
-
}
|
|
950
|
-
}
|
|
951
|
-
const hasEnvFile = existsSync2(join(workspacePath, ".env"));
|
|
952
|
-
await openTerminalWindow2({
|
|
953
|
-
workspacePath,
|
|
954
|
-
command: launchCommand,
|
|
955
|
-
...backgroundColor && { backgroundColor },
|
|
956
|
-
includeEnvSetup: hasEnvFile,
|
|
957
|
-
// source .env only if it exists
|
|
958
|
-
...port !== void 0 && { port, includePortExport: true }
|
|
959
|
-
});
|
|
960
|
-
}
|
|
961
|
-
async function generateBranchName(issueTitle, issueNumber, model = "haiku") {
|
|
962
|
-
try {
|
|
963
|
-
const isAvailable = await detectClaudeCli();
|
|
964
|
-
if (!isAvailable) {
|
|
965
|
-
logger.warn("Claude CLI not available, using fallback branch name");
|
|
966
|
-
return `feat/issue-${issueNumber}`;
|
|
967
|
-
}
|
|
968
|
-
logger.debug("Generating branch name with Claude", { issueNumber, issueTitle });
|
|
969
|
-
const prompt = `<Task>
|
|
970
|
-
Generate a git branch name for the following issue:
|
|
971
|
-
<Issue>
|
|
972
|
-
<IssueNumber>${issueNumber}</IssueNumber>
|
|
973
|
-
<IssueTitle>${issueTitle}</IssueTitle>
|
|
974
|
-
</Issue>
|
|
975
|
-
|
|
976
|
-
<Requirements>
|
|
977
|
-
<IssueNumber>Must use this exact issue number: ${issueNumber}</IssueNumber>
|
|
978
|
-
<Format>Format must be: {prefix}/issue-${issueNumber}-{description}</Format>
|
|
979
|
-
<Prefix>Prefix must be one of: feat, fix, docs, refactor, test, chore</Prefix>
|
|
980
|
-
<MaxLength>Maximum 50 characters total</MaxLength>
|
|
981
|
-
<Characters>Only lowercase letters, numbers, and hyphens allowed</Characters>
|
|
982
|
-
<Output>Reply with ONLY the branch name, nothing else</Output>
|
|
983
|
-
</Requirements>
|
|
984
|
-
</Task>`;
|
|
985
|
-
logger.debug("Sending prompt to Claude", { prompt });
|
|
986
|
-
const result = await launchClaude(prompt, {
|
|
987
|
-
model,
|
|
988
|
-
headless: true
|
|
989
|
-
});
|
|
990
|
-
const branchName = result.trim();
|
|
991
|
-
logger.debug("Claude returned branch name", { branchName, issueNumber });
|
|
992
|
-
if (!branchName || !isValidBranchName(branchName, issueNumber)) {
|
|
993
|
-
logger.warn("Invalid branch name from Claude, using fallback", { branchName });
|
|
994
|
-
return `feat/issue-${issueNumber}`;
|
|
995
|
-
}
|
|
996
|
-
return branchName;
|
|
997
|
-
} catch (error) {
|
|
998
|
-
logger.warn("Failed to generate branch name with Claude", { error });
|
|
999
|
-
return `feat/issue-${issueNumber}`;
|
|
1000
|
-
}
|
|
1001
|
-
}
|
|
1002
|
-
function isValidBranchName(name, issueNumber) {
|
|
1003
|
-
const pattern = new RegExp(`^(feat|fix|docs|refactor|test|chore)/issue-${issueNumber}-[a-z0-9-]+$`);
|
|
1004
|
-
return pattern.test(name) && name.length <= 50;
|
|
1005
|
-
}
|
|
1006
|
-
var init_claude = __esm({
|
|
1007
|
-
"src/utils/claude.ts"() {
|
|
1008
|
-
"use strict";
|
|
1009
|
-
init_logger();
|
|
1010
|
-
}
|
|
1011
|
-
});
|
|
1012
|
-
|
|
1013
|
-
// src/lib/WorkspaceManager.ts
|
|
1014
|
-
var WorkspaceManager = class {
|
|
1015
|
-
// TODO: Implement in Issue #6
|
|
1016
|
-
};
|
|
1017
|
-
|
|
1018
|
-
// src/lib/GitWorktreeManager.ts
|
|
1019
|
-
import path3 from "path";
|
|
1020
|
-
import fs from "fs-extra";
|
|
1021
|
-
|
|
1022
|
-
// src/utils/git.ts
|
|
1023
|
-
init_logger();
|
|
1024
|
-
import path2 from "path";
|
|
1025
|
-
import { execa } from "execa";
|
|
1026
|
-
async function executeGitCommand(args, options) {
|
|
1027
|
-
try {
|
|
1028
|
-
const result = await execa("git", args, {
|
|
1029
|
-
cwd: (options == null ? void 0 : options.cwd) ?? process.cwd(),
|
|
1030
|
-
timeout: (options == null ? void 0 : options.timeout) ?? 3e4,
|
|
1031
|
-
encoding: "utf8",
|
|
1032
|
-
stdio: (options == null ? void 0 : options.stdio) ?? "pipe",
|
|
1033
|
-
verbose: logger.isDebugEnabled()
|
|
1034
|
-
});
|
|
1035
|
-
return result.stdout;
|
|
1036
|
-
} catch (error) {
|
|
1037
|
-
const execaError = error;
|
|
1038
|
-
const stderr = execaError.stderr ?? execaError.message ?? "Unknown Git error";
|
|
1039
|
-
throw new Error(`Git command failed: ${stderr}`);
|
|
1040
|
-
}
|
|
1041
|
-
}
|
|
1042
|
-
function parseWorktreeList(output, defaultBranch) {
|
|
1043
|
-
var _a, _b;
|
|
1044
|
-
const worktrees = [];
|
|
1045
|
-
const lines = output.trim().split("\n");
|
|
1046
|
-
let i = 0;
|
|
1047
|
-
while (i < lines.length) {
|
|
1048
|
-
const pathLine = lines[i];
|
|
1049
|
-
if (!(pathLine == null ? void 0 : pathLine.startsWith("worktree "))) {
|
|
1050
|
-
i++;
|
|
1051
|
-
continue;
|
|
1052
|
-
}
|
|
1053
|
-
const pathMatch = pathLine.match(/^worktree (.+)$/);
|
|
1054
|
-
if (!pathMatch) {
|
|
1055
|
-
i++;
|
|
1056
|
-
continue;
|
|
1057
|
-
}
|
|
1058
|
-
let branch = "";
|
|
1059
|
-
let commit = "";
|
|
1060
|
-
let detached = false;
|
|
1061
|
-
let bare = false;
|
|
1062
|
-
let locked = false;
|
|
1063
|
-
let lockReason;
|
|
1064
|
-
i++;
|
|
1065
|
-
while (i < lines.length && !((_a = lines[i]) == null ? void 0 : _a.startsWith("worktree "))) {
|
|
1066
|
-
const line = (_b = lines[i]) == null ? void 0 : _b.trim();
|
|
1067
|
-
if (!line) {
|
|
1068
|
-
i++;
|
|
1069
|
-
continue;
|
|
1070
|
-
}
|
|
1071
|
-
if (line === "bare") {
|
|
1072
|
-
bare = true;
|
|
1073
|
-
branch = defaultBranch ?? "main";
|
|
1074
|
-
} else if (line === "detached") {
|
|
1075
|
-
detached = true;
|
|
1076
|
-
branch = "HEAD";
|
|
1077
|
-
} else if (line.startsWith("locked")) {
|
|
1078
|
-
locked = true;
|
|
1079
|
-
const lockMatch = line.match(/^locked (.+)$/);
|
|
1080
|
-
lockReason = lockMatch == null ? void 0 : lockMatch[1];
|
|
1081
|
-
branch = branch || "unknown";
|
|
1082
|
-
} else if (line.startsWith("HEAD ")) {
|
|
1083
|
-
const commitMatch = line.match(/^HEAD ([a-f0-9]+)/);
|
|
1084
|
-
if (commitMatch) {
|
|
1085
|
-
commit = commitMatch[1] ?? "";
|
|
1086
|
-
}
|
|
1087
|
-
} else if (line.startsWith("branch ")) {
|
|
1088
|
-
const branchMatch = line.match(/^branch refs\/heads\/(.+)$/);
|
|
1089
|
-
branch = (branchMatch == null ? void 0 : branchMatch[1]) ?? line.replace("branch ", "");
|
|
1090
|
-
}
|
|
1091
|
-
i++;
|
|
1092
|
-
}
|
|
1093
|
-
const worktree = {
|
|
1094
|
-
path: pathMatch[1] ?? "",
|
|
1095
|
-
branch,
|
|
1096
|
-
commit,
|
|
1097
|
-
bare,
|
|
1098
|
-
detached,
|
|
1099
|
-
locked
|
|
1100
|
-
};
|
|
1101
|
-
if (lockReason !== void 0) {
|
|
1102
|
-
worktree.lockReason = lockReason;
|
|
1103
|
-
}
|
|
1104
|
-
worktrees.push(worktree);
|
|
1035
|
+
worktrees.push(worktree);
|
|
1105
1036
|
}
|
|
1106
1037
|
return worktrees;
|
|
1107
1038
|
}
|
|
@@ -1144,6 +1075,28 @@ function extractPRNumber(branchName) {
|
|
|
1144
1075
|
}
|
|
1145
1076
|
return null;
|
|
1146
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
|
|
1093
|
+
];
|
|
1094
|
+
for (const pattern of legacyPatterns) {
|
|
1095
|
+
const match = branchName.match(pattern);
|
|
1096
|
+
if (match == null ? void 0 : match[1]) return match[1];
|
|
1097
|
+
}
|
|
1098
|
+
return null;
|
|
1099
|
+
}
|
|
1147
1100
|
function isWorktreePath(path5) {
|
|
1148
1101
|
const worktreePatterns = [
|
|
1149
1102
|
/\/worktrees?\//i,
|
|
@@ -1737,7 +1690,7 @@ var GitWorktreeManager = class {
|
|
|
1737
1690
|
*/
|
|
1738
1691
|
async findWorktreeForIssue(issueNumber) {
|
|
1739
1692
|
const worktrees = await this.listWorktrees({ porcelain: true });
|
|
1740
|
-
const pattern = new RegExp(`(?:^|[/_-])issue-${issueNumber}(
|
|
1693
|
+
const pattern = new RegExp(`(?:^|[/_-])issue-${issueNumber}(?:-|__|$)`);
|
|
1741
1694
|
return worktrees.find((wt) => pattern.test(wt.branch)) ?? null;
|
|
1742
1695
|
}
|
|
1743
1696
|
/**
|
|
@@ -1811,9 +1764,9 @@ var GitHubError = class extends Error {
|
|
|
1811
1764
|
|
|
1812
1765
|
// src/utils/github.ts
|
|
1813
1766
|
init_logger();
|
|
1814
|
-
import { execa as
|
|
1767
|
+
import { execa as execa2 } from "execa";
|
|
1815
1768
|
async function executeGhCommand(args, options) {
|
|
1816
|
-
const result = await
|
|
1769
|
+
const result = await execa2("gh", args, {
|
|
1817
1770
|
cwd: (options == null ? void 0 : options.cwd) ?? process.cwd(),
|
|
1818
1771
|
timeout: (options == null ? void 0 : options.timeout) ?? 3e4,
|
|
1819
1772
|
encoding: "utf8"
|
|
@@ -1955,21 +1908,6 @@ async function updateProjectItemField(itemId, projectId, fieldId, optionId) {
|
|
|
1955
1908
|
"json"
|
|
1956
1909
|
]);
|
|
1957
1910
|
}
|
|
1958
|
-
var SimpleBranchNameStrategy = class {
|
|
1959
|
-
async generate(issueNumber, title) {
|
|
1960
|
-
const slug = title.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "").substring(0, 20);
|
|
1961
|
-
return `feat/issue-${issueNumber}-${slug}`;
|
|
1962
|
-
}
|
|
1963
|
-
};
|
|
1964
|
-
var ClaudeBranchNameStrategy = class {
|
|
1965
|
-
constructor(claudeModel = "haiku") {
|
|
1966
|
-
this.claudeModel = claudeModel;
|
|
1967
|
-
}
|
|
1968
|
-
async generate(issueNumber, title) {
|
|
1969
|
-
const { generateBranchName: generateBranchName2 } = await Promise.resolve().then(() => (init_claude(), claude_exports));
|
|
1970
|
-
return generateBranchName2(title, issueNumber, this.claudeModel);
|
|
1971
|
-
}
|
|
1972
|
-
};
|
|
1973
1911
|
async function createIssue(title, body, options) {
|
|
1974
1912
|
const { repo, labels } = options ?? {};
|
|
1975
1913
|
logger.debug("Creating GitHub issue", { title, repo, labels });
|
|
@@ -1994,7 +1932,7 @@ async function createIssue(title, body, options) {
|
|
|
1994
1932
|
if (!repo) {
|
|
1995
1933
|
execaOptions.cwd = process.cwd();
|
|
1996
1934
|
}
|
|
1997
|
-
const result = await
|
|
1935
|
+
const result = await execa2("gh", args, execaOptions);
|
|
1998
1936
|
const urlMatch = result.stdout.trim().match(/https:\/\/github\.com\/[^/]+\/[^/]+\/issues\/(\d+)/);
|
|
1999
1937
|
if (!(urlMatch == null ? void 0 : urlMatch[1])) {
|
|
2000
1938
|
throw new Error(`Failed to parse issue URL from gh output: ${result.stdout}`);
|
|
@@ -2048,35 +1986,29 @@ async function promptConfirmation(message, defaultValue = false) {
|
|
|
2048
1986
|
// src/lib/GitHubService.ts
|
|
2049
1987
|
var GitHubService = class {
|
|
2050
1988
|
constructor(options) {
|
|
2051
|
-
|
|
2052
|
-
|
|
2053
|
-
|
|
2054
|
-
this.defaultBranchNameStrategy = new ClaudeBranchNameStrategy(
|
|
2055
|
-
options == null ? void 0 : options.claudeModel
|
|
2056
|
-
);
|
|
2057
|
-
} else {
|
|
2058
|
-
this.defaultBranchNameStrategy = new SimpleBranchNameStrategy();
|
|
2059
|
-
}
|
|
1989
|
+
// IssueTracker interface implementation
|
|
1990
|
+
this.providerName = "github";
|
|
1991
|
+
this.supportsPullRequests = true;
|
|
2060
1992
|
this.prompter = (options == null ? void 0 : options.prompter) ?? promptConfirmation;
|
|
2061
1993
|
}
|
|
2062
|
-
// Input detection
|
|
1994
|
+
// Input detection - IssueTracker interface implementation
|
|
2063
1995
|
async detectInputType(input, repo) {
|
|
2064
1996
|
const numberMatch = input.match(/^#?(\d+)$/);
|
|
2065
1997
|
if (!(numberMatch == null ? void 0 : numberMatch[1])) {
|
|
2066
|
-
return { type: "unknown",
|
|
1998
|
+
return { type: "unknown", identifier: null, rawInput: input };
|
|
2067
1999
|
}
|
|
2068
2000
|
const number = parseInt(numberMatch[1], 10);
|
|
2069
2001
|
logger.debug("Checking if input is a PR", { number });
|
|
2070
2002
|
const pr = await this.isValidPR(number, repo);
|
|
2071
2003
|
if (pr) {
|
|
2072
|
-
return { type: "pr", number, rawInput: input };
|
|
2004
|
+
return { type: "pr", identifier: number.toString(), rawInput: input };
|
|
2073
2005
|
}
|
|
2074
2006
|
logger.debug("Checking if input is an issue", { number });
|
|
2075
2007
|
const issue = await this.isValidIssue(number, repo);
|
|
2076
2008
|
if (issue) {
|
|
2077
|
-
return { type: "issue", number, rawInput: input };
|
|
2009
|
+
return { type: "issue", identifier: number.toString(), rawInput: input };
|
|
2078
2010
|
}
|
|
2079
|
-
return { type: "unknown",
|
|
2011
|
+
return { type: "unknown", identifier: null, rawInput: input };
|
|
2080
2012
|
}
|
|
2081
2013
|
// Issue fetching with validation
|
|
2082
2014
|
async fetchIssue(issueNumber, repo) {
|
|
@@ -2170,17 +2102,6 @@ var GitHubService = class {
|
|
|
2170
2102
|
}
|
|
2171
2103
|
}
|
|
2172
2104
|
}
|
|
2173
|
-
// Branch name generation using strategy pattern
|
|
2174
|
-
async generateBranchName(options) {
|
|
2175
|
-
const { issueNumber, title, strategy } = options;
|
|
2176
|
-
const nameStrategy = strategy ?? this.defaultBranchNameStrategy;
|
|
2177
|
-
logger.debug("Generating branch name", {
|
|
2178
|
-
issueNumber,
|
|
2179
|
-
title,
|
|
2180
|
-
strategy: nameStrategy.constructor.name
|
|
2181
|
-
});
|
|
2182
|
-
return nameStrategy.generate(issueNumber, title);
|
|
2183
|
-
}
|
|
2184
2105
|
// Issue creation
|
|
2185
2106
|
async createIssue(title, body, repository, labels) {
|
|
2186
2107
|
return createIssue(title, body, { repo: repository, labels });
|
|
@@ -2315,33 +2236,392 @@ State: ${entity.state}`;
|
|
|
2315
2236
|
async promptUserConfirmation(message) {
|
|
2316
2237
|
return this.prompter(message);
|
|
2317
2238
|
}
|
|
2318
|
-
// Allow setting strategy at runtime for specific operations
|
|
2319
|
-
setDefaultBranchNameStrategy(strategy) {
|
|
2320
|
-
this.defaultBranchNameStrategy = strategy;
|
|
2321
|
-
}
|
|
2322
|
-
// Get current strategy for testing
|
|
2323
|
-
getBranchNameStrategy() {
|
|
2324
|
-
return this.defaultBranchNameStrategy;
|
|
2325
|
-
}
|
|
2326
2239
|
};
|
|
2327
2240
|
|
|
2328
|
-
// src/
|
|
2329
|
-
|
|
2330
|
-
|
|
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
|
+
};
|
|
2331
2253
|
|
|
2332
|
-
// src/utils/
|
|
2254
|
+
// src/utils/linear.ts
|
|
2255
|
+
import { LinearClient } from "@linear/sdk";
|
|
2333
2256
|
init_logger();
|
|
2334
|
-
|
|
2335
|
-
|
|
2336
|
-
|
|
2337
|
-
|
|
2338
|
-
|
|
2339
|
-
|
|
2340
|
-
|
|
2341
|
-
|
|
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;
|
|
2342
2268
|
}
|
|
2343
|
-
|
|
2344
|
-
|
|
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
|
+
}
|
|
2594
|
+
}
|
|
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";
|
|
2605
|
+
}
|
|
2606
|
+
};
|
|
2607
|
+
|
|
2608
|
+
// src/lib/EnvironmentManager.ts
|
|
2609
|
+
init_logger();
|
|
2610
|
+
import fs2 from "fs-extra";
|
|
2611
|
+
|
|
2612
|
+
// src/utils/env.ts
|
|
2613
|
+
init_logger();
|
|
2614
|
+
import dotenvFlow from "dotenv-flow";
|
|
2615
|
+
function parseEnvFile(content) {
|
|
2616
|
+
const envMap = /* @__PURE__ */ new Map();
|
|
2617
|
+
const lines = content.split("\n");
|
|
2618
|
+
for (const line of lines) {
|
|
2619
|
+
const trimmedLine = line.trim();
|
|
2620
|
+
if (!trimmedLine || trimmedLine.startsWith("#")) {
|
|
2621
|
+
continue;
|
|
2622
|
+
}
|
|
2623
|
+
const cleanLine = trimmedLine.startsWith("export ") ? trimmedLine.substring(7) : trimmedLine;
|
|
2624
|
+
const equalsIndex = cleanLine.indexOf("=");
|
|
2345
2625
|
if (equalsIndex === -1) {
|
|
2346
2626
|
continue;
|
|
2347
2627
|
}
|
|
@@ -2386,12 +2666,12 @@ function isValidEnvKey(key) {
|
|
|
2386
2666
|
}
|
|
2387
2667
|
|
|
2388
2668
|
// src/utils/port.ts
|
|
2389
|
-
import { createHash
|
|
2669
|
+
import { createHash } from "crypto";
|
|
2390
2670
|
function generatePortOffsetFromBranchName(branchName) {
|
|
2391
2671
|
if (!branchName || branchName.trim().length === 0) {
|
|
2392
2672
|
throw new Error("Branch name cannot be empty");
|
|
2393
2673
|
}
|
|
2394
|
-
const hash =
|
|
2674
|
+
const hash = createHash("sha256").update(branchName).digest("hex");
|
|
2395
2675
|
const hashPrefix = hash.slice(0, 8);
|
|
2396
2676
|
const hashAsInt = parseInt(hashPrefix, 16);
|
|
2397
2677
|
const portOffset = hashAsInt % 999 + 1;
|
|
@@ -2486,6 +2766,14 @@ var EnvironmentManager = class {
|
|
|
2486
2766
|
return /* @__PURE__ */ new Map();
|
|
2487
2767
|
}
|
|
2488
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
|
+
}
|
|
2489
2777
|
/**
|
|
2490
2778
|
* Generic file copy helper that only copies if source exists
|
|
2491
2779
|
* Does not throw if source file doesn't exist - just logs and returns
|
|
@@ -2509,13 +2797,17 @@ var EnvironmentManager = class {
|
|
|
2509
2797
|
calculatePort(options) {
|
|
2510
2798
|
const basePort = options.basePort ?? 3e3;
|
|
2511
2799
|
if (options.issueNumber !== void 0) {
|
|
2512
|
-
const
|
|
2513
|
-
if (
|
|
2514
|
-
|
|
2515
|
-
|
|
2516
|
-
|
|
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;
|
|
2517
2809
|
}
|
|
2518
|
-
return
|
|
2810
|
+
return calculatePortForBranch(String(options.issueNumber), basePort);
|
|
2519
2811
|
}
|
|
2520
2812
|
if (options.prNumber !== void 0) {
|
|
2521
2813
|
const port = basePort + options.prNumber;
|
|
@@ -2635,8 +2927,9 @@ var DatabaseManager = class {
|
|
|
2635
2927
|
* @param branchName - Name of the branch to create
|
|
2636
2928
|
* @param envFilePath - Path to .env file for configuration checks
|
|
2637
2929
|
* @param cwd - Optional working directory to run commands from
|
|
2930
|
+
* @param fromBranch - Optional parent branch to create from (for child looms)
|
|
2638
2931
|
*/
|
|
2639
|
-
async createBranchIfConfigured(branchName, envFilePath, cwd) {
|
|
2932
|
+
async createBranchIfConfigured(branchName, envFilePath, cwd, fromBranch) {
|
|
2640
2933
|
if (!await this.shouldUseDatabaseBranching(envFilePath)) {
|
|
2641
2934
|
return null;
|
|
2642
2935
|
}
|
|
@@ -2658,7 +2951,7 @@ var DatabaseManager = class {
|
|
|
2658
2951
|
throw error;
|
|
2659
2952
|
}
|
|
2660
2953
|
try {
|
|
2661
|
-
const connectionString = await this.provider.createBranch(branchName,
|
|
2954
|
+
const connectionString = await this.provider.createBranch(branchName, fromBranch, cwd);
|
|
2662
2955
|
logger3.success(`Database branch ready: ${this.provider.sanitizeBranchName(branchName)}`);
|
|
2663
2956
|
return connectionString;
|
|
2664
2957
|
} catch (error) {
|
|
@@ -2745,6 +3038,24 @@ var DatabaseManager = class {
|
|
|
2745
3038
|
};
|
|
2746
3039
|
}
|
|
2747
3040
|
}
|
|
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
|
+
}
|
|
2748
3059
|
/**
|
|
2749
3060
|
* Check if .env has the configured database URL variable
|
|
2750
3061
|
* CRITICAL: If user explicitly configured a custom variable name (not default),
|
|
@@ -2788,8 +3099,187 @@ var DatabaseManager = class {
|
|
|
2788
3099
|
}
|
|
2789
3100
|
};
|
|
2790
3101
|
|
|
2791
|
-
// src/
|
|
2792
|
-
|
|
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
|
+
});
|
|
3207
|
+
}
|
|
3208
|
+
const result = await subprocess;
|
|
3209
|
+
if (isStreaming) {
|
|
3210
|
+
const rawOutput = outputBuffer.trim();
|
|
3211
|
+
if (!isDebugMode) {
|
|
3212
|
+
process.stdout.write("\n");
|
|
3213
|
+
}
|
|
3214
|
+
return isJsonStreamFormat ? parseJsonStreamOutput(rawOutput) : rawOutput;
|
|
3215
|
+
} else {
|
|
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;
|
|
3227
|
+
}
|
|
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;
|
|
3267
|
+
} catch (error) {
|
|
3268
|
+
logger.warn(
|
|
3269
|
+
`Failed to generate terminal color: ${error instanceof Error ? error.message : "Unknown error"}`
|
|
3270
|
+
);
|
|
3271
|
+
}
|
|
3272
|
+
}
|
|
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
|
+
}
|
|
2793
3283
|
|
|
2794
3284
|
// src/lib/PromptTemplateManager.ts
|
|
2795
3285
|
init_logger();
|
|
@@ -2869,6 +3359,9 @@ var PromptTemplateManager = class {
|
|
|
2869
3359
|
if (variables.SETTINGS_SCHEMA !== void 0) {
|
|
2870
3360
|
result = result.replace(/SETTINGS_SCHEMA/g, variables.SETTINGS_SCHEMA);
|
|
2871
3361
|
}
|
|
3362
|
+
if (variables.SETTINGS_GLOBAL_JSON !== void 0) {
|
|
3363
|
+
result = result.replace(/SETTINGS_GLOBAL_JSON/g, variables.SETTINGS_GLOBAL_JSON);
|
|
3364
|
+
}
|
|
2872
3365
|
if (variables.SETTINGS_JSON !== void 0) {
|
|
2873
3366
|
result = result.replace(/SETTINGS_JSON/g, variables.SETTINGS_JSON);
|
|
2874
3367
|
}
|
|
@@ -2930,6 +3423,12 @@ var PromptTemplateManager = class {
|
|
|
2930
3423
|
} else {
|
|
2931
3424
|
result = result.replace(settingsJsonRegex, "");
|
|
2932
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
|
+
}
|
|
2933
3432
|
const settingsLocalJsonRegex = /\{\{#IF SETTINGS_LOCAL_JSON\}\}(.*?)\{\{\/IF SETTINGS_LOCAL_JSON\}\}/gs;
|
|
2934
3433
|
if (variables.SETTINGS_LOCAL_JSON !== void 0 && variables.SETTINGS_LOCAL_JSON !== "") {
|
|
2935
3434
|
result = result.replace(settingsLocalJsonRegex, "$1");
|
|
@@ -3090,17 +3589,6 @@ var ClaudeService = class {
|
|
|
3090
3589
|
throw error;
|
|
3091
3590
|
}
|
|
3092
3591
|
}
|
|
3093
|
-
/**
|
|
3094
|
-
* Generate branch name with Claude, with fallback on failure
|
|
3095
|
-
*/
|
|
3096
|
-
async generateBranchNameWithFallback(issueTitle, issueNumber) {
|
|
3097
|
-
try {
|
|
3098
|
-
return await generateBranchName(issueTitle, issueNumber);
|
|
3099
|
-
} catch (error) {
|
|
3100
|
-
logger.warn("Claude branch name generation failed, using fallback", { error });
|
|
3101
|
-
return `feat/issue-${issueNumber}`;
|
|
3102
|
-
}
|
|
3103
|
-
}
|
|
3104
3592
|
};
|
|
3105
3593
|
|
|
3106
3594
|
// src/lib/ClaudeContextManager.ts
|
|
@@ -3160,17 +3648,322 @@ var ClaudeContextManager = class {
|
|
|
3160
3648
|
|
|
3161
3649
|
// src/utils/index.ts
|
|
3162
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;
|
|
3163
3952
|
export {
|
|
3164
3953
|
ClaudeContextManager,
|
|
3165
3954
|
DatabaseManager,
|
|
3166
3955
|
EnvironmentManager,
|
|
3167
3956
|
GitHubService,
|
|
3168
3957
|
GitWorktreeManager,
|
|
3958
|
+
IssueTrackerFactory,
|
|
3959
|
+
LinearMarkupConverter,
|
|
3960
|
+
TableFormatter,
|
|
3169
3961
|
WorkspaceManager,
|
|
3170
3962
|
branchExists,
|
|
3171
3963
|
createLogger,
|
|
3172
3964
|
ensureRepositoryHasCommits,
|
|
3173
3965
|
executeGitCommand,
|
|
3966
|
+
extractIssueNumber,
|
|
3174
3967
|
extractPRNumber,
|
|
3175
3968
|
findAllBranchesForIssue,
|
|
3176
3969
|
findMainWorktreePath,
|