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 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
- - Set "status": "completed"
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
- const merged = this.mergeWorktreeBranch(taskId);
1028
- if (merged) {
1029
- console.log(`[executor] Successfully merged ${runningTask.branchName}`);
1030
- } else {
1031
- console.log(`[executor] Failed to merge ${runningTask.branchName}, branch preserved for manual merge`);
1032
- }
1033
- this.removeWorktree(taskId);
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: "completed",
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: "completed",
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) { task.status = 'completed'; task.passes = true; }
2596
- showToast('Task completed: ' + (task?.title || taskId), 'success');
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..."));