kandown 0.7.1 → 0.7.3
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 +112 -8
- package/bin/tui.js +5 -5
- package/dist/index.html +259 -259
- package/package.json +1 -1
package/bin/kandown.js
CHANGED
|
@@ -53,7 +53,7 @@ import {
|
|
|
53
53
|
statSync,
|
|
54
54
|
unlinkSync,
|
|
55
55
|
} from 'node:fs';
|
|
56
|
-
import { spawnSync, spawn } from 'node:child_process';
|
|
56
|
+
import { spawnSync, spawn, execSync } from 'node:child_process';
|
|
57
57
|
|
|
58
58
|
const __filename = fileURLToPath(import.meta.url);
|
|
59
59
|
const __dirname = dirname(__filename);
|
|
@@ -791,26 +791,130 @@ function listen(server, port) {
|
|
|
791
791
|
}
|
|
792
792
|
|
|
793
793
|
async function listenOnAvailablePort(kandownDir, preferredPort) {
|
|
794
|
-
const
|
|
795
|
-
const
|
|
794
|
+
const port = preferredPort ?? START_PORT_RANGE;
|
|
795
|
+
const isSinglePort = preferredPort !== null;
|
|
796
|
+
|
|
797
|
+
// 📖 Check if the target port is already occupied by a stale kandown process.
|
|
798
|
+
// This happens when the TUI crashes but the HTTP server stays alive.
|
|
799
|
+
const staleInfo = detectStaleKandown(port, kandownDir);
|
|
800
|
+
if (staleInfo) {
|
|
801
|
+
warn(staleInfo.reason);
|
|
802
|
+
try {
|
|
803
|
+
process.kill(staleInfo.pid, 'SIGTERM');
|
|
804
|
+
// Give it a moment to clean up
|
|
805
|
+
await new Promise(r => setTimeout(r, 300));
|
|
806
|
+
info(`Reclaimed port ${c.cyan}${port}${c.reset} (killed stale kandown PID ${staleInfo.pid})`);
|
|
807
|
+
} catch {
|
|
808
|
+
// Process already dead
|
|
809
|
+
}
|
|
810
|
+
}
|
|
796
811
|
|
|
797
|
-
|
|
812
|
+
// If user specified a specific port, only try that one
|
|
813
|
+
if (isSinglePort) {
|
|
798
814
|
const server = createServeServer(kandownDir);
|
|
799
815
|
try {
|
|
800
816
|
await listen(server, port);
|
|
801
817
|
return { server, port };
|
|
818
|
+
} catch (e) {
|
|
819
|
+
if (e.code === 'EADDRINUSE') {
|
|
820
|
+
err(`Port ${c.bold}${port}${c.reset} is in use by another application.`);
|
|
821
|
+
process.exit(1);
|
|
822
|
+
}
|
|
823
|
+
throw e;
|
|
824
|
+
}
|
|
825
|
+
}
|
|
826
|
+
|
|
827
|
+
// 📖 Scan range — try ports until one works
|
|
828
|
+
for (let p = START_PORT_RANGE; p <= END_PORT_RANGE; p++) {
|
|
829
|
+
// Skip port if occupied by a stale kandown from a DIFFERENT project
|
|
830
|
+
const stale = detectStaleKandown(p, kandownDir);
|
|
831
|
+
if (stale && !stale.reason) {
|
|
832
|
+
// reason is null → different project, skip this port
|
|
833
|
+
continue;
|
|
834
|
+
}
|
|
835
|
+
if (stale) {
|
|
836
|
+
// Same project — already killed above for the first port, but handle edge case
|
|
837
|
+
try { process.kill(stale.pid, 'SIGTERM'); } catch {}
|
|
838
|
+
await new Promise(r => setTimeout(r, 200));
|
|
839
|
+
}
|
|
840
|
+
|
|
841
|
+
const server = createServeServer(kandownDir);
|
|
842
|
+
try {
|
|
843
|
+
await listen(server, p);
|
|
844
|
+
return { server, port: p };
|
|
802
845
|
} catch (e) {
|
|
803
846
|
if (e.code !== 'EADDRINUSE') throw e;
|
|
804
847
|
}
|
|
805
848
|
}
|
|
806
849
|
|
|
807
|
-
|
|
808
|
-
? `${START_PORT_RANGE}-${END_PORT_RANGE}`
|
|
809
|
-
: String(preferredPort);
|
|
810
|
-
err(`No free port available in ${c.bold}${range}${c.reset}.`);
|
|
850
|
+
err(`No free port available in ${c.bold}${START_PORT_RANGE}-${END_PORT_RANGE}${c.reset}.`);
|
|
811
851
|
process.exit(1);
|
|
812
852
|
}
|
|
813
853
|
|
|
854
|
+
/**
|
|
855
|
+
* 📖 Detects if a port is occupied by a stale/zombie kandown process.
|
|
856
|
+
* A "stale" kandown is one whose TUI has exited but the HTTP server is still alive.
|
|
857
|
+
* Returns { pid, cwd, reason } or null if the port is free or used by something else.
|
|
858
|
+
*/
|
|
859
|
+
function detectStaleKandown(port, currentKandownDir) {
|
|
860
|
+
let pid;
|
|
861
|
+
try {
|
|
862
|
+
pid = execSync(`lsof -ti :${port} -sTCP:LISTEN`, { encoding: 'utf8', timeout: 2000 }).trim();
|
|
863
|
+
} catch {
|
|
864
|
+
return null; // Port is free
|
|
865
|
+
}
|
|
866
|
+
if (!pid) return null;
|
|
867
|
+
|
|
868
|
+
const pids = pid.split('\n').filter(Boolean);
|
|
869
|
+
if (pids.length === 0) return null;
|
|
870
|
+
pid = parseInt(pids[0], 10);
|
|
871
|
+
if (isNaN(pid) || pid === process.pid) return null;
|
|
872
|
+
|
|
873
|
+
// Check if it's a kandown process
|
|
874
|
+
let cmdline;
|
|
875
|
+
try {
|
|
876
|
+
cmdline = execSync(`ps -p ${pid} -o command=`, { encoding: 'utf8', timeout: 2000 }).trim();
|
|
877
|
+
} catch {
|
|
878
|
+
return null; // Process gone
|
|
879
|
+
}
|
|
880
|
+
|
|
881
|
+
if (!/[/\\]kandown\b|^kandown\b|\skandown\b/.test(cmdline)) return null; // Not a kandown process
|
|
882
|
+
|
|
883
|
+
// Get the working directory of the existing process
|
|
884
|
+
let cwd;
|
|
885
|
+
try {
|
|
886
|
+
if (process.platform === 'linux') {
|
|
887
|
+
cwd = execSync(`readlink -f /proc/${pid}/cwd`, { encoding: 'utf8', timeout: 2000 }).trim();
|
|
888
|
+
} else {
|
|
889
|
+
// macOS
|
|
890
|
+
cwd = execSync(`lsof -p ${pid} -Fn -a -d cwd 2>/dev/null | grep '^n' | cut -c2-`, { encoding: 'utf8', timeout: 2000, shell: true }).trim();
|
|
891
|
+
}
|
|
892
|
+
} catch {
|
|
893
|
+
cwd = null;
|
|
894
|
+
}
|
|
895
|
+
|
|
896
|
+
// 📖 Compare with our project root (parent of .kandown/), not process.cwd().
|
|
897
|
+
// process.cwd() could be a subdirectory; the project root is stable.
|
|
898
|
+
const ourProjectRoot = currentKandownDir ? dirname(currentKandownDir) : process.cwd();
|
|
899
|
+
const isSameProject = cwd && (cwd === ourProjectRoot || cwd === process.cwd());
|
|
900
|
+
|
|
901
|
+
// 📖 If it's our own process, skip (we're checking our own port)
|
|
902
|
+
if (pid === process.pid) return null;
|
|
903
|
+
|
|
904
|
+
// 📖 Different project — don't touch, just skip the port
|
|
905
|
+
if (!isSameProject) {
|
|
906
|
+
return { pid, cwd: cwd || 'unknown', reason: null };
|
|
907
|
+
}
|
|
908
|
+
|
|
909
|
+
// 📖 Same project — stale/zombie or legitimate, kill it either way.
|
|
910
|
+
// The user is explicitly launching kandown, so they want a fresh instance.
|
|
911
|
+
return {
|
|
912
|
+
pid,
|
|
913
|
+
cwd: cwd || 'unknown',
|
|
914
|
+
reason: `${c.yellow}Existing kandown found on port ${c.cyan}${port}${c.yellow} (PID ${pid}, same project). Reconnecting...${c.reset}`,
|
|
915
|
+
};
|
|
916
|
+
}
|
|
917
|
+
|
|
814
918
|
/**
|
|
815
919
|
* 📖 Starts the local web UI server, opens it in the browser, then hands the
|
|
816
920
|
* terminal to the board TUI. The server intentionally stays in this process so
|
package/bin/tui.js
CHANGED
|
@@ -56299,8 +56299,8 @@ var FileWatcher = class {
|
|
|
56299
56299
|
knownTaskIds = /* @__PURE__ */ new Set();
|
|
56300
56300
|
listeners = /* @__PURE__ */ new Map();
|
|
56301
56301
|
debounceTimers = /* @__PURE__ */ new Map();
|
|
56302
|
-
debounceDelay =
|
|
56303
|
-
watchDebounceDelay =
|
|
56302
|
+
debounceDelay = 30;
|
|
56303
|
+
watchDebounceDelay = 75;
|
|
56304
56304
|
pollInterval = null;
|
|
56305
56305
|
stopped = false;
|
|
56306
56306
|
/**
|
|
@@ -56321,16 +56321,16 @@ var FileWatcher = class {
|
|
|
56321
56321
|
}
|
|
56322
56322
|
}
|
|
56323
56323
|
this.watcher = watch([join5(tasksDir, "*.md"), configPath], {
|
|
56324
|
-
persistent: true,
|
|
56325
56324
|
ignoreInitial: true,
|
|
56326
|
-
awaitWriteFinish: { stabilityThreshold:
|
|
56325
|
+
awaitWriteFinish: { stabilityThreshold: 25, pollInterval: 25 },
|
|
56326
|
+
alwaysStat: true
|
|
56327
56327
|
});
|
|
56328
56328
|
this.watcher.on("all", (event, path) => {
|
|
56329
56329
|
this.handleFsEvent(event, path, kandownDir);
|
|
56330
56330
|
});
|
|
56331
56331
|
this.pollInterval = setInterval(() => {
|
|
56332
56332
|
this.pollHashes(kandownDir);
|
|
56333
|
-
},
|
|
56333
|
+
}, 300);
|
|
56334
56334
|
}
|
|
56335
56335
|
/**
|
|
56336
56336
|
* 📖 Stop watching and clean up all resources.
|