kandown 0.32.2 → 0.33.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,18 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.33.1 — 2026-07-21 — "Instant TUI & Browser Launcher Fix"
4
+
5
+ - **Fixed**: **CLI Default Command Execution (`kandown`)** — fixed `bin/tui.js` path resolution in global installation so running `kandown` with no arguments instantly opens both the browser AND the interactive terminal board UI (TUI), eliminating the fallback message `Open kandown.html in browser`.
6
+ - **Fixed**: **Automatic Browser Launch** — `kandown` now automatically invokes `open` (macOS), `xdg-open` (Linux), or `start` (Windows) on launch unless `--no-open` is specified.
7
+
8
+ ## 0.33.0 — 2026-07-21 — "Universal Auto-Updater & Web UI Prompt Engine"
9
+
10
+ - **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`).
11
+ - **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.
12
+ - **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.
13
+ - **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.
14
+ - **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.
15
+
3
16
  ## 0.32.2 — 2026-07-21 — "Reliable Auto-Updater Engine"
4
17
 
5
18
  - **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,9 +12,10 @@ 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.1";
16
16
 
17
17
  // src/cli/lib/updater.ts
18
+ var PKG_ROOT = resolve(import.meta.url ? new URL("../../..", import.meta.url).pathname : process.cwd());
18
19
  var CACHE_DIR = join(homedir(), ".kandown");
19
20
  var UPDATE_CHECK_CACHE = join(CACHE_DIR, ".update-check.json");
20
21
  var UPDATE_CHECK_INTERVAL_MS = 5 * 60 * 1e3;
@@ -147,7 +148,7 @@ async function checkForUpdate(argv = process.argv) {
147
148
  }
148
149
  } catch {
149
150
  }
150
- const latest = await new Promise((resolve3) => {
151
+ const latest = await new Promise((resolve4) => {
151
152
  const child2 = spawn("npm", ["view", "kandown", "version"], {
152
153
  timeout: 6e3,
153
154
  stdio: ["pipe", "pipe", "pipe"],
@@ -160,11 +161,11 @@ async function checkForUpdate(argv = process.argv) {
160
161
  });
161
162
  child2.stderr.on("data", () => {
162
163
  });
163
- child2.on("error", () => resolve3(null));
164
+ child2.on("error", () => resolve4(null));
164
165
  child2.on("close", (code) => {
165
- if (code !== 0) return resolve3(null);
166
+ if (code !== 0) return resolve4(null);
166
167
  const v = stdout.trim().replace(/^"|"$/g, "");
167
- resolve3(v || null);
168
+ resolve4(v || null);
168
169
  });
169
170
  });
170
171
  if (!latest) return;
@@ -486,6 +487,10 @@ function loadConfig(kandownDir) {
486
487
  }
487
488
  return merged;
488
489
  }
490
+ function saveConfig(kandownDir, config) {
491
+ const configPath = join2(kandownDir, "kandown.json");
492
+ atomicWriteFileSync(configPath, JSON.stringify(config, null, 2) + "\n");
493
+ }
489
494
 
490
495
  // src/cli/lib/board-reader.ts
491
496
  function getProjectRoot(kandownDir) {
@@ -543,11 +548,11 @@ function readTask(kandownDir, taskId) {
543
548
  }
544
549
  };
545
550
  }
546
- var PKG_ROOT = dirname(dirname(fileURLToPath(import.meta.url)));
551
+ var PKG_ROOT2 = dirname(dirname(fileURLToPath(import.meta.url)));
547
552
  function readAgentDoc(kandownDir) {
548
553
  const sections = [];
549
554
  try {
550
- sections.push(readFileSync3(join3(PKG_ROOT, "templates", "AGENT_KANDOWN.md"), "utf8").trim());
555
+ sections.push(readFileSync3(join3(PKG_ROOT2, "templates", "AGENT_KANDOWN.md"), "utf8").trim());
551
556
  } catch (e) {
552
557
  console.warn("[kandown] Could not read base agent rules:", e.message);
553
558
  }
@@ -695,16 +700,16 @@ async function fetchDaemonInfo(port) {
695
700
  }
696
701
  }
697
702
  function isPortListening(port, timeoutMs = 400) {
698
- return new Promise((resolve3) => {
703
+ return new Promise((resolve4) => {
699
704
  const socket = createConnection({ port, host: "127.0.0.1" }, () => {
700
705
  socket.destroy();
701
- resolve3(true);
706
+ resolve4(true);
702
707
  });
703
- socket.on("error", () => resolve3(false));
708
+ socket.on("error", () => resolve4(false));
704
709
  socket.setTimeout(timeoutMs);
705
710
  socket.on("timeout", () => {
706
711
  socket.destroy();
707
- resolve3(false);
712
+ resolve4(false);
708
713
  });
709
714
  });
710
715
  }
@@ -732,7 +737,7 @@ async function waitForDaemon(kandownDir, timeoutMs = 8e3) {
732
737
  if (metadata && isProcessAlive(metadata.pid) && await isPortListening(metadata.port)) {
733
738
  return { running: true, metadata };
734
739
  }
735
- await new Promise((resolve3) => setTimeout(resolve3, 120));
740
+ await new Promise((resolve4) => setTimeout(resolve4, 120));
736
741
  }
737
742
  return { running: false, metadata: null };
738
743
  }
@@ -762,6 +767,306 @@ async function startProjectDaemon(kandownDir, preferredPort) {
762
767
  return waitForDaemon(kandownDir);
763
768
  }
764
769
 
770
+ // src/cli/lib/server.ts
771
+ import { createServer } from "http";
772
+ import { existsSync as existsSync5, readFileSync as readFileSync5, copyFileSync, unlinkSync as unlinkSync5, mkdirSync as mkdirSync3 } from "fs";
773
+ import { join as join5, resolve as resolve2, dirname as dirname3 } from "path";
774
+ import { execSync as execSync2, spawn as spawn3 } from "child_process";
775
+ import { homedir as homedir3 } from "os";
776
+ var START_PORT_RANGE = 2050;
777
+ var END_PORT_RANGE = 2099;
778
+ var UNSAFE_PORTS = /* @__PURE__ */ new Set([2049, 4045, 6e3, 6665, 6666, 6667, 6668, 6669, 6697]);
779
+ var sseClients = [];
780
+ var nextClientId = 1;
781
+ function broadcastSseEvent(data) {
782
+ const payload = `data: ${JSON.stringify(data)}
783
+
784
+ `;
785
+ sseClients.forEach((c2) => c2.res.write(payload));
786
+ }
787
+ function handleCors(res) {
788
+ res.writeHead(204, {
789
+ "Access-Control-Allow-Origin": "*",
790
+ "Access-Control-Allow-Methods": "GET, POST, PUT, DELETE, OPTIONS",
791
+ "Access-Control-Allow-Headers": "Content-Type, X-Kandown-Token"
792
+ });
793
+ res.end();
794
+ }
795
+ function writeJson(res, status, data) {
796
+ res.writeHead(status, {
797
+ "Content-Type": "application/json",
798
+ "Access-Control-Allow-Origin": "*"
799
+ });
800
+ res.end(JSON.stringify(data));
801
+ }
802
+ function writeText(res, status, text) {
803
+ res.writeHead(status, {
804
+ "Content-Type": "text/plain; charset=utf-8",
805
+ "Access-Control-Allow-Origin": "*"
806
+ });
807
+ res.end(text);
808
+ }
809
+ function syncProjectKandownHtml(kandownDir) {
810
+ try {
811
+ const projectHtml = join5(kandownDir, "kandown.html");
812
+ const cliRoot = resolve2(import.meta.url ? new URL("../..", import.meta.url).pathname : process.cwd());
813
+ const distHtml = join5(cliRoot, "dist", "index.html");
814
+ if (!existsSync5(distHtml)) return false;
815
+ if (!existsSync5(projectHtml)) {
816
+ copyFileSync(distHtml, projectHtml);
817
+ return true;
818
+ }
819
+ const currentContent = readFileSync5(projectHtml, "utf8");
820
+ const newContent = readFileSync5(distHtml, "utf8");
821
+ if (currentContent !== newContent) {
822
+ atomicWriteFileSync(projectHtml, newContent);
823
+ return true;
824
+ }
825
+ } catch {
826
+ }
827
+ return false;
828
+ }
829
+ function syncGlobalSymlinks() {
830
+ try {
831
+ const home = homedir3();
832
+ const candidatePaths = [
833
+ join5(home, ".local", "bin", "kandown"),
834
+ join5(home, "Library", "pnpm", "bin", "kandown"),
835
+ join5(home, ".nvm", "versions", "node", "v25.2.1", "bin", "kandown")
836
+ ];
837
+ const currentBin = resolve2(import.meta.url ? new URL("../../bin/kandown.js", import.meta.url).pathname : process.cwd());
838
+ for (const targetPath of candidatePaths) {
839
+ if (existsSync5(dirname3(targetPath))) {
840
+ try {
841
+ if (existsSync5(targetPath)) unlinkSync5(targetPath);
842
+ execSync2(`ln -sf "${currentBin}" "${targetPath}" 2>/dev/null || true`);
843
+ } catch {
844
+ }
845
+ }
846
+ }
847
+ } catch {
848
+ }
849
+ }
850
+ async function handleApi(req, res, url, kandownDir) {
851
+ const path = url.pathname;
852
+ const method = req.method || "GET";
853
+ if (path === "/api/daemon" && method === "GET") {
854
+ return writeJson(res, 200, {
855
+ ok: true,
856
+ pid: process.pid,
857
+ kandownDir,
858
+ version: getCurrentVersion(),
859
+ startedAt: (/* @__PURE__ */ new Date()).toISOString(),
860
+ agentHook: process.env.KANDOWN_AGENT_HOOK_URL ? { enabled: true, label: process.env.KANDOWN_AGENT_HOOK_LABEL || "Send to Agent" } : null
861
+ });
862
+ }
863
+ if (path === "/api/version" && method === "GET") {
864
+ return writeJson(res, 200, {
865
+ version: getCurrentVersion()
866
+ });
867
+ }
868
+ if (path === "/api/update/check" && method === "GET") {
869
+ const current = getCurrentVersion();
870
+ const latest = await new Promise((resolve4) => {
871
+ const child = spawn3("npm", ["view", "kandown", "version"], {
872
+ timeout: 4e3,
873
+ stdio: ["pipe", "pipe", "pipe"],
874
+ env: { ...process.env },
875
+ detached: false
876
+ });
877
+ let stdout = "";
878
+ child.stdout.on("data", (d) => {
879
+ stdout += d;
880
+ });
881
+ child.stderr.on("data", () => {
882
+ });
883
+ child.on("error", () => resolve4(null));
884
+ child.on("close", (code) => {
885
+ if (code !== 0) return resolve4(null);
886
+ resolve4(stdout.trim().replace(/^"|"$/g, "") || null);
887
+ });
888
+ });
889
+ const updateAvailable = latest ? semverGt(latest, current) > 0 : false;
890
+ return writeJson(res, 200, {
891
+ current,
892
+ latest: latest || current,
893
+ updateAvailable
894
+ });
895
+ }
896
+ if (path === "/api/update/apply" && method === "POST") {
897
+ const current = getCurrentVersion();
898
+ const latest = await new Promise((resolve4) => {
899
+ const child = spawn3("npm", ["view", "kandown", "version"], {
900
+ timeout: 4e3,
901
+ stdio: ["pipe", "pipe", "pipe"],
902
+ env: { ...process.env },
903
+ detached: false
904
+ });
905
+ let stdout = "";
906
+ child.stdout.on("data", (d) => {
907
+ stdout += d;
908
+ });
909
+ child.on("error", () => resolve4(null));
910
+ child.on("close", (code) => resolve4(code === 0 ? stdout.trim() : null));
911
+ });
912
+ const targetVersion = latest || current;
913
+ const ok = await performGlobalPackageUpdate(`kandown@${targetVersion}`);
914
+ syncGlobalSymlinks();
915
+ syncProjectKandownHtml(kandownDir);
916
+ if (ok) {
917
+ writeJson(res, 200, { ok: true, version: targetVersion, message: "Update installed successfully" });
918
+ broadcastSseEvent({ type: "update", version: targetVersion });
919
+ } else {
920
+ writeJson(res, 500, { ok: false, message: "Global package installation failed" });
921
+ }
922
+ return;
923
+ }
924
+ if (path === "/api/events" && method === "GET") {
925
+ res.writeHead(200, {
926
+ "Content-Type": "text/event-stream",
927
+ "Cache-Control": "no-cache",
928
+ "Connection": "keep-alive",
929
+ "Access-Control-Allow-Origin": "*"
930
+ });
931
+ res.write("retry: 2000\n\n");
932
+ const id = nextClientId++;
933
+ sseClients.push({ id, res });
934
+ req.on("close", () => {
935
+ sseClients = sseClients.filter((c2) => c2.id !== id);
936
+ });
937
+ return;
938
+ }
939
+ if (path === "/api/board") {
940
+ if (method === "GET") {
941
+ const tasksDir = getTasksDir(kandownDir);
942
+ const boardPath = join5(tasksDir, "board.md");
943
+ const text = existsSync5(boardPath) ? readFileSync5(boardPath, "utf8") : "";
944
+ return writeText(res, 200, text);
945
+ }
946
+ if (method === "PUT") {
947
+ let body = "";
948
+ req.on("data", (chunk) => {
949
+ body += chunk;
950
+ });
951
+ req.on("end", () => {
952
+ const tasksDir = getTasksDir(kandownDir);
953
+ if (!existsSync5(tasksDir)) mkdirSync3(tasksDir, { recursive: true });
954
+ atomicWriteFileSync(join5(tasksDir, "board.md"), body);
955
+ broadcastSseEvent({ type: "board" });
956
+ writeJson(res, 200, { ok: true });
957
+ });
958
+ return;
959
+ }
960
+ }
961
+ if (path === "/api/config") {
962
+ if (method === "GET") {
963
+ return writeJson(res, 200, loadConfig(kandownDir));
964
+ }
965
+ if (method === "PUT") {
966
+ let body = "";
967
+ req.on("data", (chunk) => {
968
+ body += chunk;
969
+ });
970
+ req.on("end", () => {
971
+ try {
972
+ const parsed = JSON.parse(body);
973
+ saveConfig(kandownDir, parsed);
974
+ broadcastSseEvent({ type: "config" });
975
+ writeJson(res, 200, { ok: true });
976
+ } catch (e) {
977
+ writeJson(res, 400, { error: e.message });
978
+ }
979
+ });
980
+ return;
981
+ }
982
+ }
983
+ if (path === "/api/tasks" && method === "GET") {
984
+ return writeJson(res, 200, listTaskIds(kandownDir));
985
+ }
986
+ if (path.startsWith("/api/tasks/")) {
987
+ const taskId = decodeURIComponent(path.slice("/api/tasks/".length));
988
+ const tasksDir = getTasksDir(kandownDir);
989
+ const taskPath = join5(tasksDir, `${taskId}.md`);
990
+ if (method === "GET") {
991
+ if (!existsSync5(taskPath)) return writeText(res, 404, "Task not found");
992
+ return writeText(res, 200, readFileSync5(taskPath, "utf8"));
993
+ }
994
+ if (method === "PUT") {
995
+ let body = "";
996
+ req.on("data", (chunk) => {
997
+ body += chunk;
998
+ });
999
+ req.on("end", () => {
1000
+ if (!existsSync5(tasksDir)) mkdirSync3(tasksDir, { recursive: true });
1001
+ atomicWriteFileSync(taskPath, body);
1002
+ broadcastSseEvent({ type: "task", id: taskId });
1003
+ writeJson(res, 200, { ok: true });
1004
+ });
1005
+ return;
1006
+ }
1007
+ if (method === "DELETE") {
1008
+ if (existsSync5(taskPath)) unlinkSync5(taskPath);
1009
+ broadcastSseEvent({ type: "task_delete", id: taskId });
1010
+ return writeJson(res, 200, { ok: true });
1011
+ }
1012
+ }
1013
+ writeJson(res, 404, { error: "Route not found" });
1014
+ }
1015
+ function serveApp(res, kandownDir) {
1016
+ syncProjectKandownHtml(kandownDir);
1017
+ const htmlPath = join5(kandownDir, "kandown.html");
1018
+ if (existsSync5(htmlPath)) {
1019
+ res.writeHead(200, { "Content-Type": "text/html; charset=utf-8" });
1020
+ res.end(readFileSync5(htmlPath, "utf8"));
1021
+ } else {
1022
+ writeText(res, 404, "kandown.html not found");
1023
+ }
1024
+ }
1025
+ function createServeServer(kandownDir) {
1026
+ syncProjectKandownHtml(kandownDir);
1027
+ syncGlobalSymlinks();
1028
+ return createServer((req, res) => {
1029
+ const url = new URL(req.url || "/", "http://localhost");
1030
+ if (req.method === "OPTIONS") return handleCors(res);
1031
+ if (url.pathname === "/" || url.pathname === "/kandown.html") {
1032
+ return serveApp(res, kandownDir);
1033
+ }
1034
+ if (url.pathname.startsWith("/api/")) {
1035
+ return handleApi(req, res, url, kandownDir);
1036
+ }
1037
+ writeText(res, 404, "Not found");
1038
+ });
1039
+ }
1040
+ function listen(server, port) {
1041
+ return new Promise((resolveListen, rejectListen) => {
1042
+ const onError = (e) => {
1043
+ server.off("listening", onListening);
1044
+ rejectListen(e);
1045
+ };
1046
+ const onListening = () => {
1047
+ server.off("error", onError);
1048
+ resolveListen();
1049
+ };
1050
+ server.once("error", onError);
1051
+ server.once("listening", onListening);
1052
+ server.listen(port, "127.0.0.1");
1053
+ });
1054
+ }
1055
+ async function listenOnAvailablePort(kandownDir, preferredPort) {
1056
+ const startPort = preferredPort ?? START_PORT_RANGE;
1057
+ for (let p = startPort; p <= END_PORT_RANGE; p++) {
1058
+ if (UNSAFE_PORTS.has(p)) continue;
1059
+ const server = createServeServer(kandownDir);
1060
+ try {
1061
+ await listen(server, p);
1062
+ return { server, port: p };
1063
+ } catch (e) {
1064
+ if (e.code !== "EADDRINUSE" && e.code !== "EACCES") throw e;
1065
+ }
1066
+ }
1067
+ throw new Error(`No free port in range ${START_PORT_RANGE}-${END_PORT_RANGE}`);
1068
+ }
1069
+
765
1070
  // src/cli/cli.ts
766
1071
  var c = {
767
1072
  reset: "\x1B[0m",
@@ -785,6 +1090,13 @@ function success(msg) {
785
1090
  function err(msg) {
786
1091
  console.error(`${c.red}\u2717${c.reset} ${msg}`);
787
1092
  }
1093
+ function openBrowser(target) {
1094
+ const cmd = process.platform === "darwin" ? "open" : process.platform === "win32" ? "start" : "xdg-open";
1095
+ try {
1096
+ spawn4(cmd, [target], { detached: true, stdio: "ignore" }).unref();
1097
+ } catch {
1098
+ }
1099
+ }
788
1100
  function parseArgs(args) {
789
1101
  const flags = {};
790
1102
  const positional = [];
@@ -808,16 +1120,16 @@ function parseArgs(args) {
808
1120
  return { flags, positional, path: typeof flags.path === "string" ? flags.path : ".kandown" };
809
1121
  }
810
1122
  function resolveKandownDir(pathArg = ".kandown", cwd = process.cwd()) {
811
- if (basename(cwd) === ".kandown" || existsSync5(join5(cwd, "kandown.json"))) {
1123
+ if (basename(cwd) === ".kandown" || existsSync6(join6(cwd, "kandown.json"))) {
812
1124
  return cwd;
813
1125
  }
814
- return resolve2(cwd, pathArg);
1126
+ return resolve3(cwd, pathArg);
815
1127
  }
816
1128
  function ensureKandownDir(rawArgs) {
817
1129
  const args = parseArgs(rawArgs);
818
1130
  const cwd = process.cwd();
819
1131
  const kandownDir = resolveKandownDir(args.path, cwd);
820
- if (!existsSync5(kandownDir)) {
1132
+ if (!existsSync6(kandownDir)) {
821
1133
  err(`No Kandown installation found at ${c.bold}${kandownDir}${c.reset}`);
822
1134
  log(` Run ${c.cyan}npx kandown init${c.reset} to create one.`);
823
1135
  process.exit(1);
@@ -833,7 +1145,7 @@ ${c.bold}USAGE:${c.reset}
833
1145
  kandown [command] [options]
834
1146
 
835
1147
  ${c.bold}COMMANDS:${c.reset}
836
- (none) Start web server & launch TUI
1148
+ (none) Start web server, open browser & launch TUI
837
1149
  work Output agent rules + live board digest
838
1150
  list List tasks (alias: ls)
839
1151
  show <id> Display task details
@@ -860,8 +1172,8 @@ ${c.bold}OPTIONS:${c.reset}
860
1172
  async function cmdUpdate(rawArgs) {
861
1173
  const current = getCurrentVersion();
862
1174
  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"], {
1175
+ const latest = await new Promise((resolve4) => {
1176
+ const child = spawn4("npm", ["view", "kandown", "version"], {
865
1177
  timeout: 6e3,
866
1178
  stdio: ["pipe", "pipe", "pipe"],
867
1179
  env: { ...process.env },
@@ -873,11 +1185,11 @@ async function cmdUpdate(rawArgs) {
873
1185
  });
874
1186
  child.stderr.on("data", () => {
875
1187
  });
876
- child.on("error", () => resolve3(null));
1188
+ child.on("error", () => resolve4(null));
877
1189
  child.on("close", (code) => {
878
- if (code !== 0) return resolve3(null);
1190
+ if (code !== 0) return resolve4(null);
879
1191
  const v = stdout.trim().replace(/^"|"$/g, "");
880
- resolve3(v || null);
1192
+ resolve4(v || null);
881
1193
  });
882
1194
  });
883
1195
  if (latest && semverGt(latest, current) > 0) {
@@ -893,12 +1205,12 @@ async function cmdUpdate(rawArgs) {
893
1205
  }
894
1206
  const args = parseArgs(rawArgs);
895
1207
  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);
1208
+ const kandownDir = resolve3(cwd, args.path);
1209
+ const htmlDest = join6(kandownDir, "kandown.html");
1210
+ if (existsSync6(htmlDest)) {
1211
+ const htmlSrc = resolve3(PKG_ROOT, "dist", "index.html");
1212
+ if (existsSync6(htmlSrc)) {
1213
+ copyFileSync2(htmlSrc, htmlDest);
902
1214
  success(`Refreshed ${args.path}/kandown.html`);
903
1215
  }
904
1216
  }
@@ -909,10 +1221,10 @@ async function cmdDoctor(rawArgs) {
909
1221
  log(`${c.bold}kandown doctor${c.reset} ${c.dim}\u2014 environment & board diagnostic${c.reset}
910
1222
  `);
911
1223
  log(` CLI Version: ${currentVersion}`);
912
- const configPath = join5(kandownDir, "kandown.json");
913
- if (existsSync5(configPath)) {
1224
+ const configPath = join6(kandownDir, "kandown.json");
1225
+ if (existsSync6(configPath)) {
914
1226
  try {
915
- JSON.parse(readFileSync5(configPath, "utf8"));
1227
+ JSON.parse(readFileSync6(configPath, "utf8"));
916
1228
  success("kandown.json valid");
917
1229
  } catch (e) {
918
1230
  err(`kandown.json invalid: ${e.message}`);
@@ -1028,18 +1340,64 @@ async function main() {
1028
1340
  case "-h":
1029
1341
  help();
1030
1342
  break;
1343
+ case "daemon": {
1344
+ const subcommand = rest[0] || "status";
1345
+ const { kandownDir } = ensureKandownDir(rest.slice(1));
1346
+ if (subcommand === "run") {
1347
+ const { port } = await listenOnAvailablePort(kandownDir);
1348
+ const url = `http://localhost:${port}`;
1349
+ const metadataPath2 = join6(kandownDir, "daemon.json");
1350
+ atomicWriteFileSync(metadataPath2, JSON.stringify({
1351
+ pid: process.pid,
1352
+ port,
1353
+ url,
1354
+ kandownDir,
1355
+ startedAt: (/* @__PURE__ */ new Date()).toISOString(),
1356
+ version: getCurrentVersion(),
1357
+ token: null
1358
+ }, null, 2));
1359
+ info(`Kandown daemon running on port ${port} (PID ${process.pid})`);
1360
+ await new Promise(() => {
1361
+ });
1362
+ } else if (subcommand === "stop") {
1363
+ const status = await getDaemonStatus(kandownDir);
1364
+ if (status.running && status.metadata) {
1365
+ try {
1366
+ process.kill(status.metadata.pid, "SIGTERM");
1367
+ } catch {
1368
+ }
1369
+ success(`Stopped daemon PID ${status.metadata.pid}`);
1370
+ } else {
1371
+ info("Daemon not running");
1372
+ }
1373
+ } else {
1374
+ const status = await getDaemonStatus(kandownDir);
1375
+ if (status.running && status.metadata) {
1376
+ success(`Daemon running on port ${status.metadata.port} (PID ${status.metadata.pid})`);
1377
+ } else {
1378
+ info("Daemon not running");
1379
+ }
1380
+ }
1381
+ break;
1382
+ }
1031
1383
  case void 0: {
1384
+ const parsed = parseArgs(rest);
1032
1385
  const { kandownDir } = ensureKandownDir(rest);
1033
- const status = await getDaemonStatus(kandownDir);
1386
+ let status = await getDaemonStatus(kandownDir);
1034
1387
  if (!status.running) {
1035
- await startProjectDaemon(kandownDir);
1388
+ status = await startProjectDaemon(kandownDir);
1389
+ }
1390
+ if (!parsed.flags["no-open"]) {
1391
+ const urlToOpen = status.metadata?.url || join6(kandownDir, "kandown.html");
1392
+ openBrowser(urlToOpen);
1036
1393
  }
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" });
1394
+ const tuiPath = join6(PKG_ROOT, "bin", "tui.js");
1395
+ if (existsSync6(tuiPath)) {
1396
+ const child = spawn4(process.execPath, [tuiPath, ...rest], { stdio: "inherit" });
1040
1397
  child.on("close", (code) => process.exit(code || 0));
1041
1398
  } else {
1042
- info("Kandown daemon running. Open kandown.html in browser.");
1399
+ err(`TUI binary not found at ${tuiPath}`);
1400
+ process.exit(1);
1043
1401
  }
1044
1402
  break;
1045
1403
  }