kandown 0.33.2 → 0.33.4

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/bin/kandown.js CHANGED
@@ -5,9 +5,9 @@ if (typeof globalThis.require === 'undefined') {
5
5
  }
6
6
 
7
7
  // src/cli/cli.ts
8
- import { existsSync as existsSync6, readFileSync as readFileSync6, copyFileSync as copyFileSync2 } from "fs";
9
- import { join as join6, resolve as resolve3, basename } from "path";
10
- import { spawn as spawn4 } from "child_process";
8
+ import { existsSync as existsSync8, readFileSync as readFileSync8, copyFileSync as copyFileSync3, mkdirSync as mkdirSync5, readdirSync as readdirSync3 } from "fs";
9
+ import { join as join8, resolve as resolve2, basename } from "path";
10
+ import { spawn as spawn4, spawnSync } from "child_process";
11
11
 
12
12
  // src/cli/lib/updater.ts
13
13
  import { existsSync, readFileSync, writeFileSync, unlinkSync, statSync, mkdirSync } from "fs";
@@ -16,7 +16,7 @@ import { spawn, execSync } from "child_process";
16
16
  import { homedir } from "os";
17
17
 
18
18
  // src/lib/version.ts
19
- var KANDOWN_VERSION = "0.33.2";
19
+ var KANDOWN_VERSION = "0.33.4";
20
20
 
21
21
  // src/cli/lib/updater.ts
22
22
  import { fileURLToPath } from "url";
@@ -99,6 +99,31 @@ function resolveKandownBin() {
99
99
  }
100
100
  return null;
101
101
  }
102
+ async function readInstalledKandownVersion(targetVersion) {
103
+ const localVersion = getCurrentVersion();
104
+ const bin = resolveKandownBin();
105
+ if (!bin) return localVersion;
106
+ return await new Promise((resolveVersion) => {
107
+ const child = spawn(bin, ["--version"], {
108
+ timeout: 5e3,
109
+ stdio: ["pipe", "pipe", "pipe"],
110
+ env: { ...process.env, KANDOWN_NO_UPDATE: "1" },
111
+ detached: false
112
+ });
113
+ let stdout = "";
114
+ child.stdout.on("data", (d) => {
115
+ stdout += d;
116
+ });
117
+ child.stderr.on("data", () => {
118
+ });
119
+ child.on("error", () => resolveVersion(localVersion));
120
+ child.on("close", (code) => {
121
+ if (code !== 0) return resolveVersion(localVersion);
122
+ const match = stdout.trim().match(/v?(\d+\.\d+\.\d+(?:-[\w.-]+)?)/);
123
+ resolveVersion(match ? match[1] : localVersion);
124
+ });
125
+ });
126
+ }
102
127
  function updateCheckedRecently() {
103
128
  try {
104
129
  if (!existsSync(UPDATE_CHECK_CACHE)) return false;
@@ -115,6 +140,10 @@ function rememberUpdateCheck() {
115
140
  } catch {
116
141
  }
117
142
  }
143
+ function requestedSemver(packageSpec) {
144
+ const match = packageSpec.match(/@(\d+\.\d+\.\d+(?:-[\w.-]+)?)$/);
145
+ return match ? match[1] : null;
146
+ }
118
147
  async function performGlobalPackageUpdate(packageSpec) {
119
148
  const cleanEnv = { ...process.env };
120
149
  for (const k of Object.keys(cleanEnv)) {
@@ -122,6 +151,12 @@ async function performGlobalPackageUpdate(packageSpec) {
122
151
  delete cleanEnv[k];
123
152
  }
124
153
  }
154
+ const targetVersion = requestedSemver(packageSpec);
155
+ const verifyInstalledVersion = async () => {
156
+ if (!targetVersion) return true;
157
+ const installedVersion = await readInstalledKandownVersion(targetVersion);
158
+ return semverGt(installedVersion, targetVersion) >= 0;
159
+ };
125
160
  const tryPkgCmd = (cmd, args) => {
126
161
  return new Promise((res) => {
127
162
  const child = spawn(cmd, args, {
@@ -138,17 +173,26 @@ async function performGlobalPackageUpdate(packageSpec) {
138
173
  child.on("close", (code) => res(code === 0));
139
174
  });
140
175
  };
176
+ const tryPkgCmdAndVerify = async (cmd, args) => {
177
+ if (!await tryPkgCmd(cmd, args)) return false;
178
+ return verifyInstalledVersion();
179
+ };
141
180
  const currentBin = resolveKandownBin() || "";
181
+ const currentBinDir = currentBin ? dirname(currentBin) : null;
182
+ const siblingNpm = currentBinDir ? join(currentBinDir, "npm") : null;
183
+ const siblingPnpm = currentBinDir ? join(currentBinDir, "pnpm") : null;
142
184
  const isPnpmInstall = currentBin.includes("pnpm");
185
+ if (siblingPnpm && existsSync(siblingPnpm) && await tryPkgCmdAndVerify(siblingPnpm, ["add", "-g", packageSpec])) return true;
186
+ if (siblingNpm && existsSync(siblingNpm) && await tryPkgCmdAndVerify(siblingNpm, ["install", "-g", packageSpec, "--force"])) return true;
143
187
  if (isPnpmInstall) {
144
- if (await tryPkgCmd("pnpm", ["add", "-g", packageSpec])) return true;
145
- if (await tryPkgCmd("npm", ["install", "-g", packageSpec, "--force"])) return true;
188
+ if (await tryPkgCmdAndVerify("pnpm", ["add", "-g", packageSpec])) return true;
189
+ if (await tryPkgCmdAndVerify("npm", ["install", "-g", packageSpec, "--force"])) return true;
146
190
  } else {
147
- if (await tryPkgCmd("npm", ["install", "-g", packageSpec, "--force"])) return true;
148
- if (await tryPkgCmd("pnpm", ["add", "-g", packageSpec])) return true;
191
+ if (await tryPkgCmdAndVerify("npm", ["install", "-g", packageSpec, "--force"])) return true;
192
+ if (await tryPkgCmdAndVerify("pnpm", ["add", "-g", packageSpec])) return true;
149
193
  }
150
- if (await tryPkgCmd("yarn", ["global", "add", packageSpec])) return true;
151
- return await tryPkgCmd("bun", ["add", "-g", packageSpec]);
194
+ if (await tryPkgCmdAndVerify("yarn", ["global", "add", packageSpec])) return true;
195
+ return await tryPkgCmdAndVerify("bun", ["add", "-g", packageSpec]);
152
196
  }
153
197
  async function checkForUpdate(argv = process.argv) {
154
198
  if (process.env.KANDOWN_NO_UPDATE === "1") return;
@@ -165,7 +209,7 @@ async function checkForUpdate(argv = process.argv) {
165
209
  }
166
210
  } catch {
167
211
  }
168
- const latest = await new Promise((resolve4) => {
212
+ const latest = await new Promise((resolve3) => {
169
213
  const child2 = spawn("npm", ["view", "kandown", "version"], {
170
214
  timeout: 6e3,
171
215
  stdio: ["pipe", "pipe", "pipe"],
@@ -178,11 +222,11 @@ async function checkForUpdate(argv = process.argv) {
178
222
  });
179
223
  child2.stderr.on("data", () => {
180
224
  });
181
- child2.on("error", () => resolve4(null));
225
+ child2.on("error", () => resolve3(null));
182
226
  child2.on("close", (code) => {
183
- if (code !== 0) return resolve4(null);
227
+ if (code !== 0) return resolve3(null);
184
228
  const v = stdout.trim().replace(/^"|"$/g, "");
185
- resolve4(v || null);
229
+ resolve3(v || null);
186
230
  });
187
231
  });
188
232
  if (!latest) return;
@@ -547,14 +591,14 @@ function readBoard(kandownDir) {
547
591
  };
548
592
  }
549
593
  function readTask(kandownDir, taskId) {
550
- const taskPath = join3(getTasksDir(kandownDir), `${taskId}.md`);
551
- if (!existsSync3(taskPath)) {
594
+ const taskPath2 = join3(getTasksDir(kandownDir), `${taskId}.md`);
595
+ if (!existsSync3(taskPath2)) {
552
596
  return {
553
597
  frontmatter: { id: taskId, title: `Task ${taskId}`, status: "Backlog" },
554
598
  body: ""
555
599
  };
556
600
  }
557
- const content = readFileSync3(taskPath, "utf8");
601
+ const content = readFileSync3(taskPath2, "utf8");
558
602
  const parsed = parseTaskFile(content);
559
603
  return {
560
604
  ...parsed,
@@ -608,21 +652,21 @@ ${gitLog}
608
652
  return sections.filter(Boolean).join("\n\n---\n\n");
609
653
  }
610
654
  function moveTaskToColumn(kandownDir, taskId, targetColumn) {
611
- const taskPath = join3(getTasksDir(kandownDir), `${taskId}.md`);
612
- if (!existsSync3(taskPath)) return false;
655
+ const taskPath2 = join3(getTasksDir(kandownDir), `${taskId}.md`);
656
+ if (!existsSync3(taskPath2)) return false;
613
657
  try {
614
- const prevContent = readFileSync3(taskPath, "utf8");
658
+ const prevContent = readFileSync3(taskPath2, "utf8");
615
659
  const parsed = readTask(kandownDir, taskId);
616
660
  const newContent = serializeTaskFile({
617
661
  ...parsed.frontmatter,
618
662
  id: taskId,
619
663
  status: targetColumn
620
664
  }, parsed.body);
621
- atomicWriteFileSync(taskPath, newContent);
665
+ atomicWriteFileSync(taskPath2, newContent);
622
666
  pushUndo(kandownDir, {
623
667
  type: "move",
624
668
  taskId,
625
- path: taskPath,
669
+ path: taskPath2,
626
670
  previousContent: prevContent,
627
671
  newContent,
628
672
  timestamp: Date.now()
@@ -652,6 +696,102 @@ function pushUndo(kandownDir, record) {
652
696
  } catch {
653
697
  }
654
698
  }
699
+ function createTaskInBoard(kandownDir, rawInput, status) {
700
+ const tasksDir = getTasksDir(kandownDir);
701
+ if (!existsSync3(tasksDir)) mkdirSync2(tasksDir, { recursive: true });
702
+ const ids = listTaskIds(kandownDir);
703
+ let maxN = 0;
704
+ for (const id of ids) {
705
+ const m = id.match(/^t(\d+)$/i);
706
+ if (m) {
707
+ const n = parseInt(m[1], 10);
708
+ if (Number.isFinite(n) && n > maxN) maxN = n;
709
+ }
710
+ }
711
+ const newId = `t${maxN + 1}`;
712
+ const config = loadConfig(kandownDir);
713
+ const targetStatus = status || (config.board.columns[0] ?? "Backlog");
714
+ let text = rawInput.trim();
715
+ let priority;
716
+ const tags = [];
717
+ let assignee;
718
+ let due;
719
+ const depends_on = [];
720
+ text = text.replace(/(?:^|\s)p([1-4])(?:\s|$)/i, (_, level) => {
721
+ priority = `P${level}`;
722
+ return " ";
723
+ });
724
+ text = text.replace(/(?:^|\s)#([a-zA-Z0-9_-]+)/g, (_, tag) => {
725
+ tags.push(tag.toLowerCase());
726
+ return " ";
727
+ });
728
+ text = text.replace(/(?:^|\s)@([a-zA-Z0-9_-]+)/g, (_, user) => {
729
+ assignee = user;
730
+ return " ";
731
+ });
732
+ text = text.replace(/(?:^|\s)due:([^\s]+)/i, (_, d) => {
733
+ due = d;
734
+ return " ";
735
+ });
736
+ text = text.replace(/(?:^|\s)\+([a-zA-Z0-9_-]+)/g, (_, depId) => {
737
+ depends_on.push(depId);
738
+ return " ";
739
+ });
740
+ const title = text.replace(/\s+/g, " ").trim() || rawInput;
741
+ const fm = {
742
+ id: newId,
743
+ title,
744
+ status: targetStatus,
745
+ created: (/* @__PURE__ */ new Date()).toISOString().slice(0, 10)
746
+ };
747
+ if (priority) fm.priority = priority;
748
+ if (assignee) fm.assignee = assignee;
749
+ if (tags.length > 0) fm.tags = tags;
750
+ if (due) fm.due = due;
751
+ if (depends_on.length > 0) fm.depends_on = depends_on;
752
+ const content = serializeTaskFile(fm, "");
753
+ const taskPath2 = join3(tasksDir, `${newId}.md`);
754
+ atomicWriteFileSync(taskPath2, content);
755
+ pushUndo(kandownDir, {
756
+ type: "create",
757
+ taskId: newId,
758
+ path: taskPath2,
759
+ previousContent: null,
760
+ newContent: content,
761
+ timestamp: Date.now()
762
+ });
763
+ return newId;
764
+ }
765
+ function archiveTaskInBoard(kandownDir, taskId) {
766
+ const tasksDir = getTasksDir(kandownDir);
767
+ const taskPath2 = join3(tasksDir, `${taskId}.md`);
768
+ if (!existsSync3(taskPath2)) return false;
769
+ try {
770
+ const prevContent = readFileSync3(taskPath2, "utf8");
771
+ const archiveDir = join3(tasksDir, "archive");
772
+ if (!existsSync3(archiveDir)) mkdirSync2(archiveDir, { recursive: true });
773
+ const parsed = readTask(kandownDir, taskId);
774
+ const newContent = serializeTaskFile({
775
+ ...parsed.frontmatter,
776
+ id: taskId,
777
+ archived: true
778
+ }, parsed.body);
779
+ const destPath = join3(archiveDir, `${taskId}.md`);
780
+ atomicWriteFileSync(destPath, newContent);
781
+ unlinkSync3(taskPath2);
782
+ pushUndo(kandownDir, {
783
+ type: "archive",
784
+ taskId,
785
+ path: destPath,
786
+ previousContent: prevContent,
787
+ newContent,
788
+ timestamp: Date.now()
789
+ });
790
+ return true;
791
+ } catch {
792
+ return false;
793
+ }
794
+ }
655
795
 
656
796
  // src/cli/lib/daemon.ts
657
797
  import { existsSync as existsSync4, readFileSync as readFileSync4, unlinkSync as unlinkSync4 } from "fs";
@@ -701,9 +841,10 @@ function isProcessAlive(pid) {
701
841
  }
702
842
  function parseRemoteDaemonInfo(value) {
703
843
  if (!isRecord(value)) return null;
704
- const { ok, pid, kandownDir } = value;
844
+ const { ok, pid, kandownDir, version } = value;
705
845
  if (ok !== true || typeof pid !== "number" || !Number.isInteger(pid) || typeof kandownDir !== "string") return null;
706
- return { ok, pid, kandownDir };
846
+ if (version !== null && typeof version !== "string" && version !== void 0) return null;
847
+ return { ok, pid, kandownDir, version: typeof version === "string" ? version : null };
707
848
  }
708
849
  async function fetchDaemonInfo(port) {
709
850
  try {
@@ -717,16 +858,16 @@ async function fetchDaemonInfo(port) {
717
858
  }
718
859
  }
719
860
  function isPortListening(port, timeoutMs = 400) {
720
- return new Promise((resolve4) => {
861
+ return new Promise((resolve3) => {
721
862
  const socket = createConnection({ port, host: "127.0.0.1" }, () => {
722
863
  socket.destroy();
723
- resolve4(true);
864
+ resolve3(true);
724
865
  });
725
- socket.on("error", () => resolve4(false));
866
+ socket.on("error", () => resolve3(false));
726
867
  socket.setTimeout(timeoutMs);
727
868
  socket.on("timeout", () => {
728
869
  socket.destroy();
729
- resolve4(false);
870
+ resolve3(false);
730
871
  });
731
872
  });
732
873
  }
@@ -745,7 +886,7 @@ async function getDaemonStatus(kandownDir) {
745
886
  removeDaemonMetadata(kandownDir);
746
887
  return { running: false, metadata: null };
747
888
  }
748
- return { running: true, metadata };
889
+ return { running: true, metadata: { ...metadata, version: remote.version ?? metadata.version } };
749
890
  }
750
891
  async function waitForDaemon(kandownDir, timeoutMs = 8e3) {
751
892
  const started = Date.now();
@@ -754,13 +895,16 @@ async function waitForDaemon(kandownDir, timeoutMs = 8e3) {
754
895
  if (metadata && isProcessAlive(metadata.pid) && await isPortListening(metadata.port)) {
755
896
  return { running: true, metadata };
756
897
  }
757
- await new Promise((resolve4) => setTimeout(resolve4, 120));
898
+ await new Promise((resolve3) => setTimeout(resolve3, 120));
758
899
  }
759
900
  return { running: false, metadata: null };
760
901
  }
761
902
  async function startProjectDaemon(kandownDir, preferredPort) {
762
903
  const current = await getDaemonStatus(kandownDir);
763
- if (current.running) return current;
904
+ if (current.running) {
905
+ if (current.metadata?.version === getCurrentVersion()) return current;
906
+ await stopProjectDaemon(kandownDir);
907
+ }
764
908
  const cliPath = process.argv[1];
765
909
  if (!cliPath) throw new Error("Cannot locate kandown CLI entrypoint");
766
910
  const args = [
@@ -783,13 +927,54 @@ async function startProjectDaemon(kandownDir, preferredPort) {
783
927
  child.unref();
784
928
  return waitForDaemon(kandownDir);
785
929
  }
930
+ async function isOwnedKandownDaemon(pid, port, kandownDir) {
931
+ const remote = await fetchDaemonInfo(port);
932
+ if (remote) return remote.pid === pid && remote.kandownDir === kandownDir;
933
+ try {
934
+ const cmd = execFileSync2("ps", ["-p", String(pid), "-o", "command="], {
935
+ encoding: "utf8",
936
+ timeout: 2e3
937
+ }).trim();
938
+ return /kandown/.test(cmd) && cmd.includes(kandownDir);
939
+ } catch {
940
+ return false;
941
+ }
942
+ }
943
+ async function stopProjectDaemon(kandownDir) {
944
+ const metadata = readDaemonMetadata(kandownDir);
945
+ if (!metadata) return false;
946
+ const pid = metadata.pid;
947
+ if (!isProcessAlive(pid)) {
948
+ removeDaemonMetadata(kandownDir);
949
+ return false;
950
+ }
951
+ if (!await isOwnedKandownDaemon(pid, metadata.port, kandownDir)) {
952
+ removeDaemonMetadata(kandownDir);
953
+ return false;
954
+ }
955
+ try {
956
+ process.kill(pid, "SIGTERM");
957
+ } catch {
958
+ }
959
+ const started = Date.now();
960
+ while (Date.now() - started < 2500 && isProcessAlive(pid)) {
961
+ await new Promise((resolve3) => setTimeout(resolve3, 100));
962
+ }
963
+ if (isProcessAlive(pid)) {
964
+ try {
965
+ process.kill(pid, "SIGKILL");
966
+ } catch {
967
+ }
968
+ }
969
+ removeDaemonMetadata(kandownDir);
970
+ return true;
971
+ }
786
972
 
787
973
  // src/cli/lib/server.ts
788
974
  import { createServer } from "http";
789
975
  import { existsSync as existsSync5, readFileSync as readFileSync5, copyFileSync, unlinkSync as unlinkSync5, mkdirSync as mkdirSync3 } from "fs";
790
- import { join as join5, resolve as resolve2, dirname as dirname4 } from "path";
791
- import { execSync as execSync2, spawn as spawn3 } from "child_process";
792
- import { homedir as homedir3 } from "os";
976
+ import { join as join5 } from "path";
977
+ import { spawn as spawn3 } from "child_process";
793
978
  var START_PORT_RANGE = 2050;
794
979
  var END_PORT_RANGE = 2099;
795
980
  var UNSAFE_PORTS = /* @__PURE__ */ new Set([2049, 4045, 6e3, 6665, 6666, 6667, 6668, 6669, 6697]);
@@ -826,8 +1011,7 @@ function writeText(res, status, text) {
826
1011
  function syncProjectKandownHtml(kandownDir) {
827
1012
  try {
828
1013
  const projectHtml = join5(kandownDir, "kandown.html");
829
- const cliRoot = resolve2(import.meta.url ? new URL("../..", import.meta.url).pathname : process.cwd());
830
- const distHtml = join5(cliRoot, "dist", "index.html");
1014
+ const distHtml = join5(PKG_ROOT, "dist", "index.html");
831
1015
  if (!existsSync5(distHtml)) return false;
832
1016
  if (!existsSync5(projectHtml)) {
833
1017
  copyFileSync(distHtml, projectHtml);
@@ -843,27 +1027,42 @@ function syncProjectKandownHtml(kandownDir) {
843
1027
  }
844
1028
  return false;
845
1029
  }
846
- function syncGlobalSymlinks() {
1030
+ function readDaemonPort(kandownDir) {
847
1031
  try {
848
- const home = homedir3();
849
- const candidatePaths = [
850
- join5(home, ".local", "bin", "kandown"),
851
- join5(home, "Library", "pnpm", "bin", "kandown"),
852
- join5(home, ".nvm", "versions", "node", "v25.2.1", "bin", "kandown")
853
- ];
854
- const currentBin = resolve2(import.meta.url ? new URL("../../bin/kandown.js", import.meta.url).pathname : process.cwd());
855
- for (const targetPath of candidatePaths) {
856
- if (existsSync5(dirname4(targetPath))) {
857
- try {
858
- if (existsSync5(targetPath)) unlinkSync5(targetPath);
859
- execSync2(`ln -sf "${currentBin}" "${targetPath}" 2>/dev/null || true`);
860
- } catch {
861
- }
862
- }
863
- }
1032
+ const raw = JSON.parse(readFileSync5(join5(kandownDir, "daemon.json"), "utf8"));
1033
+ return typeof raw.port === "number" && Number.isInteger(raw.port) ? raw.port : null;
864
1034
  } catch {
1035
+ return null;
865
1036
  }
866
1037
  }
1038
+ function restartDaemonAfterUpdateResponse(res, kandownDir) {
1039
+ const cliPath = process.argv[1];
1040
+ if (!cliPath) return;
1041
+ const args = ["--no-update-check", "daemon", "run", "--path", kandownDir];
1042
+ const port = readDaemonPort(kandownDir);
1043
+ if (port !== null) args.push("--port", String(port));
1044
+ const launcher = `
1045
+ const { spawn } = require('node:child_process');
1046
+ const [nodeBin, cliPath, ...cliArgs] = process.argv.slice(1);
1047
+ setTimeout(() => {
1048
+ const child = spawn(nodeBin, [cliPath, ...cliArgs], {
1049
+ detached: true,
1050
+ stdio: 'ignore',
1051
+ env: { ...process.env, KANDOWN_DAEMON: '1' },
1052
+ });
1053
+ child.unref();
1054
+ }, 350);
1055
+ `;
1056
+ res.on("finish", () => {
1057
+ const child = spawn3(process.execPath, ["-e", launcher, process.execPath, cliPath, ...args], {
1058
+ detached: true,
1059
+ stdio: "ignore",
1060
+ env: { ...process.env, KANDOWN_DAEMON: "1" }
1061
+ });
1062
+ child.unref();
1063
+ setTimeout(() => process.exit(0), 50).unref();
1064
+ });
1065
+ }
867
1066
  async function handleApi(req, res, url, kandownDir) {
868
1067
  const path = url.pathname;
869
1068
  const method = req.method || "GET";
@@ -884,7 +1083,7 @@ async function handleApi(req, res, url, kandownDir) {
884
1083
  }
885
1084
  if (path === "/api/update/check" && method === "GET") {
886
1085
  const current = getCurrentVersion();
887
- const latest = await new Promise((resolve4) => {
1086
+ const latest = await new Promise((resolve3) => {
888
1087
  const child = spawn3("npm", ["view", "kandown", "version"], {
889
1088
  timeout: 4e3,
890
1089
  stdio: ["pipe", "pipe", "pipe"],
@@ -897,10 +1096,10 @@ async function handleApi(req, res, url, kandownDir) {
897
1096
  });
898
1097
  child.stderr.on("data", () => {
899
1098
  });
900
- child.on("error", () => resolve4(null));
1099
+ child.on("error", () => resolve3(null));
901
1100
  child.on("close", (code) => {
902
- if (code !== 0) return resolve4(null);
903
- resolve4(stdout.trim().replace(/^"|"$/g, "") || null);
1101
+ if (code !== 0) return resolve3(null);
1102
+ resolve3(stdout.trim().replace(/^"|"$/g, "") || null);
904
1103
  });
905
1104
  });
906
1105
  const updateAvailable = latest ? semverGt(latest, current) > 0 : false;
@@ -912,7 +1111,7 @@ async function handleApi(req, res, url, kandownDir) {
912
1111
  }
913
1112
  if (path === "/api/update/apply" && method === "POST") {
914
1113
  const current = getCurrentVersion();
915
- const latest = await new Promise((resolve4) => {
1114
+ const latest = await new Promise((resolve3) => {
916
1115
  const child = spawn3("npm", ["view", "kandown", "version"], {
917
1116
  timeout: 4e3,
918
1117
  stdio: ["pipe", "pipe", "pipe"],
@@ -923,15 +1122,15 @@ async function handleApi(req, res, url, kandownDir) {
923
1122
  child.stdout.on("data", (d) => {
924
1123
  stdout += d;
925
1124
  });
926
- child.on("error", () => resolve4(null));
927
- child.on("close", (code) => resolve4(code === 0 ? stdout.trim() : null));
1125
+ child.on("error", () => resolve3(null));
1126
+ child.on("close", (code) => resolve3(code === 0 ? stdout.trim() : null));
928
1127
  });
929
1128
  const targetVersion = latest || current;
930
1129
  const ok = await performGlobalPackageUpdate(`kandown@${targetVersion}`);
931
- syncGlobalSymlinks();
932
1130
  syncProjectKandownHtml(kandownDir);
933
1131
  if (ok) {
934
- writeJson(res, 200, { ok: true, version: targetVersion, message: "Update installed successfully" });
1132
+ restartDaemonAfterUpdateResponse(res, kandownDir);
1133
+ writeJson(res, 200, { ok: true, version: targetVersion, message: "Update installed successfully; daemon is restarting" });
935
1134
  broadcastSseEvent({ type: "update", version: targetVersion });
936
1135
  } else {
937
1136
  writeJson(res, 500, { ok: false, message: "Global package installation failed" });
@@ -1003,10 +1202,10 @@ async function handleApi(req, res, url, kandownDir) {
1003
1202
  if (path.startsWith("/api/tasks/")) {
1004
1203
  const taskId = decodeURIComponent(path.slice("/api/tasks/".length));
1005
1204
  const tasksDir = getTasksDir(kandownDir);
1006
- const taskPath = join5(tasksDir, `${taskId}.md`);
1205
+ const taskPath2 = join5(tasksDir, `${taskId}.md`);
1007
1206
  if (method === "GET") {
1008
- if (!existsSync5(taskPath)) return writeText(res, 404, "Task not found");
1009
- return writeText(res, 200, readFileSync5(taskPath, "utf8"));
1207
+ if (!existsSync5(taskPath2)) return writeText(res, 404, "Task not found");
1208
+ return writeText(res, 200, readFileSync5(taskPath2, "utf8"));
1010
1209
  }
1011
1210
  if (method === "PUT") {
1012
1211
  let body = "";
@@ -1015,14 +1214,14 @@ async function handleApi(req, res, url, kandownDir) {
1015
1214
  });
1016
1215
  req.on("end", () => {
1017
1216
  if (!existsSync5(tasksDir)) mkdirSync3(tasksDir, { recursive: true });
1018
- atomicWriteFileSync(taskPath, body);
1217
+ atomicWriteFileSync(taskPath2, body);
1019
1218
  broadcastSseEvent({ type: "task", id: taskId });
1020
1219
  writeJson(res, 200, { ok: true });
1021
1220
  });
1022
1221
  return;
1023
1222
  }
1024
1223
  if (method === "DELETE") {
1025
- if (existsSync5(taskPath)) unlinkSync5(taskPath);
1224
+ if (existsSync5(taskPath2)) unlinkSync5(taskPath2);
1026
1225
  broadcastSseEvent({ type: "task_delete", id: taskId });
1027
1226
  return writeJson(res, 200, { ok: true });
1028
1227
  }
@@ -1041,7 +1240,6 @@ function serveApp(res, kandownDir) {
1041
1240
  }
1042
1241
  function createServeServer(kandownDir) {
1043
1242
  syncProjectKandownHtml(kandownDir);
1044
- syncGlobalSymlinks();
1045
1243
  return createServer((req, res) => {
1046
1244
  const url = new URL(req.url || "/", "http://localhost");
1047
1245
  if (req.method === "OPTIONS") return handleCors(res);
@@ -1084,6 +1282,281 @@ async function listenOnAvailablePort(kandownDir, preferredPort) {
1084
1282
  throw new Error(`No free port in range ${START_PORT_RANGE}-${END_PORT_RANGE}`);
1085
1283
  }
1086
1284
 
1285
+ // src/cli/lib/init.ts
1286
+ import { existsSync as existsSync6, readFileSync as readFileSync6, mkdirSync as mkdirSync4, copyFileSync as copyFileSync2, readdirSync as readdirSync2, statSync as statSync2 } from "fs";
1287
+ import { join as join6 } from "path";
1288
+ function copyRecursive(src, dest) {
1289
+ const errors = [];
1290
+ try {
1291
+ if (!existsSync6(dest)) mkdirSync4(dest, { recursive: true });
1292
+ const entries = readdirSync2(src);
1293
+ for (const entry of entries) {
1294
+ const srcPath = join6(src, entry);
1295
+ const destPath = join6(dest, entry);
1296
+ try {
1297
+ if (statSync2(srcPath).isDirectory()) {
1298
+ errors.push(...copyRecursive(srcPath, destPath));
1299
+ } else if (!existsSync6(destPath)) {
1300
+ copyFileSync2(srcPath, destPath);
1301
+ }
1302
+ } catch (error) {
1303
+ errors.push(`${entry}: ${error instanceof Error ? error.message : String(error)}`);
1304
+ }
1305
+ }
1306
+ } catch (error) {
1307
+ errors.push(`${src}: ${error instanceof Error ? error.message : String(error)}`);
1308
+ }
1309
+ return errors;
1310
+ }
1311
+ function syncKandownAgentDoc(kandownDir) {
1312
+ const source = join6(PKG_ROOT, "templates", "AGENT_KANDOWN.md");
1313
+ const target = join6(kandownDir, "AGENT_KANDOWN.md");
1314
+ if (!existsSync6(source)) return false;
1315
+ try {
1316
+ const expected = readFileSync6(source, "utf8");
1317
+ const existing = existsSync6(target) ? readFileSync6(target, "utf8") : null;
1318
+ if (existing === null || !existing.includes("# Kandown")) {
1319
+ atomicWriteFileSync(target, expected.endsWith("\n") ? expected : `${expected}
1320
+ `);
1321
+ return true;
1322
+ }
1323
+ } catch {
1324
+ }
1325
+ return false;
1326
+ }
1327
+ function doInit(kandownDir) {
1328
+ try {
1329
+ mkdirSync4(kandownDir, { recursive: true });
1330
+ const htmlSrc = join6(PKG_ROOT, "dist", "index.html");
1331
+ const htmlDest = join6(kandownDir, "kandown.html");
1332
+ if (existsSync6(htmlSrc)) {
1333
+ copyFileSync2(htmlSrc, htmlDest);
1334
+ }
1335
+ syncKandownAgentDoc(kandownDir);
1336
+ const templatesDir = join6(PKG_ROOT, "templates");
1337
+ if (existsSync6(templatesDir)) {
1338
+ if (!existsSync6(join6(kandownDir, "README.md")) && existsSync6(join6(templatesDir, "README.md"))) {
1339
+ copyFileSync2(join6(templatesDir, "README.md"), join6(kandownDir, "README.md"));
1340
+ }
1341
+ if (!existsSync6(join6(kandownDir, "AGENT.md")) && existsSync6(join6(templatesDir, "AGENT.md"))) {
1342
+ copyFileSync2(join6(templatesDir, "AGENT.md"), join6(kandownDir, "AGENT.md"));
1343
+ }
1344
+ const tasksSrc = join6(templatesDir, "tasks");
1345
+ const tasksDest = getTasksDir(kandownDir);
1346
+ if (!existsSync6(tasksDest) && existsSync6(tasksSrc)) {
1347
+ copyRecursive(tasksSrc, tasksDest);
1348
+ }
1349
+ if (!existsSync6(join6(kandownDir, "kandown.json")) && existsSync6(join6(templatesDir, "kandown.json"))) {
1350
+ copyFileSync2(join6(templatesDir, "kandown.json"), join6(kandownDir, "kandown.json"));
1351
+ }
1352
+ }
1353
+ return true;
1354
+ } catch (error) {
1355
+ console.error(`Init failed: ${error instanceof Error ? error.message : String(error)}`);
1356
+ return false;
1357
+ }
1358
+ }
1359
+
1360
+ // src/cli/lib/mcp.ts
1361
+ import { existsSync as existsSync7 } from "fs";
1362
+ import { join as join7 } from "path";
1363
+ function startMcpServer(kandownDir) {
1364
+ process.stdin.setEncoding("utf8");
1365
+ let buffer = "";
1366
+ process.stdin.on("data", (chunk) => {
1367
+ buffer += chunk;
1368
+ const lines = buffer.split("\n");
1369
+ buffer = lines.pop() ?? "";
1370
+ for (const line of lines) {
1371
+ const trimmed = line.trim();
1372
+ if (!trimmed) continue;
1373
+ try {
1374
+ const req = JSON.parse(trimmed);
1375
+ handleJsonRpc(kandownDir, req);
1376
+ } catch (e) {
1377
+ sendResponse(null, { error: { code: -32700, message: "Parse error" } });
1378
+ }
1379
+ }
1380
+ });
1381
+ }
1382
+ function sendResponse(id, resultOrError) {
1383
+ if (id === void 0) return;
1384
+ const resp = {
1385
+ jsonrpc: "2.0",
1386
+ id,
1387
+ ...resultOrError
1388
+ };
1389
+ process.stdout.write(JSON.stringify(resp) + "\n");
1390
+ }
1391
+ function handleJsonRpc(kandownDir, req) {
1392
+ const { id, method, params } = req;
1393
+ if (method === "initialize") {
1394
+ sendResponse(id, {
1395
+ result: {
1396
+ protocolVersion: "2024-11-05",
1397
+ capabilities: { tools: {} },
1398
+ serverInfo: { name: "kandown", version: "0.20.0" }
1399
+ }
1400
+ });
1401
+ return;
1402
+ }
1403
+ if (method === "notifications/initialized") {
1404
+ return;
1405
+ }
1406
+ if (method === "tools/list") {
1407
+ sendResponse(id, {
1408
+ result: {
1409
+ tools: [
1410
+ {
1411
+ name: "list_tasks",
1412
+ description: "List all tasks on the Kandown board with optional filtering",
1413
+ inputSchema: {
1414
+ type: "object",
1415
+ properties: {
1416
+ status: { type: "string", description: "Filter by column/status name" },
1417
+ assignee: { type: "string", description: "Filter by assignee" },
1418
+ tag: { type: "string", description: "Filter by tag" },
1419
+ priority: { type: "string", description: "Filter by priority (P1, P2, P3, P4)" }
1420
+ }
1421
+ }
1422
+ },
1423
+ {
1424
+ name: "get_task",
1425
+ description: "Get details and full content of a specific task by ID",
1426
+ inputSchema: {
1427
+ type: "object",
1428
+ properties: {
1429
+ id: { type: "string", description: "Task ID (e.g. t1, t42)" }
1430
+ },
1431
+ required: ["id"]
1432
+ }
1433
+ },
1434
+ {
1435
+ name: "create_task",
1436
+ description: "Create a new task on the Kandown board",
1437
+ inputSchema: {
1438
+ type: "object",
1439
+ properties: {
1440
+ title: { type: "string", description: "Task title (supports inline syntax #tag @user p1 due:date)" },
1441
+ status: { type: "string", description: "Target column name (default: Backlog)" },
1442
+ priority: { type: "string", description: "Priority level (P1, P2, P3, P4)" },
1443
+ assignee: { type: "string", description: "Assignee username" },
1444
+ tags: { type: "array", items: { type: "string" }, description: "Tags" },
1445
+ body: { type: "string", description: "Markdown body content" }
1446
+ },
1447
+ required: ["title"]
1448
+ }
1449
+ },
1450
+ {
1451
+ name: "move_task",
1452
+ description: "Move a task to a different column or to archived",
1453
+ inputSchema: {
1454
+ type: "object",
1455
+ properties: {
1456
+ id: { type: "string", description: "Task ID" },
1457
+ status: { type: "string", description: 'Target column name or "archived"' }
1458
+ },
1459
+ required: ["id", "status"]
1460
+ }
1461
+ },
1462
+ {
1463
+ name: "add_report",
1464
+ description: "Append an agent execution report to a task body",
1465
+ inputSchema: {
1466
+ type: "object",
1467
+ properties: {
1468
+ id: { type: "string", description: "Task ID" },
1469
+ report: { type: "string", description: "Markdown report content to append under ## Report" }
1470
+ },
1471
+ required: ["id", "report"]
1472
+ }
1473
+ },
1474
+ {
1475
+ name: "list_columns",
1476
+ description: "List configured board columns and task counts",
1477
+ inputSchema: { type: "object", properties: {} }
1478
+ }
1479
+ ]
1480
+ }
1481
+ });
1482
+ return;
1483
+ }
1484
+ if (method === "tools/call") {
1485
+ const { name, arguments: args = {} } = params || {};
1486
+ if (name === "list_tasks") {
1487
+ const board = readBoard(kandownDir);
1488
+ let tasks = [];
1489
+ for (const col of board.columns) {
1490
+ if (args.status && col.name.toLowerCase() !== String(args.status).toLowerCase()) continue;
1491
+ for (const t of col.tasks) {
1492
+ if (args.assignee && t.assignee !== args.assignee) continue;
1493
+ if (args.priority && t.priority !== args.priority) continue;
1494
+ if (args.tag && !t.tags.includes(args.tag)) continue;
1495
+ tasks.push({ ...t, status: col.name });
1496
+ }
1497
+ }
1498
+ sendResponse(id, { result: { content: [{ type: "text", text: JSON.stringify(tasks, null, 2) }] } });
1499
+ return;
1500
+ }
1501
+ if (name === "get_task") {
1502
+ const task = readTask(kandownDir, args.id);
1503
+ sendResponse(id, { result: { content: [{ type: "text", text: JSON.stringify(task, null, 2) }] } });
1504
+ return;
1505
+ }
1506
+ if (name === "create_task") {
1507
+ const newId = createTaskInBoard(kandownDir, args.title, args.status);
1508
+ if (args.body || args.priority || args.assignee || args.tags) {
1509
+ const task = readTask(kandownDir, newId);
1510
+ const fm = {
1511
+ ...task.frontmatter,
1512
+ ...args.priority ? { priority: args.priority } : {},
1513
+ ...args.assignee ? { assignee: args.assignee } : {},
1514
+ ...args.tags ? { tags: args.tags } : {}
1515
+ };
1516
+ const body = args.body ? (task.body + "\n\n" + args.body).trim() : task.body;
1517
+ const taskPath2 = join7(getTasksDir(kandownDir), `${newId}.md`);
1518
+ atomicWriteFileSync(taskPath2, serializeTaskFile(fm, body));
1519
+ }
1520
+ sendResponse(id, { result: { content: [{ type: "text", text: `Created task ${newId}` }] } });
1521
+ return;
1522
+ }
1523
+ if (name === "move_task") {
1524
+ const ok = moveTaskToColumn(kandownDir, args.id, args.status);
1525
+ sendResponse(id, { result: { content: [{ type: "text", text: ok ? `Moved ${args.id} to ${args.status}` : `Failed to move ${args.id}` }] } });
1526
+ return;
1527
+ }
1528
+ if (name === "add_report") {
1529
+ const taskPath2 = join7(getTasksDir(kandownDir), `${args.id}.md`);
1530
+ if (!existsSync7(taskPath2)) {
1531
+ sendResponse(id, { error: { code: -32602, message: `Task ${args.id} not found` } });
1532
+ return;
1533
+ }
1534
+ const task = readTask(kandownDir, args.id);
1535
+ const reportSection = `
1536
+
1537
+ ## Report
1538
+
1539
+ ${args.report.trim()}`;
1540
+ const newBody = task.body.includes("## Report") ? task.body.replace(/## Report[\s\S]*/, `## Report
1541
+
1542
+ ${args.report.trim()}`) : task.body.trim() + reportSection;
1543
+ atomicWriteFileSync(taskPath2, serializeTaskFile(task.frontmatter, newBody));
1544
+ sendResponse(id, { result: { content: [{ type: "text", text: `Appended report to ${args.id}` }] } });
1545
+ return;
1546
+ }
1547
+ if (name === "list_columns") {
1548
+ const config = loadConfig(kandownDir);
1549
+ const board = readBoard(kandownDir);
1550
+ const cols = board.columns.map((c2) => ({ name: c2.name, count: c2.tasks.length }));
1551
+ sendResponse(id, { result: { content: [{ type: "text", text: JSON.stringify(cols, null, 2) }] } });
1552
+ return;
1553
+ }
1554
+ sendResponse(id, { error: { code: -32601, message: `Unknown tool: ${name}` } });
1555
+ return;
1556
+ }
1557
+ sendResponse(id, { error: { code: -32601, message: `Method not found: ${method}` } });
1558
+ }
1559
+
1087
1560
  // src/cli/cli.ts
1088
1561
  var c = {
1089
1562
  reset: "\x1B[0m",
@@ -1099,10 +1572,10 @@ function log(msg) {
1099
1572
  console.log(msg);
1100
1573
  }
1101
1574
  function info(msg) {
1102
- console.log(`${c.blue}\u2139${c.reset} ${msg}`);
1575
+ console.error(`${c.blue}\u2139${c.reset} ${msg}`);
1103
1576
  }
1104
1577
  function success(msg) {
1105
- console.log(`${c.green}\u2713${c.reset} ${msg}`);
1578
+ console.error(`${c.green}\u2713${c.reset} ${msg}`);
1106
1579
  }
1107
1580
  function err(msg) {
1108
1581
  console.error(`${c.red}\u2717${c.reset} ${msg}`);
@@ -1136,20 +1609,74 @@ function parseArgs(args) {
1136
1609
  }
1137
1610
  return { flags, positional, path: typeof flags.path === "string" ? flags.path : ".kandown" };
1138
1611
  }
1612
+ var COMMANDS = /* @__PURE__ */ new Set([
1613
+ "init",
1614
+ "update",
1615
+ "upgrade",
1616
+ "doctor",
1617
+ "work",
1618
+ "list",
1619
+ "ls",
1620
+ "show",
1621
+ "move",
1622
+ "help",
1623
+ "daemon",
1624
+ "board",
1625
+ "settings",
1626
+ "tasks",
1627
+ "create",
1628
+ "new",
1629
+ "assign",
1630
+ "commit",
1631
+ "projects",
1632
+ "export",
1633
+ "import",
1634
+ "mcp",
1635
+ "version"
1636
+ ]);
1637
+ function splitCommand(args) {
1638
+ const withoutGlobalFlags = args.filter((arg) => arg !== "--no-update-check");
1639
+ const commandIndex = withoutGlobalFlags.findIndex((arg, index) => {
1640
+ if (arg.startsWith("-")) return false;
1641
+ if (COMMANDS.has(arg)) return true;
1642
+ return index === 0;
1643
+ });
1644
+ if (commandIndex === -1) {
1645
+ return { cmd: void 0, rest: withoutGlobalFlags };
1646
+ }
1647
+ return {
1648
+ cmd: withoutGlobalFlags[commandIndex],
1649
+ rest: [...withoutGlobalFlags.slice(0, commandIndex), ...withoutGlobalFlags.slice(commandIndex + 1)]
1650
+ };
1651
+ }
1652
+ function stripFirstPositional(args, value) {
1653
+ const result = [];
1654
+ let stripped = false;
1655
+ for (const arg of args) {
1656
+ if (!stripped && arg === value) {
1657
+ stripped = true;
1658
+ continue;
1659
+ }
1660
+ result.push(arg);
1661
+ }
1662
+ return result;
1663
+ }
1139
1664
  function resolveKandownDir(pathArg = ".kandown", cwd = process.cwd()) {
1140
- if (basename(cwd) === ".kandown" || existsSync6(join6(cwd, "kandown.json"))) {
1665
+ if (basename(cwd) === ".kandown" || existsSync8(join8(cwd, "kandown.json"))) {
1141
1666
  return cwd;
1142
1667
  }
1143
- return resolve3(cwd, pathArg);
1668
+ return resolve2(cwd, pathArg);
1144
1669
  }
1145
1670
  function ensureKandownDir(rawArgs) {
1146
1671
  const args = parseArgs(rawArgs);
1147
1672
  const cwd = process.cwd();
1148
1673
  const kandownDir = resolveKandownDir(args.path, cwd);
1149
- if (!existsSync6(kandownDir)) {
1150
- err(`No Kandown installation found at ${c.bold}${kandownDir}${c.reset}`);
1151
- log(` Run ${c.cyan}npx kandown init${c.reset} to create one.`);
1152
- process.exit(1);
1674
+ if (!existsSync8(kandownDir)) {
1675
+ info(`No .kandown/ found \u2014 auto-initializing ${c.bold}${kandownDir}${c.reset}`);
1676
+ if (!doInit(kandownDir)) {
1677
+ err(`Could not auto-initialize Kandown at ${c.bold}${kandownDir}${c.reset}`);
1678
+ process.exit(1);
1679
+ }
1153
1680
  }
1154
1681
  return { kandownDir, cwd };
1155
1682
  }
@@ -1163,6 +1690,7 @@ ${c.bold}USAGE:${c.reset}
1163
1690
 
1164
1691
  ${c.bold}COMMANDS:${c.reset}
1165
1692
  (none) Start web server, open browser & launch TUI
1693
+ init Initialize Kandown in this project
1166
1694
  work Output agent rules + live board digest
1167
1695
  list List tasks (alias: ls)
1168
1696
  show <id> Display task details
@@ -1183,13 +1711,25 @@ ${c.bold}OPTIONS:${c.reset}
1183
1711
  --path <dir> Path to .kandown folder (default: .kandown)
1184
1712
  --port <number> Server port (default: 2050)
1185
1713
  --no-open Don't open browser automatically
1714
+ --no-update-check Skip the registry update check for this run
1715
+ --version Print CLI version
1186
1716
  --help, -h Show help screen
1187
1717
  `);
1188
1718
  }
1719
+ function cmdInit(rawArgs) {
1720
+ const args = parseArgs(rawArgs);
1721
+ const kandownDir = resolve2(process.cwd(), args.path);
1722
+ const created = doInit(kandownDir);
1723
+ if (!created) {
1724
+ err("Failed to initialize Kandown.");
1725
+ process.exit(1);
1726
+ }
1727
+ success(`Kandown initialized at ${kandownDir}`);
1728
+ }
1189
1729
  async function cmdUpdate(rawArgs) {
1190
1730
  const current = getCurrentVersion();
1191
1731
  log(`${c.bold}kandown update${c.reset} ${c.dim}\u2014 v${current}${c.reset}`);
1192
- const latest = await new Promise((resolve4) => {
1732
+ const latest = await new Promise((resolve3) => {
1193
1733
  const child = spawn4("npm", ["view", "kandown", "version"], {
1194
1734
  timeout: 6e3,
1195
1735
  stdio: ["pipe", "pipe", "pipe"],
@@ -1202,11 +1742,11 @@ async function cmdUpdate(rawArgs) {
1202
1742
  });
1203
1743
  child.stderr.on("data", () => {
1204
1744
  });
1205
- child.on("error", () => resolve4(null));
1745
+ child.on("error", () => resolve3(null));
1206
1746
  child.on("close", (code) => {
1207
- if (code !== 0) return resolve4(null);
1747
+ if (code !== 0) return resolve3(null);
1208
1748
  const v = stdout.trim().replace(/^"|"$/g, "");
1209
- resolve4(v || null);
1749
+ resolve3(v || null);
1210
1750
  });
1211
1751
  });
1212
1752
  if (latest && semverGt(latest, current) > 0) {
@@ -1222,12 +1762,12 @@ async function cmdUpdate(rawArgs) {
1222
1762
  }
1223
1763
  const args = parseArgs(rawArgs);
1224
1764
  const cwd = process.cwd();
1225
- const kandownDir = resolve3(cwd, args.path);
1226
- const htmlDest = join6(kandownDir, "kandown.html");
1227
- if (existsSync6(htmlDest)) {
1228
- const htmlSrc = resolve3(PKG_ROOT, "dist", "index.html");
1229
- if (existsSync6(htmlSrc)) {
1230
- copyFileSync2(htmlSrc, htmlDest);
1765
+ const kandownDir = resolve2(cwd, args.path);
1766
+ const htmlDest = join8(kandownDir, "kandown.html");
1767
+ if (existsSync8(htmlDest)) {
1768
+ const htmlSrc = resolve2(PKG_ROOT, "dist", "index.html");
1769
+ if (existsSync8(htmlSrc)) {
1770
+ copyFileSync3(htmlSrc, htmlDest);
1231
1771
  success(`Refreshed ${args.path}/kandown.html`);
1232
1772
  }
1233
1773
  }
@@ -1238,10 +1778,10 @@ async function cmdDoctor(rawArgs) {
1238
1778
  log(`${c.bold}kandown doctor${c.reset} ${c.dim}\u2014 environment & board diagnostic${c.reset}
1239
1779
  `);
1240
1780
  log(` CLI Version: ${currentVersion}`);
1241
- const configPath = join6(kandownDir, "kandown.json");
1242
- if (existsSync6(configPath)) {
1781
+ const configPath = join8(kandownDir, "kandown.json");
1782
+ if (existsSync8(configPath)) {
1243
1783
  try {
1244
- JSON.parse(readFileSync6(configPath, "utf8"));
1784
+ JSON.parse(readFileSync8(configPath, "utf8"));
1245
1785
  success("kandown.json valid");
1246
1786
  } catch (e) {
1247
1787
  err(`kandown.json invalid: ${e.message}`);
@@ -1275,63 +1815,400 @@ async function cmdWork(rawArgs) {
1275
1815
  log(`- **${col.name}** (${col.tasks.length}): ${col.tasks.map((t) => `${t.id} ${t.title}`).join(", ") || "empty"}`);
1276
1816
  }
1277
1817
  }
1818
+ function addMultiFlag(flags, key, value) {
1819
+ const current = flags[key];
1820
+ if (Array.isArray(current)) current.push(value);
1821
+ else if (typeof current === "string") flags[key] = [current, value];
1822
+ else flags[key] = value;
1823
+ }
1824
+ function taskParseArgs(argv) {
1825
+ const flags = {};
1826
+ const positional = [];
1827
+ const aliases = { s: "status", p: "priority", a: "assignee", t: "tag", m: "message" };
1828
+ for (let i = 0; i < argv.length; i++) {
1829
+ const arg = argv[i];
1830
+ if (arg.startsWith("--")) {
1831
+ const [rawKey, inlineValue] = arg.slice(2).split("=", 2);
1832
+ const key = rawKey === "to" ? "to" : rawKey;
1833
+ const next = inlineValue ?? argv[i + 1];
1834
+ if (inlineValue === void 0 && next && !next.startsWith("-")) i++;
1835
+ const value = inlineValue ?? (next && !next.startsWith("-") ? next : true);
1836
+ if (key === "tag") addMultiFlag(flags, key, String(value));
1837
+ else flags[key] = value;
1838
+ continue;
1839
+ }
1840
+ if (/^-[spatm]$/.test(arg)) {
1841
+ const key = aliases[arg.slice(1)];
1842
+ const value = argv[i + 1];
1843
+ if (!value || value.startsWith("-")) {
1844
+ flags[key] = true;
1845
+ } else {
1846
+ i++;
1847
+ if (key === "tag") addMultiFlag(flags, key, value);
1848
+ else flags[key] = value;
1849
+ }
1850
+ continue;
1851
+ }
1852
+ positional.push(arg);
1853
+ }
1854
+ return { flags, positional };
1855
+ }
1856
+ function stringFlag(flags, ...keys) {
1857
+ for (const key of keys) {
1858
+ const value = flags[key];
1859
+ if (typeof value === "string" && value.trim()) return value.trim();
1860
+ }
1861
+ return null;
1862
+ }
1863
+ function listFlag(flags, key) {
1864
+ const value = flags[key];
1865
+ if (Array.isArray(value)) return value.map(String).filter(Boolean);
1866
+ if (typeof value === "string" && value) return [value];
1867
+ return [];
1868
+ }
1869
+ function resolveStatusArg(kandownDir, status) {
1870
+ const config = loadConfig(kandownDir);
1871
+ return config.board.columns.find((col) => col.toLowerCase() === status.toLowerCase()) ?? null;
1872
+ }
1873
+ function taskPath(kandownDir, id, archived = false) {
1874
+ return archived ? join8(getTasksDir(kandownDir), "archive", `${id}.md`) : join8(getTasksDir(kandownDir), `${id}.md`);
1875
+ }
1876
+ function findTaskPath(kandownDir, id) {
1877
+ const active = taskPath(kandownDir, id);
1878
+ if (existsSync8(active)) return active;
1879
+ const archived = taskPath(kandownDir, id, true);
1880
+ if (existsSync8(archived)) return archived;
1881
+ return null;
1882
+ }
1883
+ function nextTaskId(kandownDir) {
1884
+ const ids = new Set(listTaskIds(kandownDir));
1885
+ const archiveDir = join8(getTasksDir(kandownDir), "archive");
1886
+ if (existsSync8(archiveDir)) {
1887
+ for (const file of readdirSync3(archiveDir)) {
1888
+ if (file.endsWith(".md")) ids.add(file.slice(0, -3));
1889
+ }
1890
+ }
1891
+ let max = 0;
1892
+ for (const id of ids) {
1893
+ const match = id.match(/^t(\d+)$/i);
1894
+ if (match) max = Math.max(max, Number(match[1]));
1895
+ }
1896
+ return `t${max + 1}`;
1897
+ }
1898
+ function readTaskFile(kandownDir, id) {
1899
+ const path = findTaskPath(kandownDir, id);
1900
+ if (!path) return null;
1901
+ const parsed = parseTaskFile(readFileSync8(path, "utf8"));
1902
+ return {
1903
+ path,
1904
+ frontmatter: { ...parsed.frontmatter, id: parsed.frontmatter.id || id },
1905
+ body: parsed.body,
1906
+ archived: path.includes("/archive/")
1907
+ };
1908
+ }
1278
1909
  function cmdList(rawArgs) {
1279
1910
  const { kandownDir } = ensureKandownDir(rawArgs);
1280
- const board = readBoard(kandownDir);
1281
- const { flags } = parseArgs(rawArgs);
1282
- const statusFilter = typeof flags.status === "string" ? flags.status.toLowerCase() : null;
1283
- const priorityFilter = typeof flags.priority === "string" ? flags.priority.toUpperCase() : null;
1284
- for (const col of board.columns) {
1285
- if (statusFilter && col.name.toLowerCase() !== statusFilter) continue;
1911
+ const args = taskParseArgs(rawArgs);
1912
+ const includeArchived = args.flags.archived === true;
1913
+ const statusFilter = stringFlag(args.flags, "status")?.toLowerCase() ?? null;
1914
+ const priorityFilter = stringFlag(args.flags, "priority")?.toUpperCase() ?? null;
1915
+ const assigneeFilter = stringFlag(args.flags, "assignee");
1916
+ const tagFilters = listFlag(args.flags, "tag").map((tag) => tag.toLowerCase());
1917
+ const rows = [];
1918
+ for (const id of listTaskIds(kandownDir)) {
1919
+ const task = readTask(kandownDir, id);
1920
+ rows.push({
1921
+ id,
1922
+ title: task.frontmatter.title || id,
1923
+ status: task.frontmatter.status || "Backlog",
1924
+ priority: task.frontmatter.priority || "",
1925
+ assignee: task.frontmatter.assignee || "",
1926
+ tags: Array.isArray(task.frontmatter.tags) ? task.frontmatter.tags : [],
1927
+ archived: false
1928
+ });
1929
+ }
1930
+ if (includeArchived) {
1931
+ const archiveDir = join8(getTasksDir(kandownDir), "archive");
1932
+ if (existsSync8(archiveDir)) {
1933
+ for (const file of readdirSync3(archiveDir).filter((name) => name.endsWith(".md"))) {
1934
+ const id = file.slice(0, -3);
1935
+ const parsed = parseTaskFile(readFileSync8(join8(archiveDir, file), "utf8"));
1936
+ rows.push({
1937
+ id,
1938
+ title: parsed.frontmatter.title || id,
1939
+ status: `${parsed.frontmatter.status || "Backlog"} (archived)`,
1940
+ priority: parsed.frontmatter.priority || "",
1941
+ assignee: parsed.frontmatter.assignee || "",
1942
+ tags: Array.isArray(parsed.frontmatter.tags) ? parsed.frontmatter.tags : [],
1943
+ archived: true
1944
+ });
1945
+ }
1946
+ }
1947
+ }
1948
+ const filtered = rows.filter((row) => {
1949
+ if (statusFilter && row.status.toLowerCase() !== statusFilter) return false;
1950
+ if (priorityFilter && row.priority.toUpperCase() !== priorityFilter) return false;
1951
+ if (assigneeFilter && row.assignee !== assigneeFilter) return false;
1952
+ if (tagFilters.length > 0 && !tagFilters.every((tag) => row.tags.map((t) => t.toLowerCase()).includes(tag))) return false;
1953
+ return true;
1954
+ });
1955
+ if (args.flags.json === true) {
1956
+ process.stdout.write(JSON.stringify(filtered, null, 2) + "\n");
1957
+ return;
1958
+ }
1959
+ const byStatus = /* @__PURE__ */ new Map();
1960
+ for (const row of filtered) {
1961
+ const list = byStatus.get(row.status) ?? [];
1962
+ list.push(row);
1963
+ byStatus.set(row.status, list);
1964
+ }
1965
+ for (const [status, tasks] of byStatus) {
1286
1966
  log(`
1287
- ${c.bold}${col.name}${c.reset} (${col.tasks.length})`);
1288
- for (const t of col.tasks) {
1289
- if (priorityFilter && t.priority !== priorityFilter) continue;
1290
- log(` ${c.cyan}${t.id}${c.reset} [${t.priority || "P2"}] ${t.title}`);
1967
+ ${c.bold}${status}${c.reset} (${tasks.length})`);
1968
+ for (const task of tasks) {
1969
+ const pri = task.priority || "P2";
1970
+ const assignee = task.assignee ? ` @${task.assignee}` : "";
1971
+ log(` ${c.cyan}${task.id}${c.reset} [${pri}] ${task.title}${assignee}`);
1291
1972
  }
1292
1973
  }
1293
1974
  log("");
1294
1975
  }
1295
1976
  function cmdShow(rawArgs) {
1296
1977
  const { kandownDir } = ensureKandownDir(rawArgs);
1297
- const id = rawArgs.find((a) => !a.startsWith("-"));
1978
+ const args = taskParseArgs(rawArgs);
1979
+ const id = args.positional[0];
1298
1980
  if (!id) {
1299
1981
  err("Usage: kandown show <task-id>");
1300
1982
  process.exit(1);
1301
1983
  }
1302
- try {
1303
- const task = readTask(kandownDir, id);
1304
- log(`${c.bold}${task.frontmatter.id}: ${task.frontmatter.title}${c.reset}`);
1305
- log(`Status: ${task.frontmatter.status} | Priority: ${task.frontmatter.priority || "P2"} | Assignee: ${task.frontmatter.assignee || "none"}
1306
- `);
1307
- log(task.body);
1308
- } catch (e) {
1309
- err(`Could not read task ${id}: ${e.message}`);
1984
+ const path = findTaskPath(kandownDir, id);
1985
+ if (!path) {
1986
+ err(`Task not found: ${id}`);
1987
+ process.exit(1);
1988
+ }
1989
+ process.stdout.write(readFileSync8(path, "utf8"));
1990
+ }
1991
+ function cmdCreate(rawArgs) {
1992
+ const { kandownDir } = ensureKandownDir(rawArgs);
1993
+ const args = taskParseArgs(rawArgs);
1994
+ const title = args.positional.join(" ").trim();
1995
+ if (!title) {
1996
+ err('Usage: kandown create "title" [-p P1] [-a user] [-t tag] [--to status] [--id custom-id] [--json]');
1997
+ process.exit(1);
1998
+ }
1999
+ const id = stringFlag(args.flags, "id") ?? nextTaskId(kandownDir);
2000
+ if (!/^[a-zA-Z0-9_-]+$/.test(id)) {
2001
+ err(`Invalid task id: ${id}`);
2002
+ process.exit(1);
2003
+ }
2004
+ if (findTaskPath(kandownDir, id)) {
2005
+ err(`Task already exists: ${id}`);
2006
+ process.exit(1);
1310
2007
  }
2008
+ const config = loadConfig(kandownDir);
2009
+ const rawStatus = stringFlag(args.flags, "to", "status");
2010
+ const status = rawStatus ? resolveStatusArg(kandownDir, rawStatus) : config.board.columns[0] || "Backlog";
2011
+ if (!status) {
2012
+ err(`Unknown status: ${rawStatus}`);
2013
+ process.exit(1);
2014
+ }
2015
+ const fm = {
2016
+ id,
2017
+ title,
2018
+ status,
2019
+ created: (/* @__PURE__ */ new Date()).toISOString().slice(0, 10)
2020
+ };
2021
+ const priority = stringFlag(args.flags, "priority")?.toUpperCase();
2022
+ const assignee = stringFlag(args.flags, "assignee");
2023
+ const tags = listFlag(args.flags, "tag");
2024
+ if (priority) fm.priority = priority;
2025
+ if (assignee) fm.assignee = assignee;
2026
+ if (tags.length > 0) fm.tags = tags;
2027
+ const tasksDir = getTasksDir(kandownDir);
2028
+ if (!existsSync8(tasksDir)) mkdirSync5(tasksDir, { recursive: true });
2029
+ const path = taskPath(kandownDir, id);
2030
+ atomicWriteFileSync(path, serializeTaskFile(fm, ""));
2031
+ process.stderr.write(`${c.green}\u2713${c.reset} Created ${c.bold}${id}${c.reset} \u2192 ${status}
2032
+ `);
2033
+ process.stdout.write(args.flags.json === true ? JSON.stringify(fm, null, 2) + "\n" : `${id}
2034
+ `);
1311
2035
  }
1312
2036
  function cmdMove(rawArgs) {
1313
2037
  const { kandownDir } = ensureKandownDir(rawArgs);
1314
- const [id, newStatus] = rawArgs.filter((a) => !a.startsWith("-"));
1315
- if (!id || !newStatus) {
2038
+ const args = taskParseArgs(rawArgs);
2039
+ const id = args.positional[0];
2040
+ const rawStatus = args.positional.slice(1).join(" ") || stringFlag(args.flags, "to", "status");
2041
+ if (!id || !rawStatus) {
1316
2042
  err("Usage: kandown move <task-id> <status>");
1317
2043
  process.exit(1);
1318
2044
  }
2045
+ if (rawStatus.toLowerCase() === "archived") {
2046
+ if (!archiveTaskInBoard(kandownDir, id)) {
2047
+ err(`Archive failed: ${id}`);
2048
+ process.exit(1);
2049
+ }
2050
+ success(`Archived ${id}`);
2051
+ return;
2052
+ }
2053
+ const status = resolveStatusArg(kandownDir, rawStatus);
2054
+ if (!status) {
2055
+ err(`Unknown status: ${rawStatus}`);
2056
+ process.exit(1);
2057
+ }
2058
+ if (!moveTaskToColumn(kandownDir, id, status)) {
2059
+ err(`Move failed: ${id}`);
2060
+ process.exit(1);
2061
+ }
2062
+ success(`Moved ${id} \u2192 "${status}"`);
2063
+ }
2064
+ function cmdAssign(rawArgs) {
2065
+ const { kandownDir } = ensureKandownDir(rawArgs);
2066
+ const args = taskParseArgs(rawArgs);
2067
+ const [id, assignee] = args.positional;
2068
+ if (!id) {
2069
+ err("Usage: kandown assign <task-id> [assignee]");
2070
+ process.exit(1);
2071
+ }
2072
+ const task = readTaskFile(kandownDir, id);
2073
+ if (!task) {
2074
+ err(`Task not found: ${id}`);
2075
+ process.exit(1);
2076
+ }
2077
+ const frontmatter = { ...task.frontmatter, id };
2078
+ if (assignee) frontmatter.assignee = assignee;
2079
+ else delete frontmatter.assignee;
2080
+ atomicWriteFileSync(task.path, serializeTaskFile(frontmatter, task.body));
2081
+ success(assignee ? `Assigned ${id} \u2192 ${assignee}` : `Unassigned ${id}`);
2082
+ }
2083
+ function cmdCommit(rawArgs) {
2084
+ ensureKandownDir(rawArgs);
2085
+ const args = taskParseArgs(rawArgs);
2086
+ const message = stringFlag(args.flags, "message") || "tasks: update kandown board";
2087
+ const add = spawnSync("git", ["add", "tasks", ".kandown/kandown.json"], { stdio: "inherit" });
2088
+ if (add.status !== 0) process.exit(add.status ?? 1);
2089
+ const commit = spawnSync("git", ["commit", "-m", message], { stdio: "inherit" });
2090
+ process.exit(commit.status ?? 1);
2091
+ }
2092
+ function printTaskCommandsHelp() {
2093
+ log(`
2094
+ ${c.bold}Kandown task commands${c.reset}
2095
+
2096
+ kandown list [-s status] [-p P1] [-a user] [-t tag] [--archived] [--json]
2097
+ kandown show <id>
2098
+ kandown create "title" [-p P1] [-a user] [-t tag] [--to status] [--id id] [--json]
2099
+ kandown move <id> <status|archived>
2100
+ kandown assign <id> [user]
2101
+ kandown commit [-m "message"]
2102
+ `);
2103
+ }
2104
+ async function launchTui(screen, kandownDir) {
2105
+ if (!process.stdin.isTTY) {
2106
+ info(`TUI skipped because stdin is not interactive. Use ${c.cyan}kandown daemon status${c.reset} to inspect the web daemon.`);
2107
+ return;
2108
+ }
2109
+ const tuiPath = join8(PKG_ROOT, "bin", "tui.js");
2110
+ if (!existsSync8(tuiPath)) {
2111
+ err(`TUI binary not found at ${tuiPath}`);
2112
+ process.exit(1);
2113
+ }
2114
+ await new Promise((resolveTui) => {
2115
+ const child = spawn4(process.execPath, [tuiPath, screen, kandownDir, getCurrentVersion()], { stdio: "inherit" });
2116
+ child.on("close", (code) => {
2117
+ if (typeof code === "number" && code !== 0) process.exit(code);
2118
+ resolveTui();
2119
+ });
2120
+ });
2121
+ }
2122
+ async function cmdTui(screen, rawArgs) {
2123
+ const { kandownDir } = ensureKandownDir(rawArgs);
2124
+ await launchTui(screen, kandownDir);
2125
+ }
2126
+ function cmdExport(rawArgs) {
2127
+ const { kandownDir } = ensureKandownDir(rawArgs);
2128
+ const board = readBoard(kandownDir);
2129
+ process.stdout.write(JSON.stringify(board, null, 2) + "\n");
2130
+ }
2131
+ function cmdProjects(rawArgs) {
2132
+ const { kandownDir } = ensureKandownDir(rawArgs);
2133
+ const metadataPath2 = join8(kandownDir, "daemon.json");
2134
+ if (!existsSync8(metadataPath2)) {
2135
+ info("No daemon metadata for this project.");
2136
+ return;
2137
+ }
2138
+ process.stdout.write(readFileSync8(metadataPath2, "utf8").trim() + "\n");
2139
+ }
2140
+ function cmdImport(rawArgs) {
2141
+ const { kandownDir } = ensureKandownDir(rawArgs);
2142
+ const args = taskParseArgs(rawArgs);
2143
+ const file = args.positional[0];
2144
+ if (!file) {
2145
+ err("Usage: kandown import <file.json> [--overwrite]");
2146
+ process.exit(1);
2147
+ }
2148
+ const importPath = resolve2(process.cwd(), file);
2149
+ if (!existsSync8(importPath)) {
2150
+ err(`Import file not found: ${file}`);
2151
+ process.exit(1);
2152
+ }
2153
+ let raw;
1319
2154
  try {
1320
- moveTaskToColumn(kandownDir, id, newStatus);
1321
- success(`Moved ${id} \u2192 "${newStatus}"`);
1322
- } catch (e) {
1323
- err(`Move failed: ${e.message}`);
2155
+ raw = JSON.parse(readFileSync8(importPath, "utf8"));
2156
+ } catch (error) {
2157
+ err(`Import file must be JSON: ${error instanceof Error ? error.message : String(error)}`);
2158
+ process.exit(1);
2159
+ }
2160
+ const rows = [];
2161
+ if (Array.isArray(raw)) {
2162
+ rows.push(...raw.filter((value) => typeof value === "object" && value !== null));
2163
+ } else if (typeof raw === "object" && raw !== null && Array.isArray(raw.columns)) {
2164
+ for (const column of raw.columns) {
2165
+ if (typeof column !== "object" || column === null) continue;
2166
+ const col = column;
2167
+ if (!Array.isArray(col.tasks)) continue;
2168
+ for (const task of col.tasks) {
2169
+ if (typeof task === "object" && task !== null) rows.push({ ...task, status: String(col.name || "Backlog") });
2170
+ }
2171
+ }
2172
+ }
2173
+ if (rows.length === 0) {
2174
+ err("No tasks found to import. Expected a list JSON array or kandown export object.");
2175
+ process.exit(1);
2176
+ }
2177
+ const tasksDir = getTasksDir(kandownDir);
2178
+ if (!existsSync8(tasksDir)) mkdirSync5(tasksDir, { recursive: true });
2179
+ let imported = 0;
2180
+ for (const row of rows) {
2181
+ const id = typeof row.id === "string" && /^[a-zA-Z0-9_-]+$/.test(row.id) ? row.id : nextTaskId(kandownDir);
2182
+ const path = taskPath(kandownDir, id);
2183
+ if (existsSync8(path) && args.flags.overwrite !== true) continue;
2184
+ const fm = {
2185
+ id,
2186
+ title: typeof row.title === "string" && row.title ? row.title : id,
2187
+ status: typeof row.status === "string" && row.status ? row.status.replace(/ \(archived\)$/i, "") : "Backlog"
2188
+ };
2189
+ if (typeof row.priority === "string") fm.priority = row.priority;
2190
+ if (typeof row.assignee === "string") fm.assignee = row.assignee;
2191
+ if (Array.isArray(row.tags)) fm.tags = row.tags.map(String);
2192
+ atomicWriteFileSync(path, serializeTaskFile(fm, typeof row.body === "string" ? row.body : ""));
2193
+ imported++;
1324
2194
  }
2195
+ success(`Imported ${imported} task${imported === 1 ? "" : "s"}`);
1325
2196
  }
1326
2197
  async function main() {
1327
- const args = process.argv.slice(2);
1328
- const cmd = args[0];
1329
- const rest = args.slice(1);
1330
- const skipUpdate = args.includes("--no-update-check") || process.env.KANDOWN_NO_UPDATE === "1";
2198
+ const rawArgs = process.argv.slice(2);
2199
+ if (rawArgs.length === 1 && (rawArgs[0] === "--version" || rawArgs[0] === "version")) {
2200
+ log(getCurrentVersion());
2201
+ return;
2202
+ }
2203
+ const { cmd, rest } = splitCommand(rawArgs);
2204
+ const skipUpdate = rawArgs.includes("--no-update-check") || process.env.KANDOWN_NO_UPDATE === "1";
1331
2205
  if (!skipUpdate) {
1332
2206
  await checkForUpdate(process.argv);
1333
2207
  }
1334
2208
  switch (cmd) {
2209
+ case "init":
2210
+ cmdInit(rest);
2211
+ break;
1335
2212
  case "update":
1336
2213
  case "upgrade":
1337
2214
  await cmdUpdate(rest);
@@ -1342,6 +2219,12 @@ async function main() {
1342
2219
  case "work":
1343
2220
  await cmdWork(rest);
1344
2221
  break;
2222
+ case "board":
2223
+ await cmdTui("board", rest);
2224
+ break;
2225
+ case "settings":
2226
+ await cmdTui("settings", rest);
2227
+ break;
1345
2228
  case "list":
1346
2229
  case "ls":
1347
2230
  cmdList(rest);
@@ -1349,21 +2232,52 @@ async function main() {
1349
2232
  case "show":
1350
2233
  cmdShow(rest);
1351
2234
  break;
2235
+ case "create":
2236
+ case "new":
2237
+ cmdCreate(rest);
2238
+ break;
1352
2239
  case "move":
1353
2240
  cmdMove(rest);
1354
2241
  break;
2242
+ case "assign":
2243
+ cmdAssign(rest);
2244
+ break;
2245
+ case "commit":
2246
+ cmdCommit(rest);
2247
+ break;
2248
+ case "tasks":
2249
+ printTaskCommandsHelp();
2250
+ break;
2251
+ case "projects":
2252
+ cmdProjects(rest);
2253
+ break;
2254
+ case "export":
2255
+ cmdExport(rest);
2256
+ break;
2257
+ case "import":
2258
+ cmdImport(rest);
2259
+ break;
2260
+ case "mcp": {
2261
+ const { kandownDir } = ensureKandownDir(rest);
2262
+ startMcpServer(kandownDir);
2263
+ break;
2264
+ }
1355
2265
  case "help":
1356
2266
  case "--help":
1357
2267
  case "-h":
1358
2268
  help();
1359
2269
  break;
1360
2270
  case "daemon": {
1361
- const subcommand = rest[0] || "status";
1362
- const { kandownDir } = ensureKandownDir(rest.slice(1));
2271
+ const parsedDaemonArgs = parseArgs(rest);
2272
+ const subcommand = parsedDaemonArgs.positional[0] || "status";
2273
+ const daemonArgs = subcommand ? stripFirstPositional(rest, subcommand) : rest;
2274
+ const { kandownDir } = ensureKandownDir(daemonArgs);
1363
2275
  if (subcommand === "run") {
1364
- const { port } = await listenOnAvailablePort(kandownDir);
2276
+ const daemonOptions = parseArgs(daemonArgs);
2277
+ const preferredPort = typeof daemonOptions.flags.port === "string" ? Number(daemonOptions.flags.port) : null;
2278
+ const { port } = await listenOnAvailablePort(kandownDir, Number.isInteger(preferredPort) ? preferredPort : null);
1365
2279
  const url = `http://localhost:${port}`;
1366
- const metadataPath2 = join6(kandownDir, "daemon.json");
2280
+ const metadataPath2 = join8(kandownDir, "daemon.json");
1367
2281
  atomicWriteFileSync(metadataPath2, JSON.stringify({
1368
2282
  pid: process.pid,
1369
2283
  port,
@@ -1376,24 +2290,44 @@ async function main() {
1376
2290
  info(`Kandown daemon running on port ${port} (PID ${process.pid})`);
1377
2291
  await new Promise(() => {
1378
2292
  });
1379
- } else if (subcommand === "stop") {
1380
- const status = await getDaemonStatus(kandownDir);
1381
- if (status.running && status.metadata) {
1382
- try {
1383
- process.kill(status.metadata.pid, "SIGTERM");
1384
- } catch {
1385
- }
1386
- success(`Stopped daemon PID ${status.metadata.pid}`);
1387
- } else {
1388
- info("Daemon not running");
2293
+ } else if (subcommand === "start") {
2294
+ const daemonOptions = parseArgs(daemonArgs);
2295
+ const preferredPort = typeof daemonOptions.flags.port === "string" ? Number(daemonOptions.flags.port) : null;
2296
+ const status = await startProjectDaemon(kandownDir, Number.isInteger(preferredPort) ? preferredPort : null);
2297
+ if (status.running && status.metadata) success(`Daemon running on port ${status.metadata.port} (PID ${status.metadata.pid})`);
2298
+ else {
2299
+ err("Daemon failed to start");
2300
+ process.exit(1);
1389
2301
  }
1390
- } else {
2302
+ } else if (subcommand === "restart") {
2303
+ await stopProjectDaemon(kandownDir);
2304
+ const status = await startProjectDaemon(kandownDir);
2305
+ if (status.running && status.metadata) success(`Daemon restarted on port ${status.metadata.port} (PID ${status.metadata.pid})`);
2306
+ else {
2307
+ err("Daemon failed to restart");
2308
+ process.exit(1);
2309
+ }
2310
+ } else if (subcommand === "stop") {
2311
+ const stopped = await stopProjectDaemon(kandownDir);
2312
+ if (stopped) success("Daemon stopped");
2313
+ else info("Daemon not running");
2314
+ } else if (subcommand === "status") {
1391
2315
  const status = await getDaemonStatus(kandownDir);
1392
2316
  if (status.running && status.metadata) {
1393
2317
  success(`Daemon running on port ${status.metadata.port} (PID ${status.metadata.pid})`);
1394
2318
  } else {
1395
2319
  info("Daemon not running");
1396
2320
  }
2321
+ } else if (subcommand === "refresh-all") {
2322
+ const status = await getDaemonStatus(kandownDir);
2323
+ if (status.running) await stopProjectDaemon(kandownDir);
2324
+ const restarted = await startProjectDaemon(kandownDir);
2325
+ if (restarted.running && restarted.metadata) success(`Refreshed current project daemon on port ${restarted.metadata.port}`);
2326
+ else info("No running daemon refreshed");
2327
+ } else {
2328
+ err(`Unknown daemon command: ${subcommand}`);
2329
+ log(` Use ${c.cyan}kandown daemon start|stop|restart|status|refresh-all${c.reset}`);
2330
+ process.exit(1);
1397
2331
  }
1398
2332
  break;
1399
2333
  }
@@ -1405,21 +2339,14 @@ async function main() {
1405
2339
  status = await startProjectDaemon(kandownDir);
1406
2340
  }
1407
2341
  if (!parsed.flags["no-open"]) {
1408
- const urlToOpen = status.metadata?.url || join6(kandownDir, "kandown.html");
2342
+ const urlToOpen = status.metadata?.url || join8(kandownDir, "kandown.html");
1409
2343
  openBrowser(urlToOpen);
1410
2344
  }
1411
- const tuiPath = join6(PKG_ROOT, "bin", "tui.js");
1412
- if (existsSync6(tuiPath)) {
1413
- const child = spawn4(process.execPath, [tuiPath, ...rest], { stdio: "inherit" });
1414
- child.on("close", (code) => process.exit(code || 0));
1415
- } else {
1416
- err(`TUI binary not found at ${tuiPath}`);
1417
- process.exit(1);
1418
- }
2345
+ await launchTui("board", kandownDir);
1419
2346
  break;
1420
2347
  }
1421
2348
  default:
1422
- if (cmd.startsWith("-")) {
2349
+ if (!cmd || cmd.startsWith("-")) {
1423
2350
  help();
1424
2351
  break;
1425
2352
  }