claude-kanban 0.1.0 → 0.2.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/dist/bin/cli.js +171 -7
- package/dist/bin/cli.js.map +1 -1
- package/dist/server/index.js +171 -7
- package/dist/server/index.js.map +1 -1
- package/package.json +1 -1
package/dist/server/index.js
CHANGED
|
@@ -7,9 +7,9 @@ import { fileURLToPath } from "url";
|
|
|
7
7
|
import { existsSync as existsSync3 } from "fs";
|
|
8
8
|
|
|
9
9
|
// src/server/services/executor.ts
|
|
10
|
-
import { spawn } from "child_process";
|
|
10
|
+
import { spawn, execSync } from "child_process";
|
|
11
11
|
import { join as join4 } from "path";
|
|
12
|
-
import { writeFileSync as writeFileSync4, unlinkSync, mkdirSync as mkdirSync2, existsSync as existsSync2, appendFileSync as appendFileSync2, readFileSync as readFileSync4 } from "fs";
|
|
12
|
+
import { writeFileSync as writeFileSync4, unlinkSync, mkdirSync as mkdirSync2, existsSync as existsSync2, appendFileSync as appendFileSync2, readFileSync as readFileSync4, rmSync } from "fs";
|
|
13
13
|
import { EventEmitter } from "events";
|
|
14
14
|
|
|
15
15
|
// src/server/services/project.ts
|
|
@@ -206,6 +206,7 @@ function getRecentProgress(projectPath, lines = 100) {
|
|
|
206
206
|
// src/server/services/executor.ts
|
|
207
207
|
var KANBAN_DIR4 = ".claude-kanban";
|
|
208
208
|
var LOGS_DIR = "logs";
|
|
209
|
+
var WORKTREES_DIR = "worktrees";
|
|
209
210
|
var TaskExecutor = class extends EventEmitter {
|
|
210
211
|
projectPath;
|
|
211
212
|
runningTasks = /* @__PURE__ */ new Map();
|
|
@@ -255,6 +256,136 @@ var TaskExecutor = class extends EventEmitter {
|
|
|
255
256
|
if (!existsSync2(logPath)) return null;
|
|
256
257
|
return readFileSync4(logPath, "utf-8");
|
|
257
258
|
}
|
|
259
|
+
/**
|
|
260
|
+
* Check if project is a git repository
|
|
261
|
+
*/
|
|
262
|
+
isGitRepo() {
|
|
263
|
+
try {
|
|
264
|
+
execSync("git rev-parse --is-inside-work-tree", {
|
|
265
|
+
cwd: this.projectPath,
|
|
266
|
+
stdio: "pipe"
|
|
267
|
+
});
|
|
268
|
+
return true;
|
|
269
|
+
} catch {
|
|
270
|
+
return false;
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
/**
|
|
274
|
+
* Get worktrees directory path
|
|
275
|
+
*/
|
|
276
|
+
getWorktreesDir() {
|
|
277
|
+
return join4(this.projectPath, KANBAN_DIR4, WORKTREES_DIR);
|
|
278
|
+
}
|
|
279
|
+
/**
|
|
280
|
+
* Get worktree path for a task
|
|
281
|
+
*/
|
|
282
|
+
getWorktreePath(taskId) {
|
|
283
|
+
return join4(this.getWorktreesDir(), taskId);
|
|
284
|
+
}
|
|
285
|
+
/**
|
|
286
|
+
* Get branch name for a task
|
|
287
|
+
*/
|
|
288
|
+
getBranchName(taskId) {
|
|
289
|
+
return `task/${taskId}`;
|
|
290
|
+
}
|
|
291
|
+
/**
|
|
292
|
+
* Create a git worktree for isolated task execution
|
|
293
|
+
*/
|
|
294
|
+
createWorktree(taskId) {
|
|
295
|
+
if (!this.isGitRepo()) {
|
|
296
|
+
console.log("[executor] Not a git repo, skipping worktree creation");
|
|
297
|
+
return null;
|
|
298
|
+
}
|
|
299
|
+
const worktreePath = this.getWorktreePath(taskId);
|
|
300
|
+
const branchName = this.getBranchName(taskId);
|
|
301
|
+
try {
|
|
302
|
+
const worktreesDir = this.getWorktreesDir();
|
|
303
|
+
if (!existsSync2(worktreesDir)) {
|
|
304
|
+
mkdirSync2(worktreesDir, { recursive: true });
|
|
305
|
+
}
|
|
306
|
+
if (existsSync2(worktreePath)) {
|
|
307
|
+
this.removeWorktree(taskId);
|
|
308
|
+
}
|
|
309
|
+
try {
|
|
310
|
+
execSync(`git rev-parse --verify ${branchName}`, {
|
|
311
|
+
cwd: this.projectPath,
|
|
312
|
+
stdio: "pipe"
|
|
313
|
+
});
|
|
314
|
+
execSync(`git branch -D ${branchName}`, {
|
|
315
|
+
cwd: this.projectPath,
|
|
316
|
+
stdio: "pipe"
|
|
317
|
+
});
|
|
318
|
+
} catch {
|
|
319
|
+
}
|
|
320
|
+
execSync(`git worktree add -b ${branchName} "${worktreePath}"`, {
|
|
321
|
+
cwd: this.projectPath,
|
|
322
|
+
stdio: "pipe"
|
|
323
|
+
});
|
|
324
|
+
console.log(`[executor] Created worktree at ${worktreePath} on branch ${branchName}`);
|
|
325
|
+
return { worktreePath, branchName };
|
|
326
|
+
} catch (error) {
|
|
327
|
+
console.error("[executor] Failed to create worktree:", error);
|
|
328
|
+
return null;
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
/**
|
|
332
|
+
* Remove a git worktree
|
|
333
|
+
*/
|
|
334
|
+
removeWorktree(taskId) {
|
|
335
|
+
const worktreePath = this.getWorktreePath(taskId);
|
|
336
|
+
const branchName = this.getBranchName(taskId);
|
|
337
|
+
try {
|
|
338
|
+
if (existsSync2(worktreePath)) {
|
|
339
|
+
execSync(`git worktree remove "${worktreePath}" --force`, {
|
|
340
|
+
cwd: this.projectPath,
|
|
341
|
+
stdio: "pipe"
|
|
342
|
+
});
|
|
343
|
+
console.log(`[executor] Removed worktree at ${worktreePath}`);
|
|
344
|
+
}
|
|
345
|
+
} catch (error) {
|
|
346
|
+
console.error("[executor] Failed to remove worktree via git:", error);
|
|
347
|
+
try {
|
|
348
|
+
if (existsSync2(worktreePath)) {
|
|
349
|
+
rmSync(worktreePath, { recursive: true, force: true });
|
|
350
|
+
execSync("git worktree prune", {
|
|
351
|
+
cwd: this.projectPath,
|
|
352
|
+
stdio: "pipe"
|
|
353
|
+
});
|
|
354
|
+
}
|
|
355
|
+
} catch {
|
|
356
|
+
console.error("[executor] Failed to force remove worktree directory");
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
try {
|
|
360
|
+
execSync(`git branch -D ${branchName}`, {
|
|
361
|
+
cwd: this.projectPath,
|
|
362
|
+
stdio: "pipe"
|
|
363
|
+
});
|
|
364
|
+
console.log(`[executor] Deleted branch ${branchName}`);
|
|
365
|
+
} catch {
|
|
366
|
+
}
|
|
367
|
+
}
|
|
368
|
+
/**
|
|
369
|
+
* Merge worktree branch back to main branch
|
|
370
|
+
*/
|
|
371
|
+
mergeWorktreeBranch(taskId) {
|
|
372
|
+
const branchName = this.getBranchName(taskId);
|
|
373
|
+
try {
|
|
374
|
+
const currentBranch = execSync("git rev-parse --abbrev-ref HEAD", {
|
|
375
|
+
cwd: this.projectPath,
|
|
376
|
+
encoding: "utf-8"
|
|
377
|
+
}).trim();
|
|
378
|
+
execSync(`git merge ${branchName} --no-edit`, {
|
|
379
|
+
cwd: this.projectPath,
|
|
380
|
+
stdio: "pipe"
|
|
381
|
+
});
|
|
382
|
+
console.log(`[executor] Merged ${branchName} into ${currentBranch}`);
|
|
383
|
+
return true;
|
|
384
|
+
} catch (error) {
|
|
385
|
+
console.error("[executor] Failed to merge branch:", error);
|
|
386
|
+
return false;
|
|
387
|
+
}
|
|
388
|
+
}
|
|
258
389
|
/**
|
|
259
390
|
* Get number of currently running tasks
|
|
260
391
|
*/
|
|
@@ -340,10 +471,10 @@ Focus only on this task. When successfully complete, output: <promise>COMPLETE</
|
|
|
340
471
|
}
|
|
341
472
|
updateTask(this.projectPath, taskId, { status: "in_progress" });
|
|
342
473
|
const startedAt = /* @__PURE__ */ new Date();
|
|
474
|
+
const worktreeInfo = this.createWorktree(taskId);
|
|
475
|
+
const executionPath = worktreeInfo?.worktreePath || this.projectPath;
|
|
343
476
|
const prompt = this.buildTaskPrompt(task, config);
|
|
344
477
|
const kanbanDir = join4(this.projectPath, KANBAN_DIR4);
|
|
345
|
-
const prdPath = join4(kanbanDir, "prd.json");
|
|
346
|
-
const progressPath = join4(kanbanDir, "progress.txt");
|
|
347
478
|
const promptFile = join4(kanbanDir, `prompt-${taskId}.txt`);
|
|
348
479
|
writeFileSync4(promptFile, prompt);
|
|
349
480
|
const args = [];
|
|
@@ -358,9 +489,13 @@ Focus only on this task. When successfully complete, output: <promise>COMPLETE</
|
|
|
358
489
|
const commandDisplay = `${config.agent.command} ${args.join(" ")}`;
|
|
359
490
|
const fullCommand = `${config.agent.command} ${args.join(" ")}`;
|
|
360
491
|
console.log("[executor] Command:", fullCommand);
|
|
361
|
-
console.log("[executor] CWD:",
|
|
492
|
+
console.log("[executor] CWD:", executionPath);
|
|
493
|
+
if (worktreeInfo) {
|
|
494
|
+
console.log("[executor] Using worktree:", worktreeInfo.worktreePath);
|
|
495
|
+
console.log("[executor] Branch:", worktreeInfo.branchName);
|
|
496
|
+
}
|
|
362
497
|
const childProcess = spawn("bash", ["-c", fullCommand], {
|
|
363
|
-
cwd:
|
|
498
|
+
cwd: executionPath,
|
|
364
499
|
env: {
|
|
365
500
|
...process.env,
|
|
366
501
|
TERM: "xterm-256color",
|
|
@@ -376,7 +511,9 @@ Focus only on this task. When successfully complete, output: <promise>COMPLETE</
|
|
|
376
511
|
taskId,
|
|
377
512
|
process: childProcess,
|
|
378
513
|
startedAt,
|
|
379
|
-
output: []
|
|
514
|
+
output: [],
|
|
515
|
+
worktreePath: worktreeInfo?.worktreePath,
|
|
516
|
+
branchName: worktreeInfo?.branchName
|
|
380
517
|
};
|
|
381
518
|
this.runningTasks.set(taskId, runningTask);
|
|
382
519
|
this.initLogFile(taskId);
|
|
@@ -388,6 +525,12 @@ Focus only on this task. When successfully complete, output: <promise>COMPLETE</
|
|
|
388
525
|
this.emit("task:started", { taskId, timestamp: startedAt.toISOString() });
|
|
389
526
|
logOutput(`[claude-kanban] Starting task: ${task.title}
|
|
390
527
|
`);
|
|
528
|
+
if (worktreeInfo) {
|
|
529
|
+
logOutput(`[claude-kanban] Worktree: ${worktreeInfo.worktreePath}
|
|
530
|
+
`);
|
|
531
|
+
logOutput(`[claude-kanban] Branch: ${worktreeInfo.branchName}
|
|
532
|
+
`);
|
|
533
|
+
}
|
|
391
534
|
logOutput(`[claude-kanban] Command: ${commandDisplay}
|
|
392
535
|
`);
|
|
393
536
|
logOutput(`[claude-kanban] Process spawned (PID: ${childProcess.pid})
|
|
@@ -444,6 +587,9 @@ Focus only on this task. When successfully complete, output: <promise>COMPLETE</
|
|
|
444
587
|
unlinkSync(promptFile);
|
|
445
588
|
} catch {
|
|
446
589
|
}
|
|
590
|
+
if (worktreeInfo) {
|
|
591
|
+
this.removeWorktree(taskId);
|
|
592
|
+
}
|
|
447
593
|
updateTask(this.projectPath, taskId, { status: "failed", passes: false });
|
|
448
594
|
const endedAt = /* @__PURE__ */ new Date();
|
|
449
595
|
addExecutionEntry(this.projectPath, taskId, {
|
|
@@ -485,6 +631,15 @@ Focus only on this task. When successfully complete, output: <promise>COMPLETE</
|
|
|
485
631
|
const isComplete = output.includes("<promise>COMPLETE</promise>");
|
|
486
632
|
const task = getTaskById(this.projectPath, taskId);
|
|
487
633
|
if (isComplete || exitCode === 0) {
|
|
634
|
+
if (runningTask.worktreePath && runningTask.branchName) {
|
|
635
|
+
const merged = this.mergeWorktreeBranch(taskId);
|
|
636
|
+
if (merged) {
|
|
637
|
+
console.log(`[executor] Successfully merged ${runningTask.branchName}`);
|
|
638
|
+
} else {
|
|
639
|
+
console.log(`[executor] Failed to merge ${runningTask.branchName}, branch preserved for manual merge`);
|
|
640
|
+
}
|
|
641
|
+
this.removeWorktree(taskId);
|
|
642
|
+
}
|
|
488
643
|
updateTask(this.projectPath, taskId, {
|
|
489
644
|
status: "completed",
|
|
490
645
|
passes: true
|
|
@@ -504,6 +659,9 @@ Focus only on this task. When successfully complete, output: <promise>COMPLETE</
|
|
|
504
659
|
this.emit("task:completed", { taskId, duration });
|
|
505
660
|
this.afkTasksCompleted++;
|
|
506
661
|
} else {
|
|
662
|
+
if (runningTask.worktreePath) {
|
|
663
|
+
this.removeWorktree(taskId);
|
|
664
|
+
}
|
|
507
665
|
updateTask(this.projectPath, taskId, {
|
|
508
666
|
status: "failed",
|
|
509
667
|
passes: false
|
|
@@ -552,6 +710,9 @@ Focus only on this task. When successfully complete, output: <promise>COMPLETE</
|
|
|
552
710
|
}, 2e3);
|
|
553
711
|
} catch {
|
|
554
712
|
}
|
|
713
|
+
if (runningTask.worktreePath) {
|
|
714
|
+
this.removeWorktree(taskId);
|
|
715
|
+
}
|
|
555
716
|
updateTask(this.projectPath, taskId, {
|
|
556
717
|
status: "ready"
|
|
557
718
|
});
|
|
@@ -651,6 +812,9 @@ Focus only on this task. When successfully complete, output: <promise>COMPLETE</
|
|
|
651
812
|
runningTask.process.kill("SIGKILL");
|
|
652
813
|
} catch {
|
|
653
814
|
}
|
|
815
|
+
if (runningTask.worktreePath) {
|
|
816
|
+
this.removeWorktree(taskId);
|
|
817
|
+
}
|
|
654
818
|
this.runningTasks.delete(taskId);
|
|
655
819
|
}
|
|
656
820
|
this.stopAFKMode();
|