claude-kanban 0.3.1 → 0.4.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 +433 -22
- package/dist/bin/cli.js.map +1 -1
- package/dist/server/index.js +360 -22
- package/dist/server/index.js.map +1 -1
- package/package.json +1 -1
package/dist/bin/cli.js
CHANGED
|
@@ -4,6 +4,10 @@
|
|
|
4
4
|
import { Command } from "commander";
|
|
5
5
|
import chalk from "chalk";
|
|
6
6
|
import open from "open";
|
|
7
|
+
import { existsSync as existsSync4 } from "fs";
|
|
8
|
+
import { execSync as execSync2 } from "child_process";
|
|
9
|
+
import { createInterface } from "readline";
|
|
10
|
+
import { join as join6 } from "path";
|
|
7
11
|
|
|
8
12
|
// src/server/index.ts
|
|
9
13
|
import express from "express";
|
|
@@ -454,6 +458,7 @@ function getTaskCounts(projectPath) {
|
|
|
454
458
|
draft: 0,
|
|
455
459
|
ready: 0,
|
|
456
460
|
in_progress: 0,
|
|
461
|
+
in_review: 0,
|
|
457
462
|
completed: 0,
|
|
458
463
|
failed: 0
|
|
459
464
|
};
|
|
@@ -530,6 +535,7 @@ var WORKTREES_DIR = "worktrees";
|
|
|
530
535
|
var TaskExecutor = class extends EventEmitter {
|
|
531
536
|
projectPath;
|
|
532
537
|
runningTasks = /* @__PURE__ */ new Map();
|
|
538
|
+
reviewingTasks = /* @__PURE__ */ new Map();
|
|
533
539
|
afkMode = false;
|
|
534
540
|
afkIteration = 0;
|
|
535
541
|
afkMaxIterations = 0;
|
|
@@ -833,7 +839,7 @@ ${worktreeSection}## INSTRUCTIONS
|
|
|
833
839
|
${verifySection}${verifySteps.length > 0 ? "4" : "3"}. When complete, update the task in ${prdPath}:
|
|
834
840
|
- Find the task with id "${task.id}"
|
|
835
841
|
- Set "passes": true
|
|
836
|
-
|
|
842
|
+
(Note: Do NOT change the status - the system will move it to review)
|
|
837
843
|
|
|
838
844
|
${verifySteps.length > 0 ? "5" : "4"}. Document your work in ${progressPath}:
|
|
839
845
|
- What you implemented and files changed
|
|
@@ -1012,7 +1018,7 @@ Focus only on this task. When successfully complete, output: <promise>COMPLETE</
|
|
|
1012
1018
|
}, timeoutMs);
|
|
1013
1019
|
}
|
|
1014
1020
|
/**
|
|
1015
|
-
* Handle task completion
|
|
1021
|
+
* Handle task completion - moves to in_review, keeps worktree alive
|
|
1016
1022
|
*/
|
|
1017
1023
|
handleTaskComplete(taskId, exitCode, startedAt) {
|
|
1018
1024
|
const runningTask = this.runningTasks.get(taskId);
|
|
@@ -1024,31 +1030,25 @@ Focus only on this task. When successfully complete, output: <promise>COMPLETE</
|
|
|
1024
1030
|
const task = getTaskById(this.projectPath, taskId);
|
|
1025
1031
|
if (isComplete || exitCode === 0) {
|
|
1026
1032
|
if (runningTask.worktreePath && runningTask.branchName) {
|
|
1027
|
-
|
|
1028
|
-
|
|
1029
|
-
|
|
1030
|
-
|
|
1031
|
-
|
|
1032
|
-
}
|
|
1033
|
-
|
|
1033
|
+
this.reviewingTasks.set(taskId, {
|
|
1034
|
+
worktreePath: runningTask.worktreePath,
|
|
1035
|
+
branchName: runningTask.branchName,
|
|
1036
|
+
startedAt,
|
|
1037
|
+
endedAt
|
|
1038
|
+
});
|
|
1039
|
+
console.log(`[executor] Task ${taskId} ready for review at ${runningTask.worktreePath}`);
|
|
1034
1040
|
}
|
|
1035
1041
|
updateTask(this.projectPath, taskId, {
|
|
1036
|
-
status: "
|
|
1042
|
+
status: "in_review",
|
|
1037
1043
|
passes: true
|
|
1038
1044
|
});
|
|
1039
|
-
addExecutionEntry(this.projectPath, taskId, {
|
|
1040
|
-
startedAt: startedAt.toISOString(),
|
|
1041
|
-
endedAt: endedAt.toISOString(),
|
|
1042
|
-
status: "completed",
|
|
1043
|
-
duration
|
|
1044
|
-
});
|
|
1045
1045
|
logTaskExecution(this.projectPath, {
|
|
1046
1046
|
taskId,
|
|
1047
1047
|
taskTitle: task?.title || "Unknown",
|
|
1048
|
-
status: "
|
|
1048
|
+
status: "in_review",
|
|
1049
1049
|
duration
|
|
1050
1050
|
});
|
|
1051
|
-
this.emit("task:completed", { taskId, duration });
|
|
1051
|
+
this.emit("task:completed", { taskId, duration, status: "in_review" });
|
|
1052
1052
|
this.afkTasksCompleted++;
|
|
1053
1053
|
} else {
|
|
1054
1054
|
if (runningTask.worktreePath) {
|
|
@@ -1080,6 +1080,151 @@ Focus only on this task. When successfully complete, output: <promise>COMPLETE</
|
|
|
1080
1080
|
this.continueAFKMode();
|
|
1081
1081
|
}
|
|
1082
1082
|
}
|
|
1083
|
+
/**
|
|
1084
|
+
* Get info about a task in review
|
|
1085
|
+
*/
|
|
1086
|
+
getReviewingTask(taskId) {
|
|
1087
|
+
return this.reviewingTasks.get(taskId);
|
|
1088
|
+
}
|
|
1089
|
+
/**
|
|
1090
|
+
* Merge a reviewed task into the base branch
|
|
1091
|
+
*/
|
|
1092
|
+
mergeTask(taskId) {
|
|
1093
|
+
const reviewInfo = this.reviewingTasks.get(taskId);
|
|
1094
|
+
if (!reviewInfo) {
|
|
1095
|
+
return { success: false, error: "Task not in review or no worktree found" };
|
|
1096
|
+
}
|
|
1097
|
+
const merged = this.mergeWorktreeBranch(taskId);
|
|
1098
|
+
if (!merged) {
|
|
1099
|
+
return { success: false, error: "Merge failed - may have conflicts" };
|
|
1100
|
+
}
|
|
1101
|
+
this.removeWorktree(taskId);
|
|
1102
|
+
this.reviewingTasks.delete(taskId);
|
|
1103
|
+
updateTask(this.projectPath, taskId, {
|
|
1104
|
+
status: "completed"
|
|
1105
|
+
});
|
|
1106
|
+
addExecutionEntry(this.projectPath, taskId, {
|
|
1107
|
+
startedAt: reviewInfo.startedAt.toISOString(),
|
|
1108
|
+
endedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1109
|
+
status: "completed",
|
|
1110
|
+
duration: (/* @__PURE__ */ new Date()).getTime() - reviewInfo.startedAt.getTime()
|
|
1111
|
+
});
|
|
1112
|
+
console.log(`[executor] Task ${taskId} merged successfully`);
|
|
1113
|
+
return { success: true };
|
|
1114
|
+
}
|
|
1115
|
+
/**
|
|
1116
|
+
* Create a PR for a reviewed task
|
|
1117
|
+
*/
|
|
1118
|
+
createPR(taskId) {
|
|
1119
|
+
const reviewInfo = this.reviewingTasks.get(taskId);
|
|
1120
|
+
const task = getTaskById(this.projectPath, taskId);
|
|
1121
|
+
if (!reviewInfo) {
|
|
1122
|
+
return { success: false, error: "Task not in review or no worktree found" };
|
|
1123
|
+
}
|
|
1124
|
+
try {
|
|
1125
|
+
execSync(`git push -u origin ${reviewInfo.branchName}`, {
|
|
1126
|
+
cwd: this.projectPath,
|
|
1127
|
+
stdio: "pipe"
|
|
1128
|
+
});
|
|
1129
|
+
const prTitle = task?.title || `Task ${taskId}`;
|
|
1130
|
+
const prBody = task?.description || "";
|
|
1131
|
+
const result = execSync(
|
|
1132
|
+
`gh pr create --title "${prTitle.replace(/"/g, '\\"')}" --body "${prBody.replace(/"/g, '\\"')}" --head ${reviewInfo.branchName}`,
|
|
1133
|
+
{
|
|
1134
|
+
cwd: this.projectPath,
|
|
1135
|
+
encoding: "utf-8"
|
|
1136
|
+
}
|
|
1137
|
+
);
|
|
1138
|
+
const prUrl = result.trim();
|
|
1139
|
+
try {
|
|
1140
|
+
execSync(`git worktree remove "${reviewInfo.worktreePath}" --force`, {
|
|
1141
|
+
cwd: this.projectPath,
|
|
1142
|
+
stdio: "pipe"
|
|
1143
|
+
});
|
|
1144
|
+
} catch {
|
|
1145
|
+
}
|
|
1146
|
+
this.reviewingTasks.delete(taskId);
|
|
1147
|
+
updateTask(this.projectPath, taskId, {
|
|
1148
|
+
status: "completed"
|
|
1149
|
+
});
|
|
1150
|
+
addExecutionEntry(this.projectPath, taskId, {
|
|
1151
|
+
startedAt: reviewInfo.startedAt.toISOString(),
|
|
1152
|
+
endedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1153
|
+
status: "completed",
|
|
1154
|
+
duration: (/* @__PURE__ */ new Date()).getTime() - reviewInfo.startedAt.getTime()
|
|
1155
|
+
});
|
|
1156
|
+
console.log(`[executor] PR created for task ${taskId}: ${prUrl}`);
|
|
1157
|
+
return { success: true, prUrl };
|
|
1158
|
+
} catch (error) {
|
|
1159
|
+
const errorMsg = error instanceof Error ? error.message : String(error);
|
|
1160
|
+
console.error(`[executor] Failed to create PR:`, errorMsg);
|
|
1161
|
+
return { success: false, error: errorMsg };
|
|
1162
|
+
}
|
|
1163
|
+
}
|
|
1164
|
+
/**
|
|
1165
|
+
* Discard a reviewed task - delete worktree and return to ready
|
|
1166
|
+
*/
|
|
1167
|
+
discardTask(taskId) {
|
|
1168
|
+
const reviewInfo = this.reviewingTasks.get(taskId);
|
|
1169
|
+
if (!reviewInfo) {
|
|
1170
|
+
return { success: false, error: "Task not in review or no worktree found" };
|
|
1171
|
+
}
|
|
1172
|
+
this.removeWorktree(taskId);
|
|
1173
|
+
this.reviewingTasks.delete(taskId);
|
|
1174
|
+
updateTask(this.projectPath, taskId, {
|
|
1175
|
+
status: "ready",
|
|
1176
|
+
passes: false
|
|
1177
|
+
});
|
|
1178
|
+
addExecutionEntry(this.projectPath, taskId, {
|
|
1179
|
+
startedAt: reviewInfo.startedAt.toISOString(),
|
|
1180
|
+
endedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1181
|
+
status: "discarded",
|
|
1182
|
+
duration: (/* @__PURE__ */ new Date()).getTime() - reviewInfo.startedAt.getTime()
|
|
1183
|
+
});
|
|
1184
|
+
console.log(`[executor] Task ${taskId} discarded, returned to ready`);
|
|
1185
|
+
return { success: true };
|
|
1186
|
+
}
|
|
1187
|
+
/**
|
|
1188
|
+
* Get diff for a task in review
|
|
1189
|
+
*/
|
|
1190
|
+
getTaskDiff(taskId) {
|
|
1191
|
+
const reviewInfo = this.reviewingTasks.get(taskId);
|
|
1192
|
+
if (!reviewInfo) {
|
|
1193
|
+
return { success: false, error: "Task not in review or no worktree found" };
|
|
1194
|
+
}
|
|
1195
|
+
try {
|
|
1196
|
+
const diff = execSync(`git diff main...${reviewInfo.branchName}`, {
|
|
1197
|
+
cwd: this.projectPath,
|
|
1198
|
+
encoding: "utf-8",
|
|
1199
|
+
maxBuffer: 10 * 1024 * 1024
|
|
1200
|
+
// 10MB buffer for large diffs
|
|
1201
|
+
});
|
|
1202
|
+
return { success: true, diff };
|
|
1203
|
+
} catch (error) {
|
|
1204
|
+
const errorMsg = error instanceof Error ? error.message : String(error);
|
|
1205
|
+
return { success: false, error: errorMsg };
|
|
1206
|
+
}
|
|
1207
|
+
}
|
|
1208
|
+
/**
|
|
1209
|
+
* Get list of changed files for a task in review
|
|
1210
|
+
*/
|
|
1211
|
+
getTaskChangedFiles(taskId) {
|
|
1212
|
+
const reviewInfo = this.reviewingTasks.get(taskId);
|
|
1213
|
+
if (!reviewInfo) {
|
|
1214
|
+
return { success: false, error: "Task not in review or no worktree found" };
|
|
1215
|
+
}
|
|
1216
|
+
try {
|
|
1217
|
+
const result = execSync(`git diff --name-only main...${reviewInfo.branchName}`, {
|
|
1218
|
+
cwd: this.projectPath,
|
|
1219
|
+
encoding: "utf-8"
|
|
1220
|
+
});
|
|
1221
|
+
const files = result.trim().split("\n").filter((f) => f);
|
|
1222
|
+
return { success: true, files };
|
|
1223
|
+
} catch (error) {
|
|
1224
|
+
const errorMsg = error instanceof Error ? error.message : String(error);
|
|
1225
|
+
return { success: false, error: errorMsg };
|
|
1226
|
+
}
|
|
1227
|
+
}
|
|
1083
1228
|
/**
|
|
1084
1229
|
* Cancel a running task
|
|
1085
1230
|
*/
|
|
@@ -1746,6 +1891,90 @@ async function createServer(projectPath, port) {
|
|
|
1746
1891
|
res.status(500).json({ error: String(error) });
|
|
1747
1892
|
}
|
|
1748
1893
|
});
|
|
1894
|
+
app.post("/api/tasks/:id/merge", (req, res) => {
|
|
1895
|
+
try {
|
|
1896
|
+
const result = executor.mergeTask(req.params.id);
|
|
1897
|
+
if (!result.success) {
|
|
1898
|
+
res.status(400).json({ error: result.error });
|
|
1899
|
+
return;
|
|
1900
|
+
}
|
|
1901
|
+
const task = getTaskById(projectPath, req.params.id);
|
|
1902
|
+
if (task) {
|
|
1903
|
+
io.emit("task:updated", task);
|
|
1904
|
+
}
|
|
1905
|
+
res.json({ success: true });
|
|
1906
|
+
} catch (error) {
|
|
1907
|
+
res.status(500).json({ error: String(error) });
|
|
1908
|
+
}
|
|
1909
|
+
});
|
|
1910
|
+
app.post("/api/tasks/:id/create-pr", (req, res) => {
|
|
1911
|
+
try {
|
|
1912
|
+
const result = executor.createPR(req.params.id);
|
|
1913
|
+
if (!result.success) {
|
|
1914
|
+
res.status(400).json({ error: result.error });
|
|
1915
|
+
return;
|
|
1916
|
+
}
|
|
1917
|
+
const task = getTaskById(projectPath, req.params.id);
|
|
1918
|
+
if (task) {
|
|
1919
|
+
io.emit("task:updated", task);
|
|
1920
|
+
}
|
|
1921
|
+
res.json({ success: true, prUrl: result.prUrl });
|
|
1922
|
+
} catch (error) {
|
|
1923
|
+
res.status(500).json({ error: String(error) });
|
|
1924
|
+
}
|
|
1925
|
+
});
|
|
1926
|
+
app.post("/api/tasks/:id/discard", (req, res) => {
|
|
1927
|
+
try {
|
|
1928
|
+
const result = executor.discardTask(req.params.id);
|
|
1929
|
+
if (!result.success) {
|
|
1930
|
+
res.status(400).json({ error: result.error });
|
|
1931
|
+
return;
|
|
1932
|
+
}
|
|
1933
|
+
const task = getTaskById(projectPath, req.params.id);
|
|
1934
|
+
if (task) {
|
|
1935
|
+
io.emit("task:updated", task);
|
|
1936
|
+
}
|
|
1937
|
+
res.json({ success: true });
|
|
1938
|
+
} catch (error) {
|
|
1939
|
+
res.status(500).json({ error: String(error) });
|
|
1940
|
+
}
|
|
1941
|
+
});
|
|
1942
|
+
app.get("/api/tasks/:id/diff", (req, res) => {
|
|
1943
|
+
try {
|
|
1944
|
+
const result = executor.getTaskDiff(req.params.id);
|
|
1945
|
+
if (!result.success) {
|
|
1946
|
+
res.status(400).json({ error: result.error });
|
|
1947
|
+
return;
|
|
1948
|
+
}
|
|
1949
|
+
res.json({ diff: result.diff });
|
|
1950
|
+
} catch (error) {
|
|
1951
|
+
res.status(500).json({ error: String(error) });
|
|
1952
|
+
}
|
|
1953
|
+
});
|
|
1954
|
+
app.get("/api/tasks/:id/changed-files", (req, res) => {
|
|
1955
|
+
try {
|
|
1956
|
+
const result = executor.getTaskChangedFiles(req.params.id);
|
|
1957
|
+
if (!result.success) {
|
|
1958
|
+
res.status(400).json({ error: result.error });
|
|
1959
|
+
return;
|
|
1960
|
+
}
|
|
1961
|
+
res.json({ files: result.files });
|
|
1962
|
+
} catch (error) {
|
|
1963
|
+
res.status(500).json({ error: String(error) });
|
|
1964
|
+
}
|
|
1965
|
+
});
|
|
1966
|
+
app.get("/api/tasks/:id/review-info", (req, res) => {
|
|
1967
|
+
try {
|
|
1968
|
+
const info = executor.getReviewingTask(req.params.id);
|
|
1969
|
+
if (!info) {
|
|
1970
|
+
res.status(404).json({ error: "Task not in review" });
|
|
1971
|
+
return;
|
|
1972
|
+
}
|
|
1973
|
+
res.json(info);
|
|
1974
|
+
} catch (error) {
|
|
1975
|
+
res.status(500).json({ error: String(error) });
|
|
1976
|
+
}
|
|
1977
|
+
});
|
|
1749
1978
|
app.get("/api/tasks/:id/logs", (req, res) => {
|
|
1750
1979
|
try {
|
|
1751
1980
|
const logs = executor.getTaskLog(req.params.id);
|
|
@@ -2055,6 +2284,7 @@ function getClientHTML() {
|
|
|
2055
2284
|
.status-dot-draft { background: #a3a3a3; }
|
|
2056
2285
|
.status-dot-ready { background: #3b82f6; }
|
|
2057
2286
|
.status-dot-in_progress { background: #f97316; }
|
|
2287
|
+
.status-dot-in_review { background: #8b5cf6; }
|
|
2058
2288
|
.status-dot-completed { background: #22c55e; }
|
|
2059
2289
|
.status-dot-failed { background: #ef4444; }
|
|
2060
2290
|
|
|
@@ -2264,6 +2494,7 @@ function getClientHTML() {
|
|
|
2264
2494
|
.status-badge-draft { background: #f5f5f5; color: #737373; }
|
|
2265
2495
|
.status-badge-ready { background: #eff6ff; color: #3b82f6; }
|
|
2266
2496
|
.status-badge-in_progress { background: #fff7ed; color: #f97316; }
|
|
2497
|
+
.status-badge-in_review { background: #f5f3ff; color: #8b5cf6; }
|
|
2267
2498
|
.status-badge-completed { background: #f0fdf4; color: #22c55e; }
|
|
2268
2499
|
.status-badge-failed { background: #fef2f2; color: #ef4444; }
|
|
2269
2500
|
|
|
@@ -2589,11 +2820,16 @@ socket.on('task:output', ({ taskId, line }) => {
|
|
|
2589
2820
|
}
|
|
2590
2821
|
});
|
|
2591
2822
|
|
|
2592
|
-
socket.on('task:completed', ({ taskId }) => {
|
|
2823
|
+
socket.on('task:completed', ({ taskId, status }) => {
|
|
2593
2824
|
state.running = state.running.filter(id => id !== taskId);
|
|
2594
2825
|
const task = state.tasks.find(t => t.id === taskId);
|
|
2595
|
-
if (task) {
|
|
2596
|
-
|
|
2826
|
+
if (task) {
|
|
2827
|
+
// Use status from server (could be 'in_review' or 'completed')
|
|
2828
|
+
task.status = status || 'in_review';
|
|
2829
|
+
task.passes = true;
|
|
2830
|
+
}
|
|
2831
|
+
const statusMsg = (status === 'in_review') ? 'ready for review' : 'completed';
|
|
2832
|
+
showToast('Task ' + statusMsg + ': ' + (task?.title || taskId), 'success');
|
|
2597
2833
|
render();
|
|
2598
2834
|
});
|
|
2599
2835
|
|
|
@@ -2667,6 +2903,53 @@ async function retryTask(id) {
|
|
|
2667
2903
|
});
|
|
2668
2904
|
}
|
|
2669
2905
|
|
|
2906
|
+
async function mergeTask(id) {
|
|
2907
|
+
const res = await fetch('/api/tasks/' + id + '/merge', { method: 'POST' });
|
|
2908
|
+
const data = await res.json();
|
|
2909
|
+
if (!res.ok) {
|
|
2910
|
+
throw new Error(data.error || 'Merge failed');
|
|
2911
|
+
}
|
|
2912
|
+
return data;
|
|
2913
|
+
}
|
|
2914
|
+
|
|
2915
|
+
async function createPR(id) {
|
|
2916
|
+
const res = await fetch('/api/tasks/' + id + '/create-pr', { method: 'POST' });
|
|
2917
|
+
const data = await res.json();
|
|
2918
|
+
if (!res.ok) {
|
|
2919
|
+
throw new Error(data.error || 'PR creation failed');
|
|
2920
|
+
}
|
|
2921
|
+
return data;
|
|
2922
|
+
}
|
|
2923
|
+
|
|
2924
|
+
async function discardTask(id) {
|
|
2925
|
+
const res = await fetch('/api/tasks/' + id + '/discard', { method: 'POST' });
|
|
2926
|
+
const data = await res.json();
|
|
2927
|
+
if (!res.ok) {
|
|
2928
|
+
throw new Error(data.error || 'Discard failed');
|
|
2929
|
+
}
|
|
2930
|
+
return data;
|
|
2931
|
+
}
|
|
2932
|
+
|
|
2933
|
+
async function getReviewInfo(id) {
|
|
2934
|
+
const res = await fetch('/api/tasks/' + id + '/review-info');
|
|
2935
|
+
const data = await res.json();
|
|
2936
|
+
if (!res.ok) {
|
|
2937
|
+
throw new Error(data.error || 'Failed to get review info');
|
|
2938
|
+
}
|
|
2939
|
+
return data;
|
|
2940
|
+
}
|
|
2941
|
+
|
|
2942
|
+
async function openInVSCode(id) {
|
|
2943
|
+
try {
|
|
2944
|
+
const info = await getReviewInfo(id);
|
|
2945
|
+
// Open VS Code at the worktree path
|
|
2946
|
+
window.open('vscode://file/' + encodeURIComponent(info.worktreePath), '_blank');
|
|
2947
|
+
showToast('Opening in VS Code...', 'info');
|
|
2948
|
+
} catch (e) {
|
|
2949
|
+
showToast('Failed to open in VS Code: ' + e.message, 'error');
|
|
2950
|
+
}
|
|
2951
|
+
}
|
|
2952
|
+
|
|
2670
2953
|
async function generateTask(prompt) {
|
|
2671
2954
|
state.aiGenerating = true;
|
|
2672
2955
|
render();
|
|
@@ -2870,6 +3153,7 @@ function renderColumn(status, title, tasks) {
|
|
|
2870
3153
|
draft: 'To Do',
|
|
2871
3154
|
ready: 'Ready',
|
|
2872
3155
|
in_progress: 'In Progress',
|
|
3156
|
+
in_review: 'In Review',
|
|
2873
3157
|
completed: 'Done',
|
|
2874
3158
|
failed: 'Failed'
|
|
2875
3159
|
};
|
|
@@ -3235,7 +3519,7 @@ function renderSidePanel() {
|
|
|
3235
3519
|
</div>
|
|
3236
3520
|
|
|
3237
3521
|
<!-- Action Buttons -->
|
|
3238
|
-
<div class="px-5 py-4 border-b border-canvas-200 flex gap-2 flex-shrink-0">
|
|
3522
|
+
<div class="px-5 py-4 border-b border-canvas-200 flex flex-wrap gap-2 flex-shrink-0">
|
|
3239
3523
|
\${task.status === 'draft' ? \`
|
|
3240
3524
|
<button onclick="updateTask('\${task.id}', { status: 'ready' })" class="btn btn-primary px-4 py-2 text-sm">
|
|
3241
3525
|
\u2192 Move to Ready
|
|
@@ -3251,6 +3535,20 @@ function renderSidePanel() {
|
|
|
3251
3535
|
\u23F9 Stop Attempt
|
|
3252
3536
|
</button>
|
|
3253
3537
|
\` : ''}
|
|
3538
|
+
\${task.status === 'in_review' ? \`
|
|
3539
|
+
<button onclick="handleMerge('\${task.id}')" class="btn btn-primary px-4 py-2 text-sm">
|
|
3540
|
+
\u2713 Merge
|
|
3541
|
+
</button>
|
|
3542
|
+
<button onclick="handleCreatePR('\${task.id}')" class="btn btn-ghost px-4 py-2 text-sm">
|
|
3543
|
+
\u21E1 Create PR
|
|
3544
|
+
</button>
|
|
3545
|
+
<button onclick="openInVSCode('\${task.id}')" class="btn btn-ghost px-4 py-2 text-sm">
|
|
3546
|
+
\u{1F4C2} Open in VS Code
|
|
3547
|
+
</button>
|
|
3548
|
+
<button onclick="handleDiscard('\${task.id}')" class="btn btn-danger px-4 py-2 text-sm">
|
|
3549
|
+
\u2715 Discard
|
|
3550
|
+
</button>
|
|
3551
|
+
\` : ''}
|
|
3254
3552
|
\${task.status === 'failed' ? \`
|
|
3255
3553
|
<button onclick="retryTask('\${task.id}')" class="btn btn-primary px-4 py-2 text-sm">
|
|
3256
3554
|
\u21BB Retry
|
|
@@ -3451,6 +3749,41 @@ function handleStartAFK() {
|
|
|
3451
3749
|
render();
|
|
3452
3750
|
}
|
|
3453
3751
|
|
|
3752
|
+
async function handleMerge(taskId) {
|
|
3753
|
+
if (!confirm('Merge this task into the main branch?')) return;
|
|
3754
|
+
try {
|
|
3755
|
+
await mergeTask(taskId);
|
|
3756
|
+
showToast('Task merged successfully!', 'success');
|
|
3757
|
+
closeSidePanel();
|
|
3758
|
+
} catch (e) {
|
|
3759
|
+
showToast('Merge failed: ' + e.message, 'error');
|
|
3760
|
+
}
|
|
3761
|
+
}
|
|
3762
|
+
|
|
3763
|
+
async function handleCreatePR(taskId) {
|
|
3764
|
+
try {
|
|
3765
|
+
const result = await createPR(taskId);
|
|
3766
|
+
showToast('PR created successfully!', 'success');
|
|
3767
|
+
if (result.prUrl) {
|
|
3768
|
+
window.open(result.prUrl, '_blank');
|
|
3769
|
+
}
|
|
3770
|
+
closeSidePanel();
|
|
3771
|
+
} catch (e) {
|
|
3772
|
+
showToast('PR creation failed: ' + e.message, 'error');
|
|
3773
|
+
}
|
|
3774
|
+
}
|
|
3775
|
+
|
|
3776
|
+
async function handleDiscard(taskId) {
|
|
3777
|
+
if (!confirm('Discard this work? The task will return to Ready status.')) return;
|
|
3778
|
+
try {
|
|
3779
|
+
await discardTask(taskId);
|
|
3780
|
+
showToast('Task discarded, returned to Ready', 'warning');
|
|
3781
|
+
closeSidePanel();
|
|
3782
|
+
} catch (e) {
|
|
3783
|
+
showToast('Discard failed: ' + e.message, 'error');
|
|
3784
|
+
}
|
|
3785
|
+
}
|
|
3786
|
+
|
|
3454
3787
|
// Filter tasks based on search query
|
|
3455
3788
|
function filterTasks(tasks) {
|
|
3456
3789
|
if (!state.searchQuery) return tasks;
|
|
@@ -3512,6 +3845,7 @@ function render() {
|
|
|
3512
3845
|
\${renderColumn('draft', 'To Do', filterTasks(state.tasks))}
|
|
3513
3846
|
\${renderColumn('ready', 'Ready', filterTasks(state.tasks))}
|
|
3514
3847
|
\${renderColumn('in_progress', 'In Progress', filterTasks(state.tasks))}
|
|
3848
|
+
\${renderColumn('in_review', 'In Review', filterTasks(state.tasks))}
|
|
3515
3849
|
\${renderColumn('completed', 'Done', filterTasks(state.tasks))}
|
|
3516
3850
|
\${renderColumn('failed', 'Failed', filterTasks(state.tasks))}
|
|
3517
3851
|
</div>
|
|
@@ -3576,6 +3910,14 @@ window.closeSidePanel = closeSidePanel;
|
|
|
3576
3910
|
window.showTaskMenu = showTaskMenu;
|
|
3577
3911
|
window.filterTasks = filterTasks;
|
|
3578
3912
|
window.scrollSidePanelLog = scrollSidePanelLog;
|
|
3913
|
+
window.mergeTask = mergeTask;
|
|
3914
|
+
window.createPR = createPR;
|
|
3915
|
+
window.discardTask = discardTask;
|
|
3916
|
+
window.handleMerge = handleMerge;
|
|
3917
|
+
window.handleCreatePR = handleCreatePR;
|
|
3918
|
+
window.handleDiscard = handleDiscard;
|
|
3919
|
+
window.openInVSCode = openInVSCode;
|
|
3920
|
+
window.getReviewInfo = getReviewInfo;
|
|
3579
3921
|
|
|
3580
3922
|
// Keyboard shortcuts
|
|
3581
3923
|
document.addEventListener('keydown', (e) => {
|
|
@@ -3650,6 +3992,49 @@ async function findAvailablePort(startPort, maxAttempts = 10) {
|
|
|
3650
3992
|
}
|
|
3651
3993
|
|
|
3652
3994
|
// src/bin/cli.ts
|
|
3995
|
+
function isGitRepo(dir) {
|
|
3996
|
+
return existsSync4(join6(dir, ".git"));
|
|
3997
|
+
}
|
|
3998
|
+
function hasGitRemote(dir) {
|
|
3999
|
+
try {
|
|
4000
|
+
const result = execSync2("git remote -v", { cwd: dir, stdio: "pipe" }).toString();
|
|
4001
|
+
return result.trim().length > 0;
|
|
4002
|
+
} catch {
|
|
4003
|
+
return false;
|
|
4004
|
+
}
|
|
4005
|
+
}
|
|
4006
|
+
function detectRemoteType(dir) {
|
|
4007
|
+
try {
|
|
4008
|
+
const result = execSync2("git remote get-url origin", { cwd: dir, stdio: "pipe" }).toString().toLowerCase();
|
|
4009
|
+
if (result.includes("github.com")) return "github";
|
|
4010
|
+
if (result.includes("gitlab.com") || result.includes("gitlab")) return "gitlab";
|
|
4011
|
+
if (result.includes("bitbucket.org") || result.includes("bitbucket")) return "bitbucket";
|
|
4012
|
+
if (result.trim()) return "other";
|
|
4013
|
+
return null;
|
|
4014
|
+
} catch {
|
|
4015
|
+
return null;
|
|
4016
|
+
}
|
|
4017
|
+
}
|
|
4018
|
+
function initializeGit(dir) {
|
|
4019
|
+
execSync2("git init", { cwd: dir, stdio: "pipe" });
|
|
4020
|
+
try {
|
|
4021
|
+
execSync2("git add -A", { cwd: dir, stdio: "pipe" });
|
|
4022
|
+
execSync2('git commit -m "Initial commit"', { cwd: dir, stdio: "pipe" });
|
|
4023
|
+
} catch {
|
|
4024
|
+
}
|
|
4025
|
+
}
|
|
4026
|
+
async function promptYesNo(question) {
|
|
4027
|
+
const rl = createInterface({
|
|
4028
|
+
input: process.stdin,
|
|
4029
|
+
output: process.stdout
|
|
4030
|
+
});
|
|
4031
|
+
return new Promise((resolve) => {
|
|
4032
|
+
rl.question(question, (answer) => {
|
|
4033
|
+
rl.close();
|
|
4034
|
+
resolve(answer.toLowerCase().startsWith("y"));
|
|
4035
|
+
});
|
|
4036
|
+
});
|
|
4037
|
+
}
|
|
3653
4038
|
var VERSION = "0.1.0";
|
|
3654
4039
|
var banner = `
|
|
3655
4040
|
${chalk.cyan("\u2554\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2557")}
|
|
@@ -3662,6 +4047,32 @@ async function main() {
|
|
|
3662
4047
|
program.name("claude-kanban").description("Visual Kanban board for AI-powered development with Claude").version(VERSION).option("-p, --port <number>", "Port to run server on", "4242").option("-n, --no-open", "Do not auto-open browser").option("--init", "Re-initialize project files").option("--reset", "Reset all tasks (keeps config)").action(async (options) => {
|
|
3663
4048
|
console.log(banner);
|
|
3664
4049
|
const cwd = process.cwd();
|
|
4050
|
+
if (!isGitRepo(cwd)) {
|
|
4051
|
+
console.log(chalk.yellow("\n\u26A0 This directory is not a git repository."));
|
|
4052
|
+
console.log(chalk.gray("Git is required for task isolation and parallel execution.\n"));
|
|
4053
|
+
const shouldInit = await promptYesNo(chalk.white("Would you like to initialize git now? (y/n): "));
|
|
4054
|
+
if (shouldInit) {
|
|
4055
|
+
try {
|
|
4056
|
+
console.log(chalk.gray("Initializing git repository..."));
|
|
4057
|
+
initializeGit(cwd);
|
|
4058
|
+
console.log(chalk.green("\u2713 Git repository initialized"));
|
|
4059
|
+
} catch (error) {
|
|
4060
|
+
console.error(chalk.red("Failed to initialize git:"), error);
|
|
4061
|
+
process.exit(1);
|
|
4062
|
+
}
|
|
4063
|
+
} else {
|
|
4064
|
+
console.log(chalk.red("\nGit is required to run Claude Kanban."));
|
|
4065
|
+
console.log(chalk.gray('Please run "git init" manually and try again.'));
|
|
4066
|
+
process.exit(1);
|
|
4067
|
+
}
|
|
4068
|
+
} else {
|
|
4069
|
+
const remoteType = detectRemoteType(cwd);
|
|
4070
|
+
if (remoteType) {
|
|
4071
|
+
console.log(chalk.gray(`Git remote detected: ${remoteType}`));
|
|
4072
|
+
} else if (!hasGitRemote(cwd)) {
|
|
4073
|
+
console.log(chalk.gray("Git repository (local only, no remote)"));
|
|
4074
|
+
}
|
|
4075
|
+
}
|
|
3665
4076
|
const initialized = await isProjectInitialized(cwd);
|
|
3666
4077
|
if (!initialized || options.init) {
|
|
3667
4078
|
console.log(chalk.yellow("Initializing project..."));
|