kandown 0.14.1 → 0.15.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/bin/kandown.js +62 -12
- package/dist/index.html +53 -53
- package/package.json +1 -1
package/bin/kandown.js
CHANGED
|
@@ -63,15 +63,17 @@ import {
|
|
|
63
63
|
rmdirSync,
|
|
64
64
|
} from 'node:fs';
|
|
65
65
|
import { spawnSync, spawn, execSync } from 'node:child_process';
|
|
66
|
+
import { createConnection } from 'node:net';
|
|
66
67
|
|
|
67
68
|
const __filename = fileURLToPath(import.meta.url);
|
|
68
69
|
const __dirname = dirname(__filename);
|
|
69
70
|
const PKG_ROOT = resolve(__dirname, '..');
|
|
70
71
|
// 📖 Default localhost range for the zero-config `kandown` web UI server.
|
|
71
|
-
|
|
72
|
-
|
|
72
|
+
// 📖 Single source of truth for the daemon port range. Each kandown project
|
|
73
|
+
// gets its own daemon on the first free port in this range, so multiple
|
|
74
|
+
// projects can run in parallel (A=2048, B=2049, C=2050, ...).
|
|
73
75
|
const START_PORT_RANGE = 2048;
|
|
74
|
-
const END_PORT_RANGE =
|
|
76
|
+
const END_PORT_RANGE = 2150;
|
|
75
77
|
const DAEMON_FILE = 'daemon.json';
|
|
76
78
|
|
|
77
79
|
// 📖 Get current CLI version from package.json at PKG_ROOT
|
|
@@ -306,7 +308,7 @@ ${c.bold}Commands:${c.reset}
|
|
|
306
308
|
${c.cyan}help${c.reset} Show this help
|
|
307
309
|
|
|
308
310
|
${c.bold}Options:${c.reset}
|
|
309
|
-
${c.cyan}--port <n>${c.reset} Preferred local HTTP port for ${c.cyan}kandown${c.reset} (default: ${
|
|
311
|
+
${c.cyan}--port <n>${c.reset} Preferred local HTTP port for ${c.cyan}kandown${c.reset} (default: ${START_PORT_RANGE}, auto-increments to ${END_PORT_RANGE})
|
|
310
312
|
|
|
311
313
|
${c.bold}Examples:${c.reset}
|
|
312
314
|
${c.dim}$${c.reset} npx kandown ${c.dim}# local web server + board TUI${c.reset}
|
|
@@ -1280,6 +1282,31 @@ async function fetchDaemonInfo(port) {
|
|
|
1280
1282
|
}
|
|
1281
1283
|
}
|
|
1282
1284
|
|
|
1285
|
+
/**
|
|
1286
|
+
* 📖 Fast, dependency-free TCP probe. Returns true the instant a port accepts
|
|
1287
|
+
* a connection — i.e. the moment the daemon has actually bound its socket.
|
|
1288
|
+
* Used instead of an HTTP fetch to detect startup, because Node's fetch
|
|
1289
|
+
* (undici) can fail to recover from the initial ECONNREFUSED window (fetches
|
|
1290
|
+
* sent before the server binds) and report a healthy local server as down for
|
|
1291
|
+
* seconds, which orphaned freshly-started multi-project daemons. A raw TCP
|
|
1292
|
+
* connect has no connection-pool state to poison, so it reliably tracks the
|
|
1293
|
+
* real liveness of the socket.
|
|
1294
|
+
*/
|
|
1295
|
+
function isPortListening(port, timeoutMs = 400) {
|
|
1296
|
+
return new Promise((resolve) => {
|
|
1297
|
+
const socket = createConnection({ port, host: '127.0.0.1' }, () => {
|
|
1298
|
+
socket.destroy();
|
|
1299
|
+
resolve(true);
|
|
1300
|
+
});
|
|
1301
|
+
socket.on('error', () => resolve(false));
|
|
1302
|
+
socket.setTimeout(timeoutMs);
|
|
1303
|
+
socket.on('timeout', () => {
|
|
1304
|
+
socket.destroy();
|
|
1305
|
+
resolve(false);
|
|
1306
|
+
});
|
|
1307
|
+
});
|
|
1308
|
+
}
|
|
1309
|
+
|
|
1283
1310
|
async function getDaemonStatus(kandownDir) {
|
|
1284
1311
|
const metadata = readDaemonMetadata(kandownDir);
|
|
1285
1312
|
if (!metadata) return { running: false, metadata: null };
|
|
@@ -1288,7 +1315,17 @@ async function getDaemonStatus(kandownDir) {
|
|
|
1288
1315
|
return { running: false, metadata: null };
|
|
1289
1316
|
}
|
|
1290
1317
|
const remote = await fetchDaemonInfo(metadata.port);
|
|
1291
|
-
if (!remote
|
|
1318
|
+
if (!remote) {
|
|
1319
|
+
// 📖 Transient fetch failure (server still warming up, undici connection
|
|
1320
|
+
// hiccup, high load). The owning process IS alive (checked above), so we
|
|
1321
|
+
// must NOT destroy the metadata here — otherwise starting a 2nd kandown
|
|
1322
|
+
// project races the just-written metadata and orphans a healthy daemon.
|
|
1323
|
+
// Return non-running so callers retry; the metadata stays intact.
|
|
1324
|
+
return { running: false, metadata: null };
|
|
1325
|
+
}
|
|
1326
|
+
if (remote.kandownDir !== kandownDir || remote.pid !== metadata.pid) {
|
|
1327
|
+
// 📖 Real conflict: the port is owned by a DIFFERENT kandown (another
|
|
1328
|
+
// project, or a reincarnated PID). That is a genuine stale entry — remove.
|
|
1292
1329
|
removeDaemonMetadata(kandownDir);
|
|
1293
1330
|
return { running: false, metadata: null };
|
|
1294
1331
|
}
|
|
@@ -1305,12 +1342,22 @@ function refreshKandownHtml(kandownDir) {
|
|
|
1305
1342
|
return false;
|
|
1306
1343
|
}
|
|
1307
1344
|
|
|
1308
|
-
async function waitForDaemon(kandownDir, timeoutMs =
|
|
1345
|
+
async function waitForDaemon(kandownDir, timeoutMs = 8000) {
|
|
1346
|
+
// 📖 Detect daemon startup via TCP probe + process liveness rather than HTTP
|
|
1347
|
+
// fetch. The freshly spawned child is known to be ours, so once its process
|
|
1348
|
+
// is alive AND its port accepts a TCP connection, the daemon is up — no need
|
|
1349
|
+
// to wait for an HTTP round-trip that undici may not serve reliably during
|
|
1350
|
+
// the bind window. The dir/pid ownership check still happens later via
|
|
1351
|
+
// getDaemonStatus() for the status/stop commands.
|
|
1309
1352
|
const started = Date.now();
|
|
1310
1353
|
while (Date.now() - started < timeoutMs) {
|
|
1311
|
-
const
|
|
1312
|
-
if (
|
|
1313
|
-
|
|
1354
|
+
const metadata = readDaemonMetadata(kandownDir);
|
|
1355
|
+
if (metadata && isProcessAlive(metadata.pid)) {
|
|
1356
|
+
if (await isPortListening(metadata.port)) {
|
|
1357
|
+
return { running: true, metadata };
|
|
1358
|
+
}
|
|
1359
|
+
}
|
|
1360
|
+
await new Promise(r => setTimeout(r, 120));
|
|
1314
1361
|
}
|
|
1315
1362
|
return { running: false, metadata: null };
|
|
1316
1363
|
}
|
|
@@ -1835,10 +1882,13 @@ async function listenOnAvailablePort(kandownDir, preferredPort) {
|
|
|
1835
1882
|
const port = preferredPort ?? START_PORT_RANGE;
|
|
1836
1883
|
const isSinglePort = preferredPort !== null;
|
|
1837
1884
|
|
|
1838
|
-
// 📖 Check if the target port is already occupied by a stale kandown process
|
|
1839
|
-
//
|
|
1885
|
+
// 📖 Check if the target port is already occupied by a stale kandown process
|
|
1886
|
+
// from the SAME project. A stale process is one whose TUI exited but the HTTP
|
|
1887
|
+
// server is still alive. We ONLY reclaim same-project daemons here — a daemon
|
|
1888
|
+
// belonging to a DIFFERENT project (reason === null) must never be touched;
|
|
1889
|
+
// it is handled by the scan loop below (which skips it).
|
|
1840
1890
|
const staleInfo = detectStaleKandown(port, kandownDir);
|
|
1841
|
-
if (staleInfo) {
|
|
1891
|
+
if (staleInfo && staleInfo.reason) {
|
|
1842
1892
|
warn(staleInfo.reason);
|
|
1843
1893
|
try {
|
|
1844
1894
|
process.kill(staleInfo.pid, 'SIGTERM');
|