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 +13 -0
- package/bin/kandown.js +397 -39
- package/dist/index.html +174 -149
- package/package.json +1 -1
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
|
|
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,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.
|
|
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((
|
|
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", () =>
|
|
164
|
+
child2.on("error", () => resolve4(null));
|
|
164
165
|
child2.on("close", (code) => {
|
|
165
|
-
if (code !== 0) return
|
|
166
|
+
if (code !== 0) return resolve4(null);
|
|
166
167
|
const v = stdout.trim().replace(/^"|"$/g, "");
|
|
167
|
-
|
|
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
|
|
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(
|
|
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((
|
|
703
|
+
return new Promise((resolve4) => {
|
|
699
704
|
const socket = createConnection({ port, host: "127.0.0.1" }, () => {
|
|
700
705
|
socket.destroy();
|
|
701
|
-
|
|
706
|
+
resolve4(true);
|
|
702
707
|
});
|
|
703
|
-
socket.on("error", () =>
|
|
708
|
+
socket.on("error", () => resolve4(false));
|
|
704
709
|
socket.setTimeout(timeoutMs);
|
|
705
710
|
socket.on("timeout", () => {
|
|
706
711
|
socket.destroy();
|
|
707
|
-
|
|
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((
|
|
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" ||
|
|
1123
|
+
if (basename(cwd) === ".kandown" || existsSync6(join6(cwd, "kandown.json"))) {
|
|
812
1124
|
return cwd;
|
|
813
1125
|
}
|
|
814
|
-
return
|
|
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 (!
|
|
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((
|
|
864
|
-
const child =
|
|
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", () =>
|
|
1188
|
+
child.on("error", () => resolve4(null));
|
|
877
1189
|
child.on("close", (code) => {
|
|
878
|
-
if (code !== 0) return
|
|
1190
|
+
if (code !== 0) return resolve4(null);
|
|
879
1191
|
const v = stdout.trim().replace(/^"|"$/g, "");
|
|
880
|
-
|
|
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 =
|
|
897
|
-
const htmlDest =
|
|
898
|
-
if (
|
|
899
|
-
const htmlSrc =
|
|
900
|
-
if (
|
|
901
|
-
|
|
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 =
|
|
913
|
-
if (
|
|
1224
|
+
const configPath = join6(kandownDir, "kandown.json");
|
|
1225
|
+
if (existsSync6(configPath)) {
|
|
914
1226
|
try {
|
|
915
|
-
JSON.parse(
|
|
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
|
-
|
|
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 =
|
|
1038
|
-
if (
|
|
1039
|
-
const child =
|
|
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
|
-
|
|
1399
|
+
err(`TUI binary not found at ${tuiPath}`);
|
|
1400
|
+
process.exit(1);
|
|
1043
1401
|
}
|
|
1044
1402
|
break;
|
|
1045
1403
|
}
|