kandev 0.45.0 → 0.49.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 +33 -1
- package/dist/ports.js +37 -3
- package/dist/web.js +20 -1
- package/package.json +7 -7
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
|
|
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
|
|
45
|
-
*
|
|
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
|
-
|
|
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
|
|
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.
|
|
3
|
+
"version": "0.49.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.
|
|
26
|
-
"@kdlbs/runtime-linux-arm64": "0.
|
|
27
|
-
"@kdlbs/runtime-darwin-x64": "0.
|
|
28
|
-
"@kdlbs/runtime-darwin-arm64": "0.
|
|
29
|
-
"@kdlbs/runtime-win32-x64": "0.
|
|
25
|
+
"@kdlbs/runtime-linux-x64": "0.49.0",
|
|
26
|
+
"@kdlbs/runtime-linux-arm64": "0.49.0",
|
|
27
|
+
"@kdlbs/runtime-darwin-x64": "0.49.0",
|
|
28
|
+
"@kdlbs/runtime-darwin-arm64": "0.49.0",
|
|
29
|
+
"@kdlbs/runtime-win32-x64": "0.49.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": "
|
|
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",
|