kandev 0.45.0 → 0.48.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/dist/dev.js CHANGED
@@ -6,6 +6,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
6
6
  exports.runDev = runDev;
7
7
  exports.resolveDevBackendEnv = resolveDevBackendEnv;
8
8
  const node_child_process_1 = require("node:child_process");
9
+ const node_fs_1 = __importDefault(require("node:fs"));
9
10
  const node_path_1 = __importDefault(require("node:path"));
10
11
  const constants_1 = require("./constants");
11
12
  const health_1 = require("./health");
@@ -28,7 +29,12 @@ async function runDev({ repoRoot, backendPort, webPort }) {
28
29
  });
29
30
  const supervisor = (0, process_1.createProcessSupervisor)();
30
31
  supervisor.attachSignalHandlers();
31
- const backendProc = (0, node_child_process_1.spawn)("make", ["-C", node_path_1.default.join("apps", "backend"), "dev"], {
32
+ const { cmd: backendCmd, args: backendArgs } = withWinjobWrap(repoRoot, "make", [
33
+ "-C",
34
+ node_path_1.default.join("apps", "backend"),
35
+ "dev",
36
+ ]);
37
+ const backendProc = (0, node_child_process_1.spawn)(backendCmd, backendArgs, {
32
38
  cwd: repoRoot,
33
39
  env: backendEnv,
34
40
  stdio: "inherit",
@@ -97,3 +103,29 @@ function resolveDevBackendEnv(repoRoot) {
97
103
  },
98
104
  };
99
105
  }
106
+ // withWinjobWrap on Windows prepends apps/backend/bin/winjob.exe to a spawn
107
+ // command so the child runs inside a Job Object configured with
108
+ // JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE. That makes "kill the whole backend
109
+ // subtree" a kernel-level guarantee tied to winjob's process exit, instead of
110
+ // relying on the bash → make → pnpm → node → make → sh → kandev signal chain
111
+ // (which drops Ctrl-C at multiple links because MSYS bash, native Win32
112
+ // processes, and Node disagree on signal propagation semantics).
113
+ //
114
+ // On Unix this is a passthrough — POSIX process groups already give us
115
+ // reliable cascading termination.
116
+ //
117
+ // If the winjob binary isn't built yet (the user ran `make dev` before
118
+ // `make -C apps/backend build-winjob`), we fall back to a direct spawn and
119
+ // emit a one-line note. The supervisor's tree-kill still handles the happy
120
+ // path; users only notice the gap if Ctrl-C drops mid-chain.
121
+ function withWinjobWrap(repoRoot, cmd, args) {
122
+ if (process.platform !== "win32")
123
+ return { cmd, args };
124
+ const winjob = node_path_1.default.join(repoRoot, "apps", "backend", "bin", "winjob.exe");
125
+ if (!node_fs_1.default.existsSync(winjob)) {
126
+ console.warn(`[kandev] winjob.exe not built — Ctrl-C may leak processes on Windows. ` +
127
+ `Run \`make -C apps/backend build-winjob\` once to enable.`);
128
+ return { cmd, args };
129
+ }
130
+ return { cmd: winjob, args: [cmd, ...args] };
131
+ }
package/dist/ports.js CHANGED
@@ -38,18 +38,52 @@ function isPortInUse(port, host) {
38
38
  });
39
39
  });
40
40
  }
41
+ /**
42
+ * Tries to bind a port on the given host. Returns true if the bind succeeds
43
+ * (port is free). Closes immediately on success.
44
+ *
45
+ * This catches Windows "phantom" port reservations: Hyper-V/WSL silently
46
+ * reserve random port ranges at boot that don't appear in netstat or via a
47
+ * connect probe (nothing is listening, so connect-check thinks the port is
48
+ * free) — but bind fails with "Only one usage of each socket address". A
49
+ * connect-only check causes kandev's backend to choose a reserved port and
50
+ * then die when it tries to actually listen on it.
51
+ */
52
+ function canBindPort(port, host) {
53
+ return new Promise((resolve) => {
54
+ const server = node_net_1.default.createServer();
55
+ server.once("error", () => resolve(false));
56
+ server.listen(port, host, () => {
57
+ server.close(() => resolve(true));
58
+ });
59
+ });
60
+ }
41
61
  /**
42
62
  * Checks if a port is available by probing both IPv4 and IPv6 loopback.
43
63
  *
44
- * Uses a connect-based check: if we can connect to the port on either
45
- * 127.0.0.1 or ::1, something is already listening and the port is taken.
64
+ * Uses BOTH a connect check and a bind check:
65
+ * - connect: detects ports where a listener is bound with SO_REUSEADDR
66
+ * (Node's default on macOS — bind-only check would falsely succeed)
67
+ * - bind: detects Windows phantom reservations (Hyper-V/WSL) and
68
+ * ports in TIME_WAIT that connect-only check misses
69
+ *
70
+ * The port is available IFF nobody answers a connect AND a fresh bind
71
+ * succeeds — covers macOS, Linux, and Windows.
46
72
  */
47
73
  async function isPortAvailable(port) {
74
+ // Run connect probes first, then the bind probe — they cannot share the
75
+ // port concurrently. On loopback, server.listen() completes in the kernel
76
+ // before a connect SYN to the same address is processed, so a concurrent
77
+ // canBindPort+isPortInUse pair can answer each other and report a free
78
+ // port as occupied. Sequencing keeps the bind probe's temporary listener
79
+ // out of the connect probes' view.
48
80
  const [v4InUse, v6InUse] = await Promise.all([
49
81
  isPortInUse(port, "127.0.0.1"),
50
82
  isPortInUse(port, "::1"),
51
83
  ]);
52
- return !v4InUse && !v6InUse;
84
+ if (v4InUse || v6InUse)
85
+ return false;
86
+ return canBindPort(port, "127.0.0.1");
53
87
  }
54
88
  async function reserveSpecificPort(port, host = "127.0.0.1") {
55
89
  return new Promise((resolve) => {
package/dist/web.js CHANGED
@@ -4,6 +4,24 @@ exports.openBrowser = openBrowser;
4
4
  exports.launchWebApp = launchWebApp;
5
5
  const node_child_process_1 = require("node:child_process");
6
6
  const node_fs_1 = require("node:fs");
7
+ // Node CLIs that install as .cmd shims on Windows. Anything in this set goes
8
+ // through cmd.exe /c on win32 so we can spawn the shim safely:
9
+ // - Node's spawn doesn't apply PATHEXT, so spawn("pnpm", …) hits ENOENT.
10
+ // - spawn("pnpm.cmd", …) directly returns EINVAL on Node 21+ (the
11
+ // CVE-2024-27980 fix forbids direct .bat/.cmd spawning).
12
+ // - spawn(…, {shell:true}) works but trips DEP0190 (the args-aren't-escaped
13
+ // security warning) and forks an extra cmd.exe per spawn.
14
+ // Going through cmd.exe explicitly is the cleanest option: no warning, no
15
+ // EINVAL, and the args (-C, --filter, identifier-like strings) contain no
16
+ // cmd metacharacters that need extra escaping.
17
+ const WIN_SHIM_COMMANDS = new Set(["pnpm", "npm", "npx", "yarn"]);
18
+ function resolveWindowsShim(command, args) {
19
+ if (process.platform !== "win32")
20
+ return { command, args };
21
+ if (!WIN_SHIM_COMMANDS.has(command))
22
+ return { command, args };
23
+ return { command: "cmd.exe", args: ["/c", command, ...args] };
24
+ }
7
25
  let _isWSL;
8
26
  function isWSL() {
9
27
  if (_isWSL === undefined) {
@@ -34,7 +52,8 @@ function openBrowser(url) {
34
52
  }
35
53
  function launchWebApp({ command, args, cwd, env, supervisor, label, quiet = false, }) {
36
54
  const stdio = quiet ? ["ignore", "pipe", "pipe"] : "inherit";
37
- const proc = (0, node_child_process_1.spawn)(command, args, { cwd, env, stdio });
55
+ const resolved = resolveWindowsShim(command, args);
56
+ const proc = (0, node_child_process_1.spawn)(resolved.command, resolved.args, { cwd, env, stdio });
38
57
  supervisor.children.push(proc);
39
58
  // In quiet mode, only forward stderr
40
59
  if (quiet && proc.stderr) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "kandev",
3
- "version": "0.45.0",
3
+ "version": "0.48.0",
4
4
  "private": false,
5
5
  "description": "Launcher for Kandev — manage tasks, orchestrate agents, review changes, and ship value",
6
6
  "license": "AGPL-3.0-only",
@@ -22,11 +22,11 @@
22
22
  "npm": ">=7"
23
23
  },
24
24
  "optionalDependencies": {
25
- "@kdlbs/runtime-linux-x64": "0.45.0",
26
- "@kdlbs/runtime-linux-arm64": "0.45.0",
27
- "@kdlbs/runtime-darwin-x64": "0.45.0",
28
- "@kdlbs/runtime-darwin-arm64": "0.45.0",
29
- "@kdlbs/runtime-win32-x64": "0.45.0"
25
+ "@kdlbs/runtime-linux-x64": "0.48.0",
26
+ "@kdlbs/runtime-linux-arm64": "0.48.0",
27
+ "@kdlbs/runtime-darwin-x64": "0.48.0",
28
+ "@kdlbs/runtime-darwin-arm64": "0.48.0",
29
+ "@kdlbs/runtime-win32-x64": "0.48.0"
30
30
  },
31
31
  "dependencies": {
32
32
  "tar": "^7.5.11",
@@ -40,7 +40,7 @@
40
40
  "vitest": "^1.6.0"
41
41
  },
42
42
  "scripts": {
43
- "dev": "unset npm_config_prefix && tsx src/cli.ts",
43
+ "dev": "tsx src/cli.ts",
44
44
  "build": "tsc -p tsconfig.json",
45
45
  "bundle": "esbuild dist/cli.js --bundle --platform=node --format=cjs --outfile=dist/cli.bundle.js",
46
46
  "start": "node dist/cli.js",