kandown 0.32.2 → 0.33.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/CHANGELOG.md CHANGED
@@ -1,5 +1,13 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.33.0 — 2026-07-21 — "Universal Auto-Updater & Web UI Prompt Engine"
4
+
5
+ - **Added**: **Daemon HTTP Server Module (`src/cli/lib/server.ts`)** — re-introduced and modernized full REST API server & SSE live-reload for daemon mode (`/api/daemon`, `/api/board`, `/api/config`, `/api/tasks`, `/api/update/check`, `/api/update/apply`).
6
+ - **Added**: **Web UI Update Notification Banner (`UpdateNotificationBanner.tsx`)** — non-intrusive floating prompt banner that notifies users when a new Kandown version is published on npm with a 1-click **"Update Now"** action.
7
+ - **Added**: **1-Click Web UI Settings Update Button** — added interactive `Update to vX.Y.Z` button in the Settings page version card, allowing users to upgrade Kandown directly from the web browser.
8
+ - **Added**: **Automated `.kandown/kandown.html` Bundle Auto-Refresh** — project HTML files are automatically synced with the CLI's latest singlefile bundle on daemon boot and whenever an update is applied, preventing project HTML files from being stuck on older releases.
9
+ - **Added**: **Global PATH Symlink Syncing** — automatically synchronizes symlinks at `~/.local/bin/kandown`, `~/Library/pnpm/bin/kandown`, and nvm directories so all PATH executables point to the latest CLI version.
10
+
3
11
  ## 0.32.2 — 2026-07-21 — "Reliable Auto-Updater Engine"
4
12
 
5
13
  - **Fixed**: **Auto-Updater Execution Gate** — removed command filtering logic in CLI main router that previously bypassed update checks for scripted commands like `kandown work`, `kandown list`, and `kandown doctor`.
package/bin/kandown.js CHANGED
@@ -1,9 +1,9 @@
1
1
  #!/usr/bin/env node
2
2
 
3
3
  // src/cli/cli.ts
4
- import { existsSync as existsSync5, readFileSync as readFileSync5, copyFileSync } from "fs";
5
- import { join as join5, resolve as resolve2, basename } from "path";
6
- import { spawn as spawn3 } from "child_process";
4
+ import { existsSync as existsSync6, readFileSync as readFileSync6, copyFileSync as copyFileSync2 } from "fs";
5
+ import { join as join6, resolve as resolve3, basename } from "path";
6
+ import { spawn as spawn4 } from "child_process";
7
7
 
8
8
  // src/cli/lib/updater.ts
9
9
  import { existsSync, readFileSync, writeFileSync, unlinkSync, statSync, mkdirSync } from "fs";
@@ -12,7 +12,7 @@ import { spawn, execSync } from "child_process";
12
12
  import { homedir } from "os";
13
13
 
14
14
  // src/lib/version.ts
15
- var KANDOWN_VERSION = "0.32.2";
15
+ var KANDOWN_VERSION = "0.33.0";
16
16
 
17
17
  // src/cli/lib/updater.ts
18
18
  var CACHE_DIR = join(homedir(), ".kandown");
@@ -147,7 +147,7 @@ async function checkForUpdate(argv = process.argv) {
147
147
  }
148
148
  } catch {
149
149
  }
150
- const latest = await new Promise((resolve3) => {
150
+ const latest = await new Promise((resolve4) => {
151
151
  const child2 = spawn("npm", ["view", "kandown", "version"], {
152
152
  timeout: 6e3,
153
153
  stdio: ["pipe", "pipe", "pipe"],
@@ -160,11 +160,11 @@ async function checkForUpdate(argv = process.argv) {
160
160
  });
161
161
  child2.stderr.on("data", () => {
162
162
  });
163
- child2.on("error", () => resolve3(null));
163
+ child2.on("error", () => resolve4(null));
164
164
  child2.on("close", (code) => {
165
- if (code !== 0) return resolve3(null);
165
+ if (code !== 0) return resolve4(null);
166
166
  const v = stdout.trim().replace(/^"|"$/g, "");
167
- resolve3(v || null);
167
+ resolve4(v || null);
168
168
  });
169
169
  });
170
170
  if (!latest) return;
@@ -486,6 +486,10 @@ function loadConfig(kandownDir) {
486
486
  }
487
487
  return merged;
488
488
  }
489
+ function saveConfig(kandownDir, config) {
490
+ const configPath = join2(kandownDir, "kandown.json");
491
+ atomicWriteFileSync(configPath, JSON.stringify(config, null, 2) + "\n");
492
+ }
489
493
 
490
494
  // src/cli/lib/board-reader.ts
491
495
  function getProjectRoot(kandownDir) {
@@ -695,16 +699,16 @@ async function fetchDaemonInfo(port) {
695
699
  }
696
700
  }
697
701
  function isPortListening(port, timeoutMs = 400) {
698
- return new Promise((resolve3) => {
702
+ return new Promise((resolve4) => {
699
703
  const socket = createConnection({ port, host: "127.0.0.1" }, () => {
700
704
  socket.destroy();
701
- resolve3(true);
705
+ resolve4(true);
702
706
  });
703
- socket.on("error", () => resolve3(false));
707
+ socket.on("error", () => resolve4(false));
704
708
  socket.setTimeout(timeoutMs);
705
709
  socket.on("timeout", () => {
706
710
  socket.destroy();
707
- resolve3(false);
711
+ resolve4(false);
708
712
  });
709
713
  });
710
714
  }
@@ -732,7 +736,7 @@ async function waitForDaemon(kandownDir, timeoutMs = 8e3) {
732
736
  if (metadata && isProcessAlive(metadata.pid) && await isPortListening(metadata.port)) {
733
737
  return { running: true, metadata };
734
738
  }
735
- await new Promise((resolve3) => setTimeout(resolve3, 120));
739
+ await new Promise((resolve4) => setTimeout(resolve4, 120));
736
740
  }
737
741
  return { running: false, metadata: null };
738
742
  }
@@ -762,6 +766,306 @@ async function startProjectDaemon(kandownDir, preferredPort) {
762
766
  return waitForDaemon(kandownDir);
763
767
  }
764
768
 
769
+ // src/cli/lib/server.ts
770
+ import { createServer } from "http";
771
+ import { existsSync as existsSync5, readFileSync as readFileSync5, copyFileSync, unlinkSync as unlinkSync5, mkdirSync as mkdirSync3 } from "fs";
772
+ import { join as join5, resolve as resolve2, dirname as dirname3 } from "path";
773
+ import { execSync as execSync2, spawn as spawn3 } from "child_process";
774
+ import { homedir as homedir3 } from "os";
775
+ var START_PORT_RANGE = 2050;
776
+ var END_PORT_RANGE = 2099;
777
+ var UNSAFE_PORTS = /* @__PURE__ */ new Set([2049, 4045, 6e3, 6665, 6666, 6667, 6668, 6669, 6697]);
778
+ var sseClients = [];
779
+ var nextClientId = 1;
780
+ function broadcastSseEvent(data) {
781
+ const payload = `data: ${JSON.stringify(data)}
782
+
783
+ `;
784
+ sseClients.forEach((c2) => c2.res.write(payload));
785
+ }
786
+ function handleCors(res) {
787
+ res.writeHead(204, {
788
+ "Access-Control-Allow-Origin": "*",
789
+ "Access-Control-Allow-Methods": "GET, POST, PUT, DELETE, OPTIONS",
790
+ "Access-Control-Allow-Headers": "Content-Type, X-Kandown-Token"
791
+ });
792
+ res.end();
793
+ }
794
+ function writeJson(res, status, data) {
795
+ res.writeHead(status, {
796
+ "Content-Type": "application/json",
797
+ "Access-Control-Allow-Origin": "*"
798
+ });
799
+ res.end(JSON.stringify(data));
800
+ }
801
+ function writeText(res, status, text) {
802
+ res.writeHead(status, {
803
+ "Content-Type": "text/plain; charset=utf-8",
804
+ "Access-Control-Allow-Origin": "*"
805
+ });
806
+ res.end(text);
807
+ }
808
+ function syncProjectKandownHtml(kandownDir) {
809
+ try {
810
+ const projectHtml = join5(kandownDir, "kandown.html");
811
+ const cliRoot = resolve2(import.meta.url ? new URL("../..", import.meta.url).pathname : process.cwd());
812
+ const distHtml = join5(cliRoot, "dist", "index.html");
813
+ if (!existsSync5(distHtml)) return false;
814
+ if (!existsSync5(projectHtml)) {
815
+ copyFileSync(distHtml, projectHtml);
816
+ return true;
817
+ }
818
+ const currentContent = readFileSync5(projectHtml, "utf8");
819
+ const newContent = readFileSync5(distHtml, "utf8");
820
+ if (currentContent !== newContent) {
821
+ atomicWriteFileSync(projectHtml, newContent);
822
+ return true;
823
+ }
824
+ } catch {
825
+ }
826
+ return false;
827
+ }
828
+ function syncGlobalSymlinks() {
829
+ try {
830
+ const home = homedir3();
831
+ const candidatePaths = [
832
+ join5(home, ".local", "bin", "kandown"),
833
+ join5(home, "Library", "pnpm", "bin", "kandown"),
834
+ join5(home, ".nvm", "versions", "node", "v25.2.1", "bin", "kandown")
835
+ ];
836
+ const currentBin = resolve2(import.meta.url ? new URL("../../bin/kandown.js", import.meta.url).pathname : process.cwd());
837
+ for (const targetPath of candidatePaths) {
838
+ if (existsSync5(dirname3(targetPath))) {
839
+ try {
840
+ if (existsSync5(targetPath)) unlinkSync5(targetPath);
841
+ execSync2(`ln -sf "${currentBin}" "${targetPath}" 2>/dev/null || true`);
842
+ } catch {
843
+ }
844
+ }
845
+ }
846
+ } catch {
847
+ }
848
+ }
849
+ async function handleApi(req, res, url, kandownDir) {
850
+ const path = url.pathname;
851
+ const method = req.method || "GET";
852
+ if (path === "/api/daemon" && method === "GET") {
853
+ return writeJson(res, 200, {
854
+ ok: true,
855
+ pid: process.pid,
856
+ kandownDir,
857
+ version: getCurrentVersion(),
858
+ startedAt: (/* @__PURE__ */ new Date()).toISOString(),
859
+ agentHook: process.env.KANDOWN_AGENT_HOOK_URL ? { enabled: true, label: process.env.KANDOWN_AGENT_HOOK_LABEL || "Send to Agent" } : null
860
+ });
861
+ }
862
+ if (path === "/api/version" && method === "GET") {
863
+ return writeJson(res, 200, {
864
+ version: getCurrentVersion()
865
+ });
866
+ }
867
+ if (path === "/api/update/check" && method === "GET") {
868
+ const current = getCurrentVersion();
869
+ const latest = await new Promise((resolve4) => {
870
+ const child = spawn3("npm", ["view", "kandown", "version"], {
871
+ timeout: 4e3,
872
+ stdio: ["pipe", "pipe", "pipe"],
873
+ env: { ...process.env },
874
+ detached: false
875
+ });
876
+ let stdout = "";
877
+ child.stdout.on("data", (d) => {
878
+ stdout += d;
879
+ });
880
+ child.stderr.on("data", () => {
881
+ });
882
+ child.on("error", () => resolve4(null));
883
+ child.on("close", (code) => {
884
+ if (code !== 0) return resolve4(null);
885
+ resolve4(stdout.trim().replace(/^"|"$/g, "") || null);
886
+ });
887
+ });
888
+ const updateAvailable = latest ? semverGt(latest, current) > 0 : false;
889
+ return writeJson(res, 200, {
890
+ current,
891
+ latest: latest || current,
892
+ updateAvailable
893
+ });
894
+ }
895
+ if (path === "/api/update/apply" && method === "POST") {
896
+ const current = getCurrentVersion();
897
+ const latest = await new Promise((resolve4) => {
898
+ const child = spawn3("npm", ["view", "kandown", "version"], {
899
+ timeout: 4e3,
900
+ stdio: ["pipe", "pipe", "pipe"],
901
+ env: { ...process.env },
902
+ detached: false
903
+ });
904
+ let stdout = "";
905
+ child.stdout.on("data", (d) => {
906
+ stdout += d;
907
+ });
908
+ child.on("error", () => resolve4(null));
909
+ child.on("close", (code) => resolve4(code === 0 ? stdout.trim() : null));
910
+ });
911
+ const targetVersion = latest || current;
912
+ const ok = await performGlobalPackageUpdate(`kandown@${targetVersion}`);
913
+ syncGlobalSymlinks();
914
+ syncProjectKandownHtml(kandownDir);
915
+ if (ok) {
916
+ writeJson(res, 200, { ok: true, version: targetVersion, message: "Update installed successfully" });
917
+ broadcastSseEvent({ type: "update", version: targetVersion });
918
+ } else {
919
+ writeJson(res, 500, { ok: false, message: "Global package installation failed" });
920
+ }
921
+ return;
922
+ }
923
+ if (path === "/api/events" && method === "GET") {
924
+ res.writeHead(200, {
925
+ "Content-Type": "text/event-stream",
926
+ "Cache-Control": "no-cache",
927
+ "Connection": "keep-alive",
928
+ "Access-Control-Allow-Origin": "*"
929
+ });
930
+ res.write("retry: 2000\n\n");
931
+ const id = nextClientId++;
932
+ sseClients.push({ id, res });
933
+ req.on("close", () => {
934
+ sseClients = sseClients.filter((c2) => c2.id !== id);
935
+ });
936
+ return;
937
+ }
938
+ if (path === "/api/board") {
939
+ if (method === "GET") {
940
+ const tasksDir = getTasksDir(kandownDir);
941
+ const boardPath = join5(tasksDir, "board.md");
942
+ const text = existsSync5(boardPath) ? readFileSync5(boardPath, "utf8") : "";
943
+ return writeText(res, 200, text);
944
+ }
945
+ if (method === "PUT") {
946
+ let body = "";
947
+ req.on("data", (chunk) => {
948
+ body += chunk;
949
+ });
950
+ req.on("end", () => {
951
+ const tasksDir = getTasksDir(kandownDir);
952
+ if (!existsSync5(tasksDir)) mkdirSync3(tasksDir, { recursive: true });
953
+ atomicWriteFileSync(join5(tasksDir, "board.md"), body);
954
+ broadcastSseEvent({ type: "board" });
955
+ writeJson(res, 200, { ok: true });
956
+ });
957
+ return;
958
+ }
959
+ }
960
+ if (path === "/api/config") {
961
+ if (method === "GET") {
962
+ return writeJson(res, 200, loadConfig(kandownDir));
963
+ }
964
+ if (method === "PUT") {
965
+ let body = "";
966
+ req.on("data", (chunk) => {
967
+ body += chunk;
968
+ });
969
+ req.on("end", () => {
970
+ try {
971
+ const parsed = JSON.parse(body);
972
+ saveConfig(kandownDir, parsed);
973
+ broadcastSseEvent({ type: "config" });
974
+ writeJson(res, 200, { ok: true });
975
+ } catch (e) {
976
+ writeJson(res, 400, { error: e.message });
977
+ }
978
+ });
979
+ return;
980
+ }
981
+ }
982
+ if (path === "/api/tasks" && method === "GET") {
983
+ return writeJson(res, 200, listTaskIds(kandownDir));
984
+ }
985
+ if (path.startsWith("/api/tasks/")) {
986
+ const taskId = decodeURIComponent(path.slice("/api/tasks/".length));
987
+ const tasksDir = getTasksDir(kandownDir);
988
+ const taskPath = join5(tasksDir, `${taskId}.md`);
989
+ if (method === "GET") {
990
+ if (!existsSync5(taskPath)) return writeText(res, 404, "Task not found");
991
+ return writeText(res, 200, readFileSync5(taskPath, "utf8"));
992
+ }
993
+ if (method === "PUT") {
994
+ let body = "";
995
+ req.on("data", (chunk) => {
996
+ body += chunk;
997
+ });
998
+ req.on("end", () => {
999
+ if (!existsSync5(tasksDir)) mkdirSync3(tasksDir, { recursive: true });
1000
+ atomicWriteFileSync(taskPath, body);
1001
+ broadcastSseEvent({ type: "task", id: taskId });
1002
+ writeJson(res, 200, { ok: true });
1003
+ });
1004
+ return;
1005
+ }
1006
+ if (method === "DELETE") {
1007
+ if (existsSync5(taskPath)) unlinkSync5(taskPath);
1008
+ broadcastSseEvent({ type: "task_delete", id: taskId });
1009
+ return writeJson(res, 200, { ok: true });
1010
+ }
1011
+ }
1012
+ writeJson(res, 404, { error: "Route not found" });
1013
+ }
1014
+ function serveApp(res, kandownDir) {
1015
+ syncProjectKandownHtml(kandownDir);
1016
+ const htmlPath = join5(kandownDir, "kandown.html");
1017
+ if (existsSync5(htmlPath)) {
1018
+ res.writeHead(200, { "Content-Type": "text/html; charset=utf-8" });
1019
+ res.end(readFileSync5(htmlPath, "utf8"));
1020
+ } else {
1021
+ writeText(res, 404, "kandown.html not found");
1022
+ }
1023
+ }
1024
+ function createServeServer(kandownDir) {
1025
+ syncProjectKandownHtml(kandownDir);
1026
+ syncGlobalSymlinks();
1027
+ return createServer((req, res) => {
1028
+ const url = new URL(req.url || "/", "http://localhost");
1029
+ if (req.method === "OPTIONS") return handleCors(res);
1030
+ if (url.pathname === "/" || url.pathname === "/kandown.html") {
1031
+ return serveApp(res, kandownDir);
1032
+ }
1033
+ if (url.pathname.startsWith("/api/")) {
1034
+ return handleApi(req, res, url, kandownDir);
1035
+ }
1036
+ writeText(res, 404, "Not found");
1037
+ });
1038
+ }
1039
+ function listen(server, port) {
1040
+ return new Promise((resolveListen, rejectListen) => {
1041
+ const onError = (e) => {
1042
+ server.off("listening", onListening);
1043
+ rejectListen(e);
1044
+ };
1045
+ const onListening = () => {
1046
+ server.off("error", onError);
1047
+ resolveListen();
1048
+ };
1049
+ server.once("error", onError);
1050
+ server.once("listening", onListening);
1051
+ server.listen(port, "127.0.0.1");
1052
+ });
1053
+ }
1054
+ async function listenOnAvailablePort(kandownDir, preferredPort) {
1055
+ const startPort = preferredPort ?? START_PORT_RANGE;
1056
+ for (let p = startPort; p <= END_PORT_RANGE; p++) {
1057
+ if (UNSAFE_PORTS.has(p)) continue;
1058
+ const server = createServeServer(kandownDir);
1059
+ try {
1060
+ await listen(server, p);
1061
+ return { server, port: p };
1062
+ } catch (e) {
1063
+ if (e.code !== "EADDRINUSE" && e.code !== "EACCES") throw e;
1064
+ }
1065
+ }
1066
+ throw new Error(`No free port in range ${START_PORT_RANGE}-${END_PORT_RANGE}`);
1067
+ }
1068
+
765
1069
  // src/cli/cli.ts
766
1070
  var c = {
767
1071
  reset: "\x1B[0m",
@@ -808,16 +1112,16 @@ function parseArgs(args) {
808
1112
  return { flags, positional, path: typeof flags.path === "string" ? flags.path : ".kandown" };
809
1113
  }
810
1114
  function resolveKandownDir(pathArg = ".kandown", cwd = process.cwd()) {
811
- if (basename(cwd) === ".kandown" || existsSync5(join5(cwd, "kandown.json"))) {
1115
+ if (basename(cwd) === ".kandown" || existsSync6(join6(cwd, "kandown.json"))) {
812
1116
  return cwd;
813
1117
  }
814
- return resolve2(cwd, pathArg);
1118
+ return resolve3(cwd, pathArg);
815
1119
  }
816
1120
  function ensureKandownDir(rawArgs) {
817
1121
  const args = parseArgs(rawArgs);
818
1122
  const cwd = process.cwd();
819
1123
  const kandownDir = resolveKandownDir(args.path, cwd);
820
- if (!existsSync5(kandownDir)) {
1124
+ if (!existsSync6(kandownDir)) {
821
1125
  err(`No Kandown installation found at ${c.bold}${kandownDir}${c.reset}`);
822
1126
  log(` Run ${c.cyan}npx kandown init${c.reset} to create one.`);
823
1127
  process.exit(1);
@@ -860,8 +1164,8 @@ ${c.bold}OPTIONS:${c.reset}
860
1164
  async function cmdUpdate(rawArgs) {
861
1165
  const current = getCurrentVersion();
862
1166
  log(`${c.bold}kandown update${c.reset} ${c.dim}\u2014 v${current}${c.reset}`);
863
- const latest = await new Promise((resolve3) => {
864
- const child = spawn3("npm", ["view", "kandown", "version"], {
1167
+ const latest = await new Promise((resolve4) => {
1168
+ const child = spawn4("npm", ["view", "kandown", "version"], {
865
1169
  timeout: 6e3,
866
1170
  stdio: ["pipe", "pipe", "pipe"],
867
1171
  env: { ...process.env },
@@ -873,11 +1177,11 @@ async function cmdUpdate(rawArgs) {
873
1177
  });
874
1178
  child.stderr.on("data", () => {
875
1179
  });
876
- child.on("error", () => resolve3(null));
1180
+ child.on("error", () => resolve4(null));
877
1181
  child.on("close", (code) => {
878
- if (code !== 0) return resolve3(null);
1182
+ if (code !== 0) return resolve4(null);
879
1183
  const v = stdout.trim().replace(/^"|"$/g, "");
880
- resolve3(v || null);
1184
+ resolve4(v || null);
881
1185
  });
882
1186
  });
883
1187
  if (latest && semverGt(latest, current) > 0) {
@@ -893,12 +1197,12 @@ async function cmdUpdate(rawArgs) {
893
1197
  }
894
1198
  const args = parseArgs(rawArgs);
895
1199
  const cwd = process.cwd();
896
- const kandownDir = resolve2(cwd, args.path);
897
- const htmlDest = join5(kandownDir, "kandown.html");
898
- if (existsSync5(htmlDest)) {
899
- const htmlSrc = resolve2(import.meta.url ? new URL("../..", import.meta.url).pathname : process.cwd(), "dist", "index.html");
900
- if (existsSync5(htmlSrc)) {
901
- copyFileSync(htmlSrc, htmlDest);
1200
+ const kandownDir = resolve3(cwd, args.path);
1201
+ const htmlDest = join6(kandownDir, "kandown.html");
1202
+ if (existsSync6(htmlDest)) {
1203
+ const htmlSrc = resolve3(import.meta.url ? new URL("../..", import.meta.url).pathname : process.cwd(), "dist", "index.html");
1204
+ if (existsSync6(htmlSrc)) {
1205
+ copyFileSync2(htmlSrc, htmlDest);
902
1206
  success(`Refreshed ${args.path}/kandown.html`);
903
1207
  }
904
1208
  }
@@ -909,10 +1213,10 @@ async function cmdDoctor(rawArgs) {
909
1213
  log(`${c.bold}kandown doctor${c.reset} ${c.dim}\u2014 environment & board diagnostic${c.reset}
910
1214
  `);
911
1215
  log(` CLI Version: ${currentVersion}`);
912
- const configPath = join5(kandownDir, "kandown.json");
913
- if (existsSync5(configPath)) {
1216
+ const configPath = join6(kandownDir, "kandown.json");
1217
+ if (existsSync6(configPath)) {
914
1218
  try {
915
- JSON.parse(readFileSync5(configPath, "utf8"));
1219
+ JSON.parse(readFileSync6(configPath, "utf8"));
916
1220
  success("kandown.json valid");
917
1221
  } catch (e) {
918
1222
  err(`kandown.json invalid: ${e.message}`);
@@ -1028,15 +1332,55 @@ async function main() {
1028
1332
  case "-h":
1029
1333
  help();
1030
1334
  break;
1335
+ case "daemon": {
1336
+ const subcommand = rest[0] || "status";
1337
+ const { kandownDir } = ensureKandownDir(rest.slice(1));
1338
+ if (subcommand === "run") {
1339
+ const { port } = await listenOnAvailablePort(kandownDir);
1340
+ const url = `http://localhost:${port}`;
1341
+ const metadataPath2 = join6(kandownDir, "daemon.json");
1342
+ atomicWriteFileSync(metadataPath2, JSON.stringify({
1343
+ pid: process.pid,
1344
+ port,
1345
+ url,
1346
+ kandownDir,
1347
+ startedAt: (/* @__PURE__ */ new Date()).toISOString(),
1348
+ version: getCurrentVersion(),
1349
+ token: null
1350
+ }, null, 2));
1351
+ info(`Kandown daemon running on port ${port} (PID ${process.pid})`);
1352
+ await new Promise(() => {
1353
+ });
1354
+ } else if (subcommand === "stop") {
1355
+ const status = await getDaemonStatus(kandownDir);
1356
+ if (status.running && status.metadata) {
1357
+ try {
1358
+ process.kill(status.metadata.pid, "SIGTERM");
1359
+ } catch {
1360
+ }
1361
+ success(`Stopped daemon PID ${status.metadata.pid}`);
1362
+ } else {
1363
+ info("Daemon not running");
1364
+ }
1365
+ } else {
1366
+ const status = await getDaemonStatus(kandownDir);
1367
+ if (status.running && status.metadata) {
1368
+ success(`Daemon running on port ${status.metadata.port} (PID ${status.metadata.pid})`);
1369
+ } else {
1370
+ info("Daemon not running");
1371
+ }
1372
+ }
1373
+ break;
1374
+ }
1031
1375
  case void 0: {
1032
1376
  const { kandownDir } = ensureKandownDir(rest);
1033
1377
  const status = await getDaemonStatus(kandownDir);
1034
1378
  if (!status.running) {
1035
1379
  await startProjectDaemon(kandownDir);
1036
1380
  }
1037
- const tuiPath = resolve2(import.meta.url ? new URL("../..", import.meta.url).pathname : process.cwd(), "bin", "tui.js");
1038
- if (existsSync5(tuiPath)) {
1039
- const child = spawn3("node", [tuiPath, ...rest], { stdio: "inherit" });
1381
+ const tuiPath = resolve3(import.meta.url ? new URL("../..", import.meta.url).pathname : process.cwd(), "bin", "tui.js");
1382
+ if (existsSync6(tuiPath)) {
1383
+ const child = spawn4("node", [tuiPath, ...rest], { stdio: "inherit" });
1040
1384
  child.on("close", (code) => process.exit(code || 0));
1041
1385
  } else {
1042
1386
  info("Kandown daemon running. Open kandown.html in browser.");