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/bin/cli.js
CHANGED
|
@@ -14,9 +14,9 @@ import { fileURLToPath } from "url";
|
|
|
14
14
|
import { existsSync as existsSync3 } from "fs";
|
|
15
15
|
|
|
16
16
|
// src/server/services/executor.ts
|
|
17
|
-
import { spawn } from "child_process";
|
|
17
|
+
import { spawn, execSync } from "child_process";
|
|
18
18
|
import { join as join4 } from "path";
|
|
19
|
-
import { writeFileSync as writeFileSync4, unlinkSync, mkdirSync as mkdirSync2, existsSync as existsSync2, appendFileSync as appendFileSync2, readFileSync as readFileSync4 } from "fs";
|
|
19
|
+
import { writeFileSync as writeFileSync4, unlinkSync, mkdirSync as mkdirSync2, existsSync as existsSync2, appendFileSync as appendFileSync2, readFileSync as readFileSync4, rmSync } from "fs";
|
|
20
20
|
import { EventEmitter } from "events";
|
|
21
21
|
|
|
22
22
|
// src/server/services/project.ts
|
|
@@ -444,6 +444,7 @@ function getRecentProgress(projectPath, lines = 100) {
|
|
|
444
444
|
// src/server/services/executor.ts
|
|
445
445
|
var KANBAN_DIR4 = ".claude-kanban";
|
|
446
446
|
var LOGS_DIR = "logs";
|
|
447
|
+
var WORKTREES_DIR = "worktrees";
|
|
447
448
|
var TaskExecutor = class extends EventEmitter {
|
|
448
449
|
projectPath;
|
|
449
450
|
runningTasks = /* @__PURE__ */ new Map();
|
|
@@ -493,6 +494,136 @@ var TaskExecutor = class extends EventEmitter {
|
|
|
493
494
|
if (!existsSync2(logPath)) return null;
|
|
494
495
|
return readFileSync4(logPath, "utf-8");
|
|
495
496
|
}
|
|
497
|
+
/**
|
|
498
|
+
* Check if project is a git repository
|
|
499
|
+
*/
|
|
500
|
+
isGitRepo() {
|
|
501
|
+
try {
|
|
502
|
+
execSync("git rev-parse --is-inside-work-tree", {
|
|
503
|
+
cwd: this.projectPath,
|
|
504
|
+
stdio: "pipe"
|
|
505
|
+
});
|
|
506
|
+
return true;
|
|
507
|
+
} catch {
|
|
508
|
+
return false;
|
|
509
|
+
}
|
|
510
|
+
}
|
|
511
|
+
/**
|
|
512
|
+
* Get worktrees directory path
|
|
513
|
+
*/
|
|
514
|
+
getWorktreesDir() {
|
|
515
|
+
return join4(this.projectPath, KANBAN_DIR4, WORKTREES_DIR);
|
|
516
|
+
}
|
|
517
|
+
/**
|
|
518
|
+
* Get worktree path for a task
|
|
519
|
+
*/
|
|
520
|
+
getWorktreePath(taskId) {
|
|
521
|
+
return join4(this.getWorktreesDir(), taskId);
|
|
522
|
+
}
|
|
523
|
+
/**
|
|
524
|
+
* Get branch name for a task
|
|
525
|
+
*/
|
|
526
|
+
getBranchName(taskId) {
|
|
527
|
+
return `task/${taskId}`;
|
|
528
|
+
}
|
|
529
|
+
/**
|
|
530
|
+
* Create a git worktree for isolated task execution
|
|
531
|
+
*/
|
|
532
|
+
createWorktree(taskId) {
|
|
533
|
+
if (!this.isGitRepo()) {
|
|
534
|
+
console.log("[executor] Not a git repo, skipping worktree creation");
|
|
535
|
+
return null;
|
|
536
|
+
}
|
|
537
|
+
const worktreePath = this.getWorktreePath(taskId);
|
|
538
|
+
const branchName = this.getBranchName(taskId);
|
|
539
|
+
try {
|
|
540
|
+
const worktreesDir = this.getWorktreesDir();
|
|
541
|
+
if (!existsSync2(worktreesDir)) {
|
|
542
|
+
mkdirSync2(worktreesDir, { recursive: true });
|
|
543
|
+
}
|
|
544
|
+
if (existsSync2(worktreePath)) {
|
|
545
|
+
this.removeWorktree(taskId);
|
|
546
|
+
}
|
|
547
|
+
try {
|
|
548
|
+
execSync(`git rev-parse --verify ${branchName}`, {
|
|
549
|
+
cwd: this.projectPath,
|
|
550
|
+
stdio: "pipe"
|
|
551
|
+
});
|
|
552
|
+
execSync(`git branch -D ${branchName}`, {
|
|
553
|
+
cwd: this.projectPath,
|
|
554
|
+
stdio: "pipe"
|
|
555
|
+
});
|
|
556
|
+
} catch {
|
|
557
|
+
}
|
|
558
|
+
execSync(`git worktree add -b ${branchName} "${worktreePath}"`, {
|
|
559
|
+
cwd: this.projectPath,
|
|
560
|
+
stdio: "pipe"
|
|
561
|
+
});
|
|
562
|
+
console.log(`[executor] Created worktree at ${worktreePath} on branch ${branchName}`);
|
|
563
|
+
return { worktreePath, branchName };
|
|
564
|
+
} catch (error) {
|
|
565
|
+
console.error("[executor] Failed to create worktree:", error);
|
|
566
|
+
return null;
|
|
567
|
+
}
|
|
568
|
+
}
|
|
569
|
+
/**
|
|
570
|
+
* Remove a git worktree
|
|
571
|
+
*/
|
|
572
|
+
removeWorktree(taskId) {
|
|
573
|
+
const worktreePath = this.getWorktreePath(taskId);
|
|
574
|
+
const branchName = this.getBranchName(taskId);
|
|
575
|
+
try {
|
|
576
|
+
if (existsSync2(worktreePath)) {
|
|
577
|
+
execSync(`git worktree remove "${worktreePath}" --force`, {
|
|
578
|
+
cwd: this.projectPath,
|
|
579
|
+
stdio: "pipe"
|
|
580
|
+
});
|
|
581
|
+
console.log(`[executor] Removed worktree at ${worktreePath}`);
|
|
582
|
+
}
|
|
583
|
+
} catch (error) {
|
|
584
|
+
console.error("[executor] Failed to remove worktree via git:", error);
|
|
585
|
+
try {
|
|
586
|
+
if (existsSync2(worktreePath)) {
|
|
587
|
+
rmSync(worktreePath, { recursive: true, force: true });
|
|
588
|
+
execSync("git worktree prune", {
|
|
589
|
+
cwd: this.projectPath,
|
|
590
|
+
stdio: "pipe"
|
|
591
|
+
});
|
|
592
|
+
}
|
|
593
|
+
} catch {
|
|
594
|
+
console.error("[executor] Failed to force remove worktree directory");
|
|
595
|
+
}
|
|
596
|
+
}
|
|
597
|
+
try {
|
|
598
|
+
execSync(`git branch -D ${branchName}`, {
|
|
599
|
+
cwd: this.projectPath,
|
|
600
|
+
stdio: "pipe"
|
|
601
|
+
});
|
|
602
|
+
console.log(`[executor] Deleted branch ${branchName}`);
|
|
603
|
+
} catch {
|
|
604
|
+
}
|
|
605
|
+
}
|
|
606
|
+
/**
|
|
607
|
+
* Merge worktree branch back to main branch
|
|
608
|
+
*/
|
|
609
|
+
mergeWorktreeBranch(taskId) {
|
|
610
|
+
const branchName = this.getBranchName(taskId);
|
|
611
|
+
try {
|
|
612
|
+
const currentBranch = execSync("git rev-parse --abbrev-ref HEAD", {
|
|
613
|
+
cwd: this.projectPath,
|
|
614
|
+
encoding: "utf-8"
|
|
615
|
+
}).trim();
|
|
616
|
+
execSync(`git merge ${branchName} --no-edit`, {
|
|
617
|
+
cwd: this.projectPath,
|
|
618
|
+
stdio: "pipe"
|
|
619
|
+
});
|
|
620
|
+
console.log(`[executor] Merged ${branchName} into ${currentBranch}`);
|
|
621
|
+
return true;
|
|
622
|
+
} catch (error) {
|
|
623
|
+
console.error("[executor] Failed to merge branch:", error);
|
|
624
|
+
return false;
|
|
625
|
+
}
|
|
626
|
+
}
|
|
496
627
|
/**
|
|
497
628
|
* Get number of currently running tasks
|
|
498
629
|
*/
|
|
@@ -578,10 +709,10 @@ Focus only on this task. When successfully complete, output: <promise>COMPLETE</
|
|
|
578
709
|
}
|
|
579
710
|
updateTask(this.projectPath, taskId, { status: "in_progress" });
|
|
580
711
|
const startedAt = /* @__PURE__ */ new Date();
|
|
712
|
+
const worktreeInfo = this.createWorktree(taskId);
|
|
713
|
+
const executionPath = worktreeInfo?.worktreePath || this.projectPath;
|
|
581
714
|
const prompt = this.buildTaskPrompt(task, config);
|
|
582
715
|
const kanbanDir = join4(this.projectPath, KANBAN_DIR4);
|
|
583
|
-
const prdPath = join4(kanbanDir, "prd.json");
|
|
584
|
-
const progressPath = join4(kanbanDir, "progress.txt");
|
|
585
716
|
const promptFile = join4(kanbanDir, `prompt-${taskId}.txt`);
|
|
586
717
|
writeFileSync4(promptFile, prompt);
|
|
587
718
|
const args = [];
|
|
@@ -596,9 +727,13 @@ Focus only on this task. When successfully complete, output: <promise>COMPLETE</
|
|
|
596
727
|
const commandDisplay = `${config.agent.command} ${args.join(" ")}`;
|
|
597
728
|
const fullCommand = `${config.agent.command} ${args.join(" ")}`;
|
|
598
729
|
console.log("[executor] Command:", fullCommand);
|
|
599
|
-
console.log("[executor] CWD:",
|
|
730
|
+
console.log("[executor] CWD:", executionPath);
|
|
731
|
+
if (worktreeInfo) {
|
|
732
|
+
console.log("[executor] Using worktree:", worktreeInfo.worktreePath);
|
|
733
|
+
console.log("[executor] Branch:", worktreeInfo.branchName);
|
|
734
|
+
}
|
|
600
735
|
const childProcess = spawn("bash", ["-c", fullCommand], {
|
|
601
|
-
cwd:
|
|
736
|
+
cwd: executionPath,
|
|
602
737
|
env: {
|
|
603
738
|
...process.env,
|
|
604
739
|
TERM: "xterm-256color",
|
|
@@ -614,7 +749,9 @@ Focus only on this task. When successfully complete, output: <promise>COMPLETE</
|
|
|
614
749
|
taskId,
|
|
615
750
|
process: childProcess,
|
|
616
751
|
startedAt,
|
|
617
|
-
output: []
|
|
752
|
+
output: [],
|
|
753
|
+
worktreePath: worktreeInfo?.worktreePath,
|
|
754
|
+
branchName: worktreeInfo?.branchName
|
|
618
755
|
};
|
|
619
756
|
this.runningTasks.set(taskId, runningTask);
|
|
620
757
|
this.initLogFile(taskId);
|
|
@@ -626,6 +763,12 @@ Focus only on this task. When successfully complete, output: <promise>COMPLETE</
|
|
|
626
763
|
this.emit("task:started", { taskId, timestamp: startedAt.toISOString() });
|
|
627
764
|
logOutput(`[claude-kanban] Starting task: ${task.title}
|
|
628
765
|
`);
|
|
766
|
+
if (worktreeInfo) {
|
|
767
|
+
logOutput(`[claude-kanban] Worktree: ${worktreeInfo.worktreePath}
|
|
768
|
+
`);
|
|
769
|
+
logOutput(`[claude-kanban] Branch: ${worktreeInfo.branchName}
|
|
770
|
+
`);
|
|
771
|
+
}
|
|
629
772
|
logOutput(`[claude-kanban] Command: ${commandDisplay}
|
|
630
773
|
`);
|
|
631
774
|
logOutput(`[claude-kanban] Process spawned (PID: ${childProcess.pid})
|
|
@@ -682,6 +825,9 @@ Focus only on this task. When successfully complete, output: <promise>COMPLETE</
|
|
|
682
825
|
unlinkSync(promptFile);
|
|
683
826
|
} catch {
|
|
684
827
|
}
|
|
828
|
+
if (worktreeInfo) {
|
|
829
|
+
this.removeWorktree(taskId);
|
|
830
|
+
}
|
|
685
831
|
updateTask(this.projectPath, taskId, { status: "failed", passes: false });
|
|
686
832
|
const endedAt = /* @__PURE__ */ new Date();
|
|
687
833
|
addExecutionEntry(this.projectPath, taskId, {
|
|
@@ -723,6 +869,15 @@ Focus only on this task. When successfully complete, output: <promise>COMPLETE</
|
|
|
723
869
|
const isComplete = output.includes("<promise>COMPLETE</promise>");
|
|
724
870
|
const task = getTaskById(this.projectPath, taskId);
|
|
725
871
|
if (isComplete || exitCode === 0) {
|
|
872
|
+
if (runningTask.worktreePath && runningTask.branchName) {
|
|
873
|
+
const merged = this.mergeWorktreeBranch(taskId);
|
|
874
|
+
if (merged) {
|
|
875
|
+
console.log(`[executor] Successfully merged ${runningTask.branchName}`);
|
|
876
|
+
} else {
|
|
877
|
+
console.log(`[executor] Failed to merge ${runningTask.branchName}, branch preserved for manual merge`);
|
|
878
|
+
}
|
|
879
|
+
this.removeWorktree(taskId);
|
|
880
|
+
}
|
|
726
881
|
updateTask(this.projectPath, taskId, {
|
|
727
882
|
status: "completed",
|
|
728
883
|
passes: true
|
|
@@ -742,6 +897,9 @@ Focus only on this task. When successfully complete, output: <promise>COMPLETE</
|
|
|
742
897
|
this.emit("task:completed", { taskId, duration });
|
|
743
898
|
this.afkTasksCompleted++;
|
|
744
899
|
} else {
|
|
900
|
+
if (runningTask.worktreePath) {
|
|
901
|
+
this.removeWorktree(taskId);
|
|
902
|
+
}
|
|
745
903
|
updateTask(this.projectPath, taskId, {
|
|
746
904
|
status: "failed",
|
|
747
905
|
passes: false
|
|
@@ -790,6 +948,9 @@ Focus only on this task. When successfully complete, output: <promise>COMPLETE</
|
|
|
790
948
|
}, 2e3);
|
|
791
949
|
} catch {
|
|
792
950
|
}
|
|
951
|
+
if (runningTask.worktreePath) {
|
|
952
|
+
this.removeWorktree(taskId);
|
|
953
|
+
}
|
|
793
954
|
updateTask(this.projectPath, taskId, {
|
|
794
955
|
status: "ready"
|
|
795
956
|
});
|
|
@@ -889,6 +1050,9 @@ Focus only on this task. When successfully complete, output: <promise>COMPLETE</
|
|
|
889
1050
|
runningTask.process.kill("SIGKILL");
|
|
890
1051
|
} catch {
|
|
891
1052
|
}
|
|
1053
|
+
if (runningTask.worktreePath) {
|
|
1054
|
+
this.removeWorktree(taskId);
|
|
1055
|
+
}
|
|
892
1056
|
this.runningTasks.delete(taskId);
|
|
893
1057
|
}
|
|
894
1058
|
this.stopAFKMode();
|