claude-kanban 0.1.0 → 0.2.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/bin/cli.js +230 -9
- package/dist/bin/cli.js.map +1 -1
- package/dist/server/index.js +230 -9
- 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,194 @@ var TaskExecutor = class extends EventEmitter {
|
|
|
493
494
|
if (!existsSync2(logPath)) return null;
|
|
494
495
|
return readFileSync4(logPath, "utf-8");
|
|
495
496
|
}
|
|
497
|
+
/**
|
|
498
|
+
* Format tool use for display in logs
|
|
499
|
+
*/
|
|
500
|
+
formatToolUse(toolName, input) {
|
|
501
|
+
const truncate = (str, maxLen = 80) => {
|
|
502
|
+
if (str.length <= maxLen) return str;
|
|
503
|
+
return str.slice(0, maxLen) + "...";
|
|
504
|
+
};
|
|
505
|
+
switch (toolName) {
|
|
506
|
+
case "Bash":
|
|
507
|
+
return `[Bash] $ ${truncate(String(input.command || ""))}
|
|
508
|
+
`;
|
|
509
|
+
case "Read":
|
|
510
|
+
return `[Read] ${input.file_path}
|
|
511
|
+
`;
|
|
512
|
+
case "Edit":
|
|
513
|
+
return `[Edit] ${input.file_path}
|
|
514
|
+
`;
|
|
515
|
+
case "Write":
|
|
516
|
+
return `[Write] ${input.file_path}
|
|
517
|
+
`;
|
|
518
|
+
case "Grep":
|
|
519
|
+
return `[Grep] "${truncate(String(input.pattern || ""))}" in ${input.path || "."}
|
|
520
|
+
`;
|
|
521
|
+
case "Glob":
|
|
522
|
+
return `[Glob] ${input.pattern} in ${input.path || "."}
|
|
523
|
+
`;
|
|
524
|
+
case "Task":
|
|
525
|
+
return `[Task] ${input.description || truncate(String(input.prompt || ""))}
|
|
526
|
+
`;
|
|
527
|
+
case "TodoWrite":
|
|
528
|
+
const todos = input.todos;
|
|
529
|
+
if (todos && Array.isArray(todos)) {
|
|
530
|
+
const summary = todos.map((t) => ` ${t.status === "completed" ? "\u2713" : t.status === "in_progress" ? "\u2192" : "\u25CB"} ${truncate(t.content, 60)}`).join("\n");
|
|
531
|
+
return `[TodoWrite]
|
|
532
|
+
${summary}
|
|
533
|
+
`;
|
|
534
|
+
}
|
|
535
|
+
return `[TodoWrite]
|
|
536
|
+
`;
|
|
537
|
+
case "WebFetch":
|
|
538
|
+
return `[WebFetch] ${input.url}
|
|
539
|
+
`;
|
|
540
|
+
case "WebSearch":
|
|
541
|
+
return `[WebSearch] "${truncate(String(input.query || ""))}"
|
|
542
|
+
`;
|
|
543
|
+
default:
|
|
544
|
+
const meaningfulParams = ["file_path", "path", "command", "query", "url", "pattern", "target"];
|
|
545
|
+
for (const param of meaningfulParams) {
|
|
546
|
+
if (input[param]) {
|
|
547
|
+
return `[${toolName}] ${truncate(String(input[param]))}
|
|
548
|
+
`;
|
|
549
|
+
}
|
|
550
|
+
}
|
|
551
|
+
return `[${toolName}]
|
|
552
|
+
`;
|
|
553
|
+
}
|
|
554
|
+
}
|
|
555
|
+
/**
|
|
556
|
+
* Check if project is a git repository
|
|
557
|
+
*/
|
|
558
|
+
isGitRepo() {
|
|
559
|
+
try {
|
|
560
|
+
execSync("git rev-parse --is-inside-work-tree", {
|
|
561
|
+
cwd: this.projectPath,
|
|
562
|
+
stdio: "pipe"
|
|
563
|
+
});
|
|
564
|
+
return true;
|
|
565
|
+
} catch {
|
|
566
|
+
return false;
|
|
567
|
+
}
|
|
568
|
+
}
|
|
569
|
+
/**
|
|
570
|
+
* Get worktrees directory path
|
|
571
|
+
*/
|
|
572
|
+
getWorktreesDir() {
|
|
573
|
+
return join4(this.projectPath, KANBAN_DIR4, WORKTREES_DIR);
|
|
574
|
+
}
|
|
575
|
+
/**
|
|
576
|
+
* Get worktree path for a task
|
|
577
|
+
*/
|
|
578
|
+
getWorktreePath(taskId) {
|
|
579
|
+
return join4(this.getWorktreesDir(), taskId);
|
|
580
|
+
}
|
|
581
|
+
/**
|
|
582
|
+
* Get branch name for a task
|
|
583
|
+
*/
|
|
584
|
+
getBranchName(taskId) {
|
|
585
|
+
return `task/${taskId}`;
|
|
586
|
+
}
|
|
587
|
+
/**
|
|
588
|
+
* Create a git worktree for isolated task execution
|
|
589
|
+
*/
|
|
590
|
+
createWorktree(taskId) {
|
|
591
|
+
if (!this.isGitRepo()) {
|
|
592
|
+
console.log("[executor] Not a git repo, skipping worktree creation");
|
|
593
|
+
return null;
|
|
594
|
+
}
|
|
595
|
+
const worktreePath = this.getWorktreePath(taskId);
|
|
596
|
+
const branchName = this.getBranchName(taskId);
|
|
597
|
+
try {
|
|
598
|
+
const worktreesDir = this.getWorktreesDir();
|
|
599
|
+
if (!existsSync2(worktreesDir)) {
|
|
600
|
+
mkdirSync2(worktreesDir, { recursive: true });
|
|
601
|
+
}
|
|
602
|
+
if (existsSync2(worktreePath)) {
|
|
603
|
+
this.removeWorktree(taskId);
|
|
604
|
+
}
|
|
605
|
+
try {
|
|
606
|
+
execSync(`git rev-parse --verify ${branchName}`, {
|
|
607
|
+
cwd: this.projectPath,
|
|
608
|
+
stdio: "pipe"
|
|
609
|
+
});
|
|
610
|
+
execSync(`git branch -D ${branchName}`, {
|
|
611
|
+
cwd: this.projectPath,
|
|
612
|
+
stdio: "pipe"
|
|
613
|
+
});
|
|
614
|
+
} catch {
|
|
615
|
+
}
|
|
616
|
+
execSync(`git worktree add -b ${branchName} "${worktreePath}"`, {
|
|
617
|
+
cwd: this.projectPath,
|
|
618
|
+
stdio: "pipe"
|
|
619
|
+
});
|
|
620
|
+
console.log(`[executor] Created worktree at ${worktreePath} on branch ${branchName}`);
|
|
621
|
+
return { worktreePath, branchName };
|
|
622
|
+
} catch (error) {
|
|
623
|
+
console.error("[executor] Failed to create worktree:", error);
|
|
624
|
+
return null;
|
|
625
|
+
}
|
|
626
|
+
}
|
|
627
|
+
/**
|
|
628
|
+
* Remove a git worktree
|
|
629
|
+
*/
|
|
630
|
+
removeWorktree(taskId) {
|
|
631
|
+
const worktreePath = this.getWorktreePath(taskId);
|
|
632
|
+
const branchName = this.getBranchName(taskId);
|
|
633
|
+
try {
|
|
634
|
+
if (existsSync2(worktreePath)) {
|
|
635
|
+
execSync(`git worktree remove "${worktreePath}" --force`, {
|
|
636
|
+
cwd: this.projectPath,
|
|
637
|
+
stdio: "pipe"
|
|
638
|
+
});
|
|
639
|
+
console.log(`[executor] Removed worktree at ${worktreePath}`);
|
|
640
|
+
}
|
|
641
|
+
} catch (error) {
|
|
642
|
+
console.error("[executor] Failed to remove worktree via git:", error);
|
|
643
|
+
try {
|
|
644
|
+
if (existsSync2(worktreePath)) {
|
|
645
|
+
rmSync(worktreePath, { recursive: true, force: true });
|
|
646
|
+
execSync("git worktree prune", {
|
|
647
|
+
cwd: this.projectPath,
|
|
648
|
+
stdio: "pipe"
|
|
649
|
+
});
|
|
650
|
+
}
|
|
651
|
+
} catch {
|
|
652
|
+
console.error("[executor] Failed to force remove worktree directory");
|
|
653
|
+
}
|
|
654
|
+
}
|
|
655
|
+
try {
|
|
656
|
+
execSync(`git branch -D ${branchName}`, {
|
|
657
|
+
cwd: this.projectPath,
|
|
658
|
+
stdio: "pipe"
|
|
659
|
+
});
|
|
660
|
+
console.log(`[executor] Deleted branch ${branchName}`);
|
|
661
|
+
} catch {
|
|
662
|
+
}
|
|
663
|
+
}
|
|
664
|
+
/**
|
|
665
|
+
* Merge worktree branch back to main branch
|
|
666
|
+
*/
|
|
667
|
+
mergeWorktreeBranch(taskId) {
|
|
668
|
+
const branchName = this.getBranchName(taskId);
|
|
669
|
+
try {
|
|
670
|
+
const currentBranch = execSync("git rev-parse --abbrev-ref HEAD", {
|
|
671
|
+
cwd: this.projectPath,
|
|
672
|
+
encoding: "utf-8"
|
|
673
|
+
}).trim();
|
|
674
|
+
execSync(`git merge ${branchName} --no-edit`, {
|
|
675
|
+
cwd: this.projectPath,
|
|
676
|
+
stdio: "pipe"
|
|
677
|
+
});
|
|
678
|
+
console.log(`[executor] Merged ${branchName} into ${currentBranch}`);
|
|
679
|
+
return true;
|
|
680
|
+
} catch (error) {
|
|
681
|
+
console.error("[executor] Failed to merge branch:", error);
|
|
682
|
+
return false;
|
|
683
|
+
}
|
|
684
|
+
}
|
|
496
685
|
/**
|
|
497
686
|
* Get number of currently running tasks
|
|
498
687
|
*/
|
|
@@ -578,10 +767,10 @@ Focus only on this task. When successfully complete, output: <promise>COMPLETE</
|
|
|
578
767
|
}
|
|
579
768
|
updateTask(this.projectPath, taskId, { status: "in_progress" });
|
|
580
769
|
const startedAt = /* @__PURE__ */ new Date();
|
|
770
|
+
const worktreeInfo = this.createWorktree(taskId);
|
|
771
|
+
const executionPath = worktreeInfo?.worktreePath || this.projectPath;
|
|
581
772
|
const prompt = this.buildTaskPrompt(task, config);
|
|
582
773
|
const kanbanDir = join4(this.projectPath, KANBAN_DIR4);
|
|
583
|
-
const prdPath = join4(kanbanDir, "prd.json");
|
|
584
|
-
const progressPath = join4(kanbanDir, "progress.txt");
|
|
585
774
|
const promptFile = join4(kanbanDir, `prompt-${taskId}.txt`);
|
|
586
775
|
writeFileSync4(promptFile, prompt);
|
|
587
776
|
const args = [];
|
|
@@ -596,9 +785,13 @@ Focus only on this task. When successfully complete, output: <promise>COMPLETE</
|
|
|
596
785
|
const commandDisplay = `${config.agent.command} ${args.join(" ")}`;
|
|
597
786
|
const fullCommand = `${config.agent.command} ${args.join(" ")}`;
|
|
598
787
|
console.log("[executor] Command:", fullCommand);
|
|
599
|
-
console.log("[executor] CWD:",
|
|
788
|
+
console.log("[executor] CWD:", executionPath);
|
|
789
|
+
if (worktreeInfo) {
|
|
790
|
+
console.log("[executor] Using worktree:", worktreeInfo.worktreePath);
|
|
791
|
+
console.log("[executor] Branch:", worktreeInfo.branchName);
|
|
792
|
+
}
|
|
600
793
|
const childProcess = spawn("bash", ["-c", fullCommand], {
|
|
601
|
-
cwd:
|
|
794
|
+
cwd: executionPath,
|
|
602
795
|
env: {
|
|
603
796
|
...process.env,
|
|
604
797
|
TERM: "xterm-256color",
|
|
@@ -614,7 +807,9 @@ Focus only on this task. When successfully complete, output: <promise>COMPLETE</
|
|
|
614
807
|
taskId,
|
|
615
808
|
process: childProcess,
|
|
616
809
|
startedAt,
|
|
617
|
-
output: []
|
|
810
|
+
output: [],
|
|
811
|
+
worktreePath: worktreeInfo?.worktreePath,
|
|
812
|
+
branchName: worktreeInfo?.branchName
|
|
618
813
|
};
|
|
619
814
|
this.runningTasks.set(taskId, runningTask);
|
|
620
815
|
this.initLogFile(taskId);
|
|
@@ -626,6 +821,12 @@ Focus only on this task. When successfully complete, output: <promise>COMPLETE</
|
|
|
626
821
|
this.emit("task:started", { taskId, timestamp: startedAt.toISOString() });
|
|
627
822
|
logOutput(`[claude-kanban] Starting task: ${task.title}
|
|
628
823
|
`);
|
|
824
|
+
if (worktreeInfo) {
|
|
825
|
+
logOutput(`[claude-kanban] Worktree: ${worktreeInfo.worktreePath}
|
|
826
|
+
`);
|
|
827
|
+
logOutput(`[claude-kanban] Branch: ${worktreeInfo.branchName}
|
|
828
|
+
`);
|
|
829
|
+
}
|
|
629
830
|
logOutput(`[claude-kanban] Command: ${commandDisplay}
|
|
630
831
|
`);
|
|
631
832
|
logOutput(`[claude-kanban] Process spawned (PID: ${childProcess.pid})
|
|
@@ -645,8 +846,7 @@ Focus only on this task. When successfully complete, output: <promise>COMPLETE</
|
|
|
645
846
|
if (block.type === "text") {
|
|
646
847
|
text += block.text;
|
|
647
848
|
} else if (block.type === "tool_use") {
|
|
648
|
-
text +=
|
|
649
|
-
`;
|
|
849
|
+
text += this.formatToolUse(block.name, block.input);
|
|
650
850
|
}
|
|
651
851
|
}
|
|
652
852
|
} else if (json.type === "content_block_delta" && json.delta?.text) {
|
|
@@ -682,6 +882,9 @@ Focus only on this task. When successfully complete, output: <promise>COMPLETE</
|
|
|
682
882
|
unlinkSync(promptFile);
|
|
683
883
|
} catch {
|
|
684
884
|
}
|
|
885
|
+
if (worktreeInfo) {
|
|
886
|
+
this.removeWorktree(taskId);
|
|
887
|
+
}
|
|
685
888
|
updateTask(this.projectPath, taskId, { status: "failed", passes: false });
|
|
686
889
|
const endedAt = /* @__PURE__ */ new Date();
|
|
687
890
|
addExecutionEntry(this.projectPath, taskId, {
|
|
@@ -723,6 +926,15 @@ Focus only on this task. When successfully complete, output: <promise>COMPLETE</
|
|
|
723
926
|
const isComplete = output.includes("<promise>COMPLETE</promise>");
|
|
724
927
|
const task = getTaskById(this.projectPath, taskId);
|
|
725
928
|
if (isComplete || exitCode === 0) {
|
|
929
|
+
if (runningTask.worktreePath && runningTask.branchName) {
|
|
930
|
+
const merged = this.mergeWorktreeBranch(taskId);
|
|
931
|
+
if (merged) {
|
|
932
|
+
console.log(`[executor] Successfully merged ${runningTask.branchName}`);
|
|
933
|
+
} else {
|
|
934
|
+
console.log(`[executor] Failed to merge ${runningTask.branchName}, branch preserved for manual merge`);
|
|
935
|
+
}
|
|
936
|
+
this.removeWorktree(taskId);
|
|
937
|
+
}
|
|
726
938
|
updateTask(this.projectPath, taskId, {
|
|
727
939
|
status: "completed",
|
|
728
940
|
passes: true
|
|
@@ -742,6 +954,9 @@ Focus only on this task. When successfully complete, output: <promise>COMPLETE</
|
|
|
742
954
|
this.emit("task:completed", { taskId, duration });
|
|
743
955
|
this.afkTasksCompleted++;
|
|
744
956
|
} else {
|
|
957
|
+
if (runningTask.worktreePath) {
|
|
958
|
+
this.removeWorktree(taskId);
|
|
959
|
+
}
|
|
745
960
|
updateTask(this.projectPath, taskId, {
|
|
746
961
|
status: "failed",
|
|
747
962
|
passes: false
|
|
@@ -790,6 +1005,9 @@ Focus only on this task. When successfully complete, output: <promise>COMPLETE</
|
|
|
790
1005
|
}, 2e3);
|
|
791
1006
|
} catch {
|
|
792
1007
|
}
|
|
1008
|
+
if (runningTask.worktreePath) {
|
|
1009
|
+
this.removeWorktree(taskId);
|
|
1010
|
+
}
|
|
793
1011
|
updateTask(this.projectPath, taskId, {
|
|
794
1012
|
status: "ready"
|
|
795
1013
|
});
|
|
@@ -889,6 +1107,9 @@ Focus only on this task. When successfully complete, output: <promise>COMPLETE</
|
|
|
889
1107
|
runningTask.process.kill("SIGKILL");
|
|
890
1108
|
} catch {
|
|
891
1109
|
}
|
|
1110
|
+
if (runningTask.worktreePath) {
|
|
1111
|
+
this.removeWorktree(taskId);
|
|
1112
|
+
}
|
|
892
1113
|
this.runningTasks.delete(taskId);
|
|
893
1114
|
}
|
|
894
1115
|
this.stopAFKMode();
|