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 +8 -0
- package/bin/kandown.js +377 -33
- package/dist/index.html +174 -149
- package/package.json +1 -1
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
|
|
5
|
-
import { join as
|
|
6
|
-
import { spawn as
|
|
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.
|
|
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((
|
|
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", () =>
|
|
163
|
+
child2.on("error", () => resolve4(null));
|
|
164
164
|
child2.on("close", (code) => {
|
|
165
|
-
if (code !== 0) return
|
|
165
|
+
if (code !== 0) return resolve4(null);
|
|
166
166
|
const v = stdout.trim().replace(/^"|"$/g, "");
|
|
167
|
-
|
|
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((
|
|
702
|
+
return new Promise((resolve4) => {
|
|
699
703
|
const socket = createConnection({ port, host: "127.0.0.1" }, () => {
|
|
700
704
|
socket.destroy();
|
|
701
|
-
|
|
705
|
+
resolve4(true);
|
|
702
706
|
});
|
|
703
|
-
socket.on("error", () =>
|
|
707
|
+
socket.on("error", () => resolve4(false));
|
|
704
708
|
socket.setTimeout(timeoutMs);
|
|
705
709
|
socket.on("timeout", () => {
|
|
706
710
|
socket.destroy();
|
|
707
|
-
|
|
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((
|
|
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" ||
|
|
1115
|
+
if (basename(cwd) === ".kandown" || existsSync6(join6(cwd, "kandown.json"))) {
|
|
812
1116
|
return cwd;
|
|
813
1117
|
}
|
|
814
|
-
return
|
|
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 (!
|
|
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((
|
|
864
|
-
const child =
|
|
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", () =>
|
|
1180
|
+
child.on("error", () => resolve4(null));
|
|
877
1181
|
child.on("close", (code) => {
|
|
878
|
-
if (code !== 0) return
|
|
1182
|
+
if (code !== 0) return resolve4(null);
|
|
879
1183
|
const v = stdout.trim().replace(/^"|"$/g, "");
|
|
880
|
-
|
|
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 =
|
|
897
|
-
const htmlDest =
|
|
898
|
-
if (
|
|
899
|
-
const htmlSrc =
|
|
900
|
-
if (
|
|
901
|
-
|
|
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 =
|
|
913
|
-
if (
|
|
1216
|
+
const configPath = join6(kandownDir, "kandown.json");
|
|
1217
|
+
if (existsSync6(configPath)) {
|
|
914
1218
|
try {
|
|
915
|
-
JSON.parse(
|
|
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 =
|
|
1038
|
-
if (
|
|
1039
|
-
const child =
|
|
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.");
|