kantban-cli 0.1.51 → 0.1.52

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.
@@ -5,10 +5,155 @@ import {
5
5
  crossSpawnOptions,
6
6
  defaultPath,
7
7
  killProcessTree,
8
+ normalizeEol,
8
9
  npxCommand,
9
10
  resolveCommand
10
11
  } from "./chunk-5ZU2OOES.js";
11
12
 
13
+ // src/lib/worktree.ts
14
+ import { execFile as defaultExecFile, execFileSync } from "child_process";
15
+ import { homedir } from "os";
16
+ import { join } from "path";
17
+ function generateWorktreeName(ticketNumber, columnName) {
18
+ const slug = columnSlug(columnName);
19
+ return `kantban-${ticketNumber}-${slug}`;
20
+ }
21
+ function columnSlug(columnName) {
22
+ return columnName.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "");
23
+ }
24
+ function renderWorktreePath(ticketNumber, columnName, pathPattern, options) {
25
+ const slug = columnSlug(columnName);
26
+ const worktreeName = generateWorktreeName(ticketNumber, columnName);
27
+ if (pathPattern) {
28
+ return pathPattern.replace(/\{ticket_number\}/g, String(ticketNumber)).replace(/\{column_slug\}/g, slug).replace(/\{worktree_name\}/g, worktreeName);
29
+ }
30
+ return join(options.defaultRoot, options.boardId, slug, String(ticketNumber));
31
+ }
32
+ function defaultWorktreeRoot() {
33
+ return join(homedir(), ".kantban", "worktrees");
34
+ }
35
+ function isPlausibleRemoteUrl(url) {
36
+ return url.includes("/") || url.includes("@") || url.includes("://");
37
+ }
38
+ function ensureWorktreeRemote(worktreePath) {
39
+ try {
40
+ const remotes = normalizeEol(execFileSync("git", ["-C", worktreePath, "remote"], {
41
+ stdio: "pipe",
42
+ encoding: "utf-8"
43
+ })).trim();
44
+ if (remotes.split("\n").includes("origin")) {
45
+ const currentUrl = normalizeEol(execFileSync("git", ["-C", worktreePath, "remote", "get-url", "origin"], {
46
+ stdio: "pipe",
47
+ encoding: "utf-8"
48
+ })).trim();
49
+ if (isPlausibleRemoteUrl(currentUrl)) return;
50
+ console.error(`[worktree] Invalid origin URL in ${worktreePath}: "${currentUrl}" \u2014 fixing`);
51
+ execFileSync("git", ["-C", worktreePath, "remote", "remove", "origin"], {
52
+ stdio: "pipe"
53
+ });
54
+ }
55
+ const originUrl = normalizeEol(execFileSync("git", ["remote", "get-url", "origin"], {
56
+ stdio: "pipe",
57
+ encoding: "utf-8"
58
+ })).trim();
59
+ if (originUrl && isPlausibleRemoteUrl(originUrl)) {
60
+ execFileSync("git", ["-C", worktreePath, "remote", "add", "origin", originUrl], {
61
+ stdio: "pipe"
62
+ });
63
+ console.error(`[worktree] Added missing origin remote to ${worktreePath}: ${originUrl}`);
64
+ } else {
65
+ console.error(`[worktree] WARNING: main repo origin URL is also invalid: "${originUrl}"`);
66
+ }
67
+ } catch (err) {
68
+ const msg = err instanceof Error ? err.message : String(err);
69
+ console.error(`[worktree] Failed to ensure remote for ${worktreePath}: ${msg}`);
70
+ }
71
+ }
72
+ async function cleanupWorktree(worktreeName, exec = defaultExecFile) {
73
+ let target = worktreeName;
74
+ try {
75
+ const path = await findWorktreeForBranch(exec, worktreeName);
76
+ if (!path) return true;
77
+ target = path;
78
+ } catch {
79
+ return true;
80
+ }
81
+ return new Promise((resolve) => {
82
+ exec("git", ["worktree", "remove", "--force", target], (err) => {
83
+ if (err) {
84
+ console.error(`[worktree] cleanup failed for ${worktreeName} (${target}): ${err.message}`);
85
+ resolve(false);
86
+ } else {
87
+ resolve(true);
88
+ }
89
+ });
90
+ });
91
+ }
92
+ function execPromise(exec, cmd, args) {
93
+ return new Promise((resolve, reject) => {
94
+ exec(cmd, args, (err, stdout, stderr) => {
95
+ if (err) reject(Object.assign(err, { stdout, stderr }));
96
+ else resolve({ stdout, stderr });
97
+ });
98
+ });
99
+ }
100
+ async function findWorktreeForBranch(exec, branch) {
101
+ try {
102
+ const { stdout } = await execPromise(exec, "git", ["worktree", "list", "--porcelain"]);
103
+ const targetRef = `refs/heads/${branch}`;
104
+ let currentPath = null;
105
+ for (const line of normalizeEol(stdout).split("\n")) {
106
+ if (line.startsWith("worktree ")) currentPath = line.slice("worktree ".length);
107
+ if (line.startsWith("branch ") && line.slice("branch ".length) === targetRef && currentPath) {
108
+ return currentPath;
109
+ }
110
+ }
111
+ return null;
112
+ } catch {
113
+ return null;
114
+ }
115
+ }
116
+ async function mergeWorktreeBranch(worktreeName, integrationBranch, exec = defaultExecFile) {
117
+ try {
118
+ try {
119
+ await execPromise(exec, "git", ["rev-parse", "--verify", worktreeName]);
120
+ } catch {
121
+ return true;
122
+ }
123
+ await execPromise(exec, "git", ["branch", integrationBranch, "HEAD"]).catch(() => {
124
+ });
125
+ const checkedOutPath = await findWorktreeForBranch(exec, integrationBranch);
126
+ if (checkedOutPath) {
127
+ await execPromise(exec, "git", ["-C", checkedOutPath, "merge", "--no-edit", worktreeName]);
128
+ console.error(`[worktree] merged ${worktreeName} \u2192 ${integrationBranch}`);
129
+ return true;
130
+ }
131
+ const { stdout: baseOut } = await execPromise(exec, "git", ["merge-base", integrationBranch, worktreeName]);
132
+ const mergeBase = baseOut.trim();
133
+ const { stdout: integrationSha } = await execPromise(exec, "git", ["rev-parse", integrationBranch]);
134
+ if (integrationSha.trim() === mergeBase) {
135
+ const { stdout: worktreeSha } = await execPromise(exec, "git", ["rev-parse", worktreeName]);
136
+ await execPromise(exec, "git", ["update-ref", `refs/heads/${integrationBranch}`, worktreeSha.trim()]);
137
+ console.error(`[worktree] fast-forward merged ${worktreeName} \u2192 ${integrationBranch}`);
138
+ return true;
139
+ }
140
+ const tmpWorktree = `merge-tmp-${Date.now()}`;
141
+ try {
142
+ await execPromise(exec, "git", ["worktree", "add", tmpWorktree, integrationBranch]);
143
+ await execPromise(exec, "git", ["-C", tmpWorktree, "merge", "--no-edit", worktreeName]);
144
+ console.error(`[worktree] merged ${worktreeName} \u2192 ${integrationBranch}`);
145
+ } finally {
146
+ await execPromise(exec, "git", ["worktree", "remove", "--force", tmpWorktree]).catch(() => {
147
+ });
148
+ }
149
+ return true;
150
+ } catch (err) {
151
+ const msg = err instanceof Error ? err.message : String(err);
152
+ console.error(`[worktree] merge failed for ${worktreeName} \u2192 ${integrationBranch}: ${msg}`);
153
+ return false;
154
+ }
155
+ }
156
+
12
157
  // src/lib/stuck-detector.ts
13
158
  import { z } from "zod";
14
159
 
@@ -971,13 +1116,13 @@ function fingerprintsMatch(a, b) {
971
1116
 
972
1117
  // src/lib/mcp-config.ts
973
1118
  import { writeFileSync, unlinkSync, mkdirSync, existsSync, readdirSync, rmdirSync, rmSync } from "fs";
974
- import { join, dirname } from "path";
1119
+ import { join as join2, dirname } from "path";
975
1120
  import { fileURLToPath } from "url";
976
- import { homedir } from "os";
1121
+ import { homedir as homedir2 } from "os";
977
1122
  var __filename = fileURLToPath(import.meta.url);
978
1123
  var __dirname = dirname(__filename);
979
1124
  function generateMcpConfig(apiUrl, apiToken, boardId) {
980
- const localMcpPath = existsSync(join(__dirname, "..", "..", "mcp", "dist", "index.js")) ? join(__dirname, "..", "..", "mcp", "dist", "index.js") : join(__dirname, "..", "..", "..", "mcp", "dist", "index.js");
1125
+ const localMcpPath = existsSync(join2(__dirname, "..", "..", "mcp", "dist", "index.js")) ? join2(__dirname, "..", "..", "mcp", "dist", "index.js") : join2(__dirname, "..", "..", "..", "mcp", "dist", "index.js");
981
1126
  const useLocal = existsSync(localMcpPath);
982
1127
  const kantbanServer = useLocal ? {
983
1128
  command: "node",
@@ -999,9 +1144,9 @@ function generateMcpConfig(apiUrl, apiToken, boardId) {
999
1144
  kantban: kantbanServer
1000
1145
  }
1001
1146
  };
1002
- const dir = join(homedir(), ".kantban", "pipelines", boardId, String(process.pid));
1147
+ const dir = join2(homedir2(), ".kantban", "pipelines", boardId, String(process.pid));
1003
1148
  mkdirSync(dir, { recursive: true, mode: 448 });
1004
- const filePath = join(dir, "mcp-config.json");
1149
+ const filePath = join2(dir, "mcp-config.json");
1005
1150
  writeFileSync(filePath, JSON.stringify(config, null, 2), { mode: 384 });
1006
1151
  return filePath;
1007
1152
  }
@@ -1018,10 +1163,10 @@ function cleanupMcpConfig(filePath) {
1018
1163
  }
1019
1164
  }
1020
1165
  function generateGateProxyMcpConfig(apiUrl, apiToken, boardId, gateConfigPath, columnId, columnName, projectId, gateCwd, ticketId) {
1021
- const localMcpPath = existsSync(join(__dirname, "..", "..", "mcp", "dist", "index.js")) ? join(__dirname, "..", "..", "mcp", "dist", "index.js") : join(__dirname, "..", "..", "..", "mcp", "dist", "index.js");
1166
+ const localMcpPath = existsSync(join2(__dirname, "..", "..", "mcp", "dist", "index.js")) ? join2(__dirname, "..", "..", "mcp", "dist", "index.js") : join2(__dirname, "..", "..", "..", "mcp", "dist", "index.js");
1022
1167
  const useLocal = existsSync(localMcpPath);
1023
1168
  const kantbanServer = useLocal ? { command: "node", args: [localMcpPath], env: { KANTBAN_API_TOKEN: apiToken, KANTBAN_API_URL: apiUrl, KANTBAN_HIDDEN_TOOLS: "kantban_move_ticket,kantban_move_tickets,kantban_complete_task,kantban_move_to_board" } } : { command: npxCommand(), args: ["-y", "kantban-mcp@latest"], env: { KANTBAN_API_TOKEN: apiToken, KANTBAN_API_URL: apiUrl, KANTBAN_HIDDEN_TOOLS: "kantban_move_ticket,kantban_move_tickets,kantban_complete_task,kantban_move_to_board" } };
1024
- const gateProxyPath = existsSync(join(__dirname, "lib", "gate-proxy-server.js")) ? join(__dirname, "lib", "gate-proxy-server.js") : join(__dirname, "gate-proxy-server.js");
1169
+ const gateProxyPath = existsSync(join2(__dirname, "lib", "gate-proxy-server.js")) ? join2(__dirname, "lib", "gate-proxy-server.js") : join2(__dirname, "gate-proxy-server.js");
1025
1170
  const gateProxyEnv = {
1026
1171
  GATE_CONFIG_PATH: gateConfigPath,
1027
1172
  COLUMN_ID: columnId,
@@ -1047,10 +1192,10 @@ function generateGateProxyMcpConfig(apiUrl, apiToken, boardId, gateConfigPath, c
1047
1192
  "kantban-gates": gateProxyServer
1048
1193
  }
1049
1194
  };
1050
- const dir = join(homedir(), ".kantban", "pipelines", boardId, String(process.pid));
1195
+ const dir = join2(homedir2(), ".kantban", "pipelines", boardId, String(process.pid));
1051
1196
  mkdirSync(dir, { recursive: true, mode: 448 });
1052
1197
  const suffix = ticketId ? `${columnId}-${ticketId}` : columnId;
1053
- const filePath = join(dir, `mcp-config-${suffix}.json`);
1198
+ const filePath = join2(dir, `mcp-config-${suffix}.json`);
1054
1199
  writeFileSync(filePath, JSON.stringify(config, null, 2), { mode: 384 });
1055
1200
  return filePath;
1056
1201
  }
@@ -1060,7 +1205,7 @@ function cleanupGateProxyConfigs(pipelineDir) {
1060
1205
  for (const f of files) {
1061
1206
  if (f.startsWith("mcp-config-") && f.endsWith(".json")) {
1062
1207
  try {
1063
- unlinkSync(join(pipelineDir, f));
1208
+ unlinkSync(join2(pipelineDir, f));
1064
1209
  } catch {
1065
1210
  }
1066
1211
  }
@@ -1088,7 +1233,7 @@ function reapOrphanedMcpConfigDirs(boardDir) {
1088
1233
  }
1089
1234
  if (!alive) {
1090
1235
  try {
1091
- rmSync(join(boardDir, entry), { recursive: true, force: true });
1236
+ rmSync(join2(boardDir, entry), { recursive: true, force: true });
1092
1237
  } catch {
1093
1238
  }
1094
1239
  }
@@ -1096,10 +1241,10 @@ function reapOrphanedMcpConfigDirs(boardDir) {
1096
1241
  }
1097
1242
 
1098
1243
  // src/providers/claude-provider.ts
1099
- import { spawn } from "child_process";
1100
- import { writeFileSync as writeFileSync2, mkdirSync as mkdirSync2 } from "fs";
1101
- import { join as join2 } from "path";
1102
- import { homedir as homedir2 } from "os";
1244
+ import { spawn, execFileSync as execFileSync2 } from "child_process";
1245
+ import { writeFileSync as writeFileSync2, mkdirSync as mkdirSync2, existsSync as existsSync2, rmSync as rmSync2 } from "fs";
1246
+ import { join as join3 } from "path";
1247
+ import { homedir as homedir3 } from "os";
1103
1248
 
1104
1249
  // src/providers/claude-stream-parser.ts
1105
1250
  var ClaudeStreamParser = class {
@@ -1202,7 +1347,7 @@ var ClaudeProvider = class {
1202
1347
  supportsMaxTurns: true,
1203
1348
  supportsMcpConfigInjection: true,
1204
1349
  supportsMcpConfigOverride: false,
1205
- supportsWorktreeFlag: true,
1350
+ supportsWorktreeFlag: false,
1206
1351
  supportsSandboxModes: false,
1207
1352
  supportedModels: [
1208
1353
  { id: "claude-haiku-4-5-20251001", displayName: "Haiku 4.5", tier: "fast" },
@@ -1215,13 +1360,56 @@ var ClaudeProvider = class {
1215
1360
  async invoke(request) {
1216
1361
  const args = this.buildArgs(request);
1217
1362
  const startTime = Date.now();
1363
+ if (request.workingDirectory) {
1364
+ const branch = request.branch ?? request.workingDirectory;
1365
+ if (!existsSync2(request.workingDirectory)) {
1366
+ try {
1367
+ execFileSync2("git", ["worktree", "add", "-b", branch, request.workingDirectory, "HEAD"], {
1368
+ stdio: "pipe"
1369
+ });
1370
+ } catch {
1371
+ try {
1372
+ execFileSync2("git", ["worktree", "add", request.workingDirectory, branch], {
1373
+ stdio: "pipe"
1374
+ });
1375
+ } catch (err) {
1376
+ const msg = err instanceof Error ? err.message : String(err);
1377
+ throw new Error(`worktree_creation_failed: ${msg}`);
1378
+ }
1379
+ }
1380
+ } else {
1381
+ try {
1382
+ execFileSync2("git", ["-C", request.workingDirectory, "rev-parse", "--git-dir"], {
1383
+ stdio: "pipe"
1384
+ });
1385
+ } catch {
1386
+ try {
1387
+ rmSync2(request.workingDirectory, { recursive: true, force: true });
1388
+ execFileSync2("git", ["worktree", "add", "-b", branch, request.workingDirectory, "HEAD"], {
1389
+ stdio: "pipe"
1390
+ });
1391
+ } catch {
1392
+ try {
1393
+ execFileSync2("git", ["worktree", "add", request.workingDirectory, branch], {
1394
+ stdio: "pipe"
1395
+ });
1396
+ } catch (err) {
1397
+ const msg = err instanceof Error ? err.message : String(err);
1398
+ throw new Error(`worktree_creation_failed: ${msg}`);
1399
+ }
1400
+ }
1401
+ }
1402
+ }
1403
+ if (existsSync2(request.workingDirectory)) {
1404
+ ensureWorktreeRemote(request.workingDirectory);
1405
+ }
1406
+ }
1218
1407
  const [cmd, prefixArgs] = resolveCommand("claude");
1219
1408
  const resolvedArgs = [...prefixArgs, ...args];
1220
1409
  return new Promise((resolve) => {
1221
1410
  const child = spawn(cmd, resolvedArgs, {
1222
1411
  stdio: ["pipe", "pipe", "pipe"],
1223
- // Do NOT set cwd to the worktree name — the worktree doesn't exist yet.
1224
- // Claude Code's --worktree flag creates it internally.
1412
+ ...request.workingDirectory ? { cwd: request.workingDirectory } : {},
1225
1413
  // Only use shell:true as fallback when resolveCommand couldn't resolve
1226
1414
  ...prefixArgs.length > 0 ? {} : crossSpawnOptions()
1227
1415
  });
@@ -1300,10 +1488,6 @@ var ClaudeProvider = class {
1300
1488
  if (request.maxTurns) {
1301
1489
  args.push("--max-turns", String(request.maxTurns));
1302
1490
  }
1303
- const worktreeName = request.branch ?? request.workingDirectory;
1304
- if (worktreeName) {
1305
- args.push("--worktree", worktreeName);
1306
- }
1307
1491
  if (request.toolRestrictions) {
1308
1492
  const tr = request.toolRestrictions;
1309
1493
  if (tr.tools !== void 0) args.push("--tools", tr.tools);
@@ -1315,15 +1499,21 @@ var ClaudeProvider = class {
1315
1499
  writeMcpConfigJson(mcpConfig) {
1316
1500
  if (!mcpConfig) return "";
1317
1501
  const config = { mcpServers: mcpConfig.servers };
1318
- const dir = join2(homedir2(), ".kantban", "tmp");
1502
+ const dir = join3(homedir3(), ".kantban", "tmp");
1319
1503
  mkdirSync2(dir, { recursive: true, mode: 448 });
1320
- const filePath = join2(dir, `mcp-config-${Date.now()}.json`);
1504
+ const filePath = join3(dir, `mcp-config-${Date.now()}.json`);
1321
1505
  writeFileSync2(filePath, JSON.stringify(config, null, 2), { mode: 384 });
1322
1506
  return filePath;
1323
1507
  }
1324
1508
  };
1325
1509
 
1326
1510
  export {
1511
+ generateWorktreeName,
1512
+ renderWorktreePath,
1513
+ defaultWorktreeRoot,
1514
+ ensureWorktreeRemote,
1515
+ cleanupWorktree,
1516
+ mergeWorktreeBranch,
1327
1517
  parseJsonFromLlmOutput,
1328
1518
  composeStuckDetectionPrompt,
1329
1519
  parseStuckDetectionResponse,
@@ -1337,4 +1527,4 @@ export {
1337
1527
  reapOrphanedMcpConfigDirs,
1338
1528
  ClaudeProvider
1339
1529
  };
1340
- //# sourceMappingURL=chunk-X2CJ3ZAI.js.map
1530
+ //# sourceMappingURL=chunk-3A4B7CUH.js.map