kandev 0.65.0 → 0.67.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/README.md +2 -10
- package/bin/cli.js +3 -1
- package/bin/native-shim.js +94 -0
- package/package.json +14 -16
- package/dist/args.js +0 -123
- package/dist/backup.js +0 -98
- package/dist/bundle.js +0 -80
- package/dist/cli.js +0 -155
- package/dist/constants.js +0 -54
- package/dist/dev.js +0 -157
- package/dist/github.js +0 -221
- package/dist/health.js +0 -71
- package/dist/kandev-env.js +0 -28
- package/dist/platform.js +0 -53
- package/dist/ports.js +0 -142
- package/dist/process.js +0 -122
- package/dist/run.js +0 -283
- package/dist/runtime.js +0 -76
- package/dist/service/args.js +0 -131
- package/dist/service/config.js +0 -111
- package/dist/service/health_check.js +0 -84
- package/dist/service/index.js +0 -70
- package/dist/service/install_helpers.js +0 -51
- package/dist/service/launchctl.js +0 -73
- package/dist/service/linux.js +0 -165
- package/dist/service/macos.js +0 -221
- package/dist/service/metadata.js +0 -46
- package/dist/service/paths.js +0 -168
- package/dist/service/self_update.js +0 -223
- package/dist/service/stale_check.js +0 -78
- package/dist/service/templates.js +0 -229
- package/dist/shared.js +0 -214
- package/dist/start.js +0 -188
- package/dist/supervisor/backend.js +0 -83
- package/dist/supervisor/child.js +0 -64
- package/dist/supervisor/control.js +0 -97
- package/dist/supervisor/manifest.js +0 -74
- package/dist/supervisor/paths.js +0 -33
- package/dist/version.js +0 -57
- package/dist/web.js +0 -68
package/dist/ports.js
DELETED
|
@@ -1,142 +0,0 @@
|
|
|
1
|
-
"use strict";
|
|
2
|
-
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
-
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
-
};
|
|
5
|
-
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
-
exports.__testing = void 0;
|
|
7
|
-
exports.ensureValidPort = ensureValidPort;
|
|
8
|
-
exports.pickAvailablePort = pickAvailablePort;
|
|
9
|
-
exports.pickAndReservePort = pickAndReservePort;
|
|
10
|
-
const node_crypto_1 = __importDefault(require("node:crypto"));
|
|
11
|
-
const node_net_1 = __importDefault(require("node:net"));
|
|
12
|
-
const constants_1 = require("./constants");
|
|
13
|
-
const CONNECT_PROBE_TIMEOUT_MS = 500;
|
|
14
|
-
function ensureValidPort(port, name) {
|
|
15
|
-
if (port === undefined) {
|
|
16
|
-
return undefined;
|
|
17
|
-
}
|
|
18
|
-
if (!Number.isInteger(port) || port <= 0 || port > 65535) {
|
|
19
|
-
throw new Error(`${name} must be an integer between 1 and 65535`);
|
|
20
|
-
}
|
|
21
|
-
return port;
|
|
22
|
-
}
|
|
23
|
-
/**
|
|
24
|
-
* Tries to connect to a port on the given host. Returns true if something
|
|
25
|
-
* is already listening (i.e. the port is in use). Returns false on connection
|
|
26
|
-
* refused, on any other socket error, or when the probe takes longer than
|
|
27
|
-
* `timeoutMs` — under WSL2 mirrored networking the kernel can silently drop
|
|
28
|
-
* the SYN to an unbound loopback port, so an unbounded connect would hang
|
|
29
|
-
* forever and deadlock isPortAvailable.
|
|
30
|
-
*
|
|
31
|
-
* Used alongside canBindPort because it detects ports where a listener is
|
|
32
|
-
* bound with SO_REUSEADDR (Node's default on macOS), which a bind-only check
|
|
33
|
-
* would miss.
|
|
34
|
-
*/
|
|
35
|
-
function isPortInUse(port, host, timeoutMs = CONNECT_PROBE_TIMEOUT_MS) {
|
|
36
|
-
return new Promise((resolve) => {
|
|
37
|
-
const socket = node_net_1.default.createConnection({ port, host });
|
|
38
|
-
let settled = false;
|
|
39
|
-
const done = (v) => {
|
|
40
|
-
if (settled)
|
|
41
|
-
return;
|
|
42
|
-
settled = true;
|
|
43
|
-
socket.destroy();
|
|
44
|
-
resolve(v);
|
|
45
|
-
};
|
|
46
|
-
socket.once("connect", () => done(true));
|
|
47
|
-
socket.once("error", () => done(false));
|
|
48
|
-
setTimeout(() => done(false), timeoutMs).unref();
|
|
49
|
-
});
|
|
50
|
-
}
|
|
51
|
-
/**
|
|
52
|
-
* Tries to bind a port on the given host. Returns true if the bind succeeds
|
|
53
|
-
* (port is free). Closes immediately on success.
|
|
54
|
-
*
|
|
55
|
-
* This catches Windows "phantom" port reservations: Hyper-V/WSL silently
|
|
56
|
-
* reserve random port ranges at boot that don't appear in netstat or via a
|
|
57
|
-
* connect probe (nothing is listening, so connect-check thinks the port is
|
|
58
|
-
* free) — but bind fails with "Only one usage of each socket address". A
|
|
59
|
-
* connect-only check causes kandev's backend to choose a reserved port and
|
|
60
|
-
* then die when it tries to actually listen on it.
|
|
61
|
-
*/
|
|
62
|
-
function canBindPort(port, host) {
|
|
63
|
-
return new Promise((resolve) => {
|
|
64
|
-
const server = node_net_1.default.createServer();
|
|
65
|
-
server.once("error", () => resolve(false));
|
|
66
|
-
server.listen(port, host, () => {
|
|
67
|
-
server.close(() => resolve(true));
|
|
68
|
-
});
|
|
69
|
-
});
|
|
70
|
-
}
|
|
71
|
-
/**
|
|
72
|
-
* Checks if a port is available by probing both IPv4 and IPv6 loopback.
|
|
73
|
-
*
|
|
74
|
-
* Uses BOTH a connect check and a bind check:
|
|
75
|
-
* - connect: detects ports where a listener is bound with SO_REUSEADDR
|
|
76
|
-
* (Node's default on macOS — bind-only check would falsely succeed)
|
|
77
|
-
* - bind: detects Windows phantom reservations (Hyper-V/WSL) and
|
|
78
|
-
* ports in TIME_WAIT that connect-only check misses
|
|
79
|
-
*
|
|
80
|
-
* The port is available IFF nobody answers a connect AND a fresh bind
|
|
81
|
-
* succeeds — covers macOS, Linux, and Windows.
|
|
82
|
-
*/
|
|
83
|
-
async function isPortAvailable(port) {
|
|
84
|
-
// Run connect probes first, then the bind probe — they cannot share the
|
|
85
|
-
// port concurrently. On loopback, server.listen() completes in the kernel
|
|
86
|
-
// before a connect SYN to the same address is processed, so a concurrent
|
|
87
|
-
// canBindPort+isPortInUse pair can answer each other and report a free
|
|
88
|
-
// port as occupied. Sequencing keeps the bind probe's temporary listener
|
|
89
|
-
// out of the connect probes' view.
|
|
90
|
-
const [v4InUse, v6InUse] = await Promise.all([
|
|
91
|
-
isPortInUse(port, "127.0.0.1"),
|
|
92
|
-
isPortInUse(port, "::1"),
|
|
93
|
-
]);
|
|
94
|
-
if (v4InUse || v6InUse)
|
|
95
|
-
return false;
|
|
96
|
-
return canBindPort(port, "127.0.0.1");
|
|
97
|
-
}
|
|
98
|
-
async function reserveSpecificPort(port, host = "127.0.0.1") {
|
|
99
|
-
return new Promise((resolve) => {
|
|
100
|
-
const server = node_net_1.default.createServer();
|
|
101
|
-
server.on("error", () => resolve(null));
|
|
102
|
-
server.listen(port, host, () => resolve(server));
|
|
103
|
-
});
|
|
104
|
-
}
|
|
105
|
-
async function pickAvailablePort(preferred, retries = constants_1.RANDOM_PORT_RETRIES) {
|
|
106
|
-
if (await isPortAvailable(preferred)) {
|
|
107
|
-
return preferred;
|
|
108
|
-
}
|
|
109
|
-
for (let i = 0; i < retries; i += 1) {
|
|
110
|
-
const candidate = node_crypto_1.default.randomInt(constants_1.RANDOM_PORT_MIN, constants_1.RANDOM_PORT_MAX + 1);
|
|
111
|
-
if (await isPortAvailable(candidate)) {
|
|
112
|
-
return candidate;
|
|
113
|
-
}
|
|
114
|
-
}
|
|
115
|
-
throw new Error(`Unable to find a free port after ${retries + 1} attempts`);
|
|
116
|
-
}
|
|
117
|
-
async function pickAndReservePort(preferred, retries = constants_1.RANDOM_PORT_RETRIES) {
|
|
118
|
-
if (await isPortAvailable(preferred)) {
|
|
119
|
-
const reservedPreferred = await reserveSpecificPort(preferred);
|
|
120
|
-
if (reservedPreferred) {
|
|
121
|
-
return {
|
|
122
|
-
port: preferred,
|
|
123
|
-
release: () => new Promise((resolve) => reservedPreferred.close(() => resolve())),
|
|
124
|
-
};
|
|
125
|
-
}
|
|
126
|
-
}
|
|
127
|
-
for (let i = 0; i < retries; i += 1) {
|
|
128
|
-
const candidate = node_crypto_1.default.randomInt(constants_1.RANDOM_PORT_MIN, constants_1.RANDOM_PORT_MAX + 1);
|
|
129
|
-
if (!(await isPortAvailable(candidate)))
|
|
130
|
-
continue;
|
|
131
|
-
const reserved = await reserveSpecificPort(candidate);
|
|
132
|
-
if (reserved) {
|
|
133
|
-
return {
|
|
134
|
-
port: candidate,
|
|
135
|
-
release: () => new Promise((resolve) => reserved.close(() => resolve())),
|
|
136
|
-
};
|
|
137
|
-
}
|
|
138
|
-
}
|
|
139
|
-
throw new Error(`Unable to reserve a free port after ${retries + 1} attempts`);
|
|
140
|
-
}
|
|
141
|
-
// Exported for tests only.
|
|
142
|
-
exports.__testing = { isPortInUse, isPortAvailable };
|
package/dist/process.js
DELETED
|
@@ -1,122 +0,0 @@
|
|
|
1
|
-
"use strict";
|
|
2
|
-
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
-
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
-
};
|
|
5
|
-
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
-
exports.createProcessSupervisor = createProcessSupervisor;
|
|
7
|
-
exports.ignoreBrokenPipe = ignoreBrokenPipe;
|
|
8
|
-
exports.__resetBrokenPipeGuard = __resetBrokenPipeGuard;
|
|
9
|
-
const tree_kill_1 = __importDefault(require("tree-kill"));
|
|
10
|
-
const SHUTDOWN_TIMEOUT_MS = 10000;
|
|
11
|
-
function createProcessSupervisor() {
|
|
12
|
-
let shutdownPromise = null;
|
|
13
|
-
const children = [];
|
|
14
|
-
const shutdown = async (reason) => {
|
|
15
|
-
// If already shutting down, wait for the existing shutdown to complete
|
|
16
|
-
if (shutdownPromise) {
|
|
17
|
-
return shutdownPromise;
|
|
18
|
-
}
|
|
19
|
-
console.log(`[kandev] shutting down (${reason})...`);
|
|
20
|
-
// Wait for all child processes to actually exit, not just for signal to be sent
|
|
21
|
-
shutdownPromise = Promise.all(children
|
|
22
|
-
.filter((child) => child.pid)
|
|
23
|
-
.map((child) => waitForProcessExit(child, SHUTDOWN_TIMEOUT_MS))).then(() => { });
|
|
24
|
-
return shutdownPromise;
|
|
25
|
-
};
|
|
26
|
-
const onSignal = (signal) => {
|
|
27
|
-
void shutdown(`signal ${signal}`).then(() => process.exit(0));
|
|
28
|
-
};
|
|
29
|
-
const attachSignalHandlers = () => {
|
|
30
|
-
// Ctrl-C sends SIGINT to the whole process group, so the parent (make/shell)
|
|
31
|
-
// exits and closes the pipe our stdout/stderr write to. Any console.log during
|
|
32
|
-
// shutdown then triggers an EPIPE 'error' event with no listener, which Node
|
|
33
|
-
// treats as fatal and crashes us before children are gracefully terminated.
|
|
34
|
-
// Swallow EPIPE so shutdown can finish.
|
|
35
|
-
ignoreBrokenPipe();
|
|
36
|
-
process.on("SIGINT", onSignal);
|
|
37
|
-
// SIGTERM is not available on Windows — only attach where supported
|
|
38
|
-
if (process.platform !== "win32") {
|
|
39
|
-
process.on("SIGTERM", onSignal);
|
|
40
|
-
}
|
|
41
|
-
};
|
|
42
|
-
return { children, shutdown, attachSignalHandlers };
|
|
43
|
-
}
|
|
44
|
-
let brokenPipeGuarded = false;
|
|
45
|
-
/**
|
|
46
|
-
* Install error handlers on stdout/stderr that swallow EPIPE (broken pipe).
|
|
47
|
-
* Idempotent: only attaches once per process.
|
|
48
|
-
*/
|
|
49
|
-
function ignoreBrokenPipe() {
|
|
50
|
-
if (brokenPipeGuarded)
|
|
51
|
-
return;
|
|
52
|
-
brokenPipeGuarded = true;
|
|
53
|
-
const onPipeError = (err) => {
|
|
54
|
-
if (err.code === "EPIPE")
|
|
55
|
-
return;
|
|
56
|
-
throw err;
|
|
57
|
-
};
|
|
58
|
-
process.stdout.on("error", onPipeError);
|
|
59
|
-
process.stderr.on("error", onPipeError);
|
|
60
|
-
}
|
|
61
|
-
/**
|
|
62
|
-
* Test-only: resets the broken-pipe guard so a later `ignoreBrokenPipe()` call
|
|
63
|
-
* reattaches listeners. Keeps the module guard consistent with suites that
|
|
64
|
-
* remove the listeners they installed.
|
|
65
|
-
*/
|
|
66
|
-
function __resetBrokenPipeGuard() {
|
|
67
|
-
brokenPipeGuarded = false;
|
|
68
|
-
}
|
|
69
|
-
/**
|
|
70
|
-
* Terminate a process and wait for it to exit.
|
|
71
|
-
* On Unix: sends SIGTERM, falls back to SIGKILL after timeout.
|
|
72
|
-
* On Windows: tree-kill uses taskkill (no SIGTERM/SIGKILL distinction).
|
|
73
|
-
*/
|
|
74
|
-
function waitForProcessExit(child, timeoutMs) {
|
|
75
|
-
const isWindows = process.platform === "win32";
|
|
76
|
-
return new Promise((resolve) => {
|
|
77
|
-
const pid = child.pid;
|
|
78
|
-
// Check if this is a ChildProcess with exit event support
|
|
79
|
-
const proc = child;
|
|
80
|
-
const hasExitEvent = typeof proc.on === "function" && typeof proc.exitCode !== "undefined";
|
|
81
|
-
// If process already exited, resolve immediately
|
|
82
|
-
if (hasExitEvent && proc.exitCode !== null) {
|
|
83
|
-
resolve();
|
|
84
|
-
return;
|
|
85
|
-
}
|
|
86
|
-
let resolved = false;
|
|
87
|
-
const done = () => {
|
|
88
|
-
if (resolved)
|
|
89
|
-
return;
|
|
90
|
-
resolved = true;
|
|
91
|
-
resolve();
|
|
92
|
-
};
|
|
93
|
-
// Set up timeout for force-kill fallback
|
|
94
|
-
const timeout = setTimeout(() => {
|
|
95
|
-
console.log(`[kandev] process ${pid} did not exit in time, force killing`);
|
|
96
|
-
// On Windows, tree-kill always force-kills (no signal distinction)
|
|
97
|
-
// On Unix, escalate to SIGKILL
|
|
98
|
-
(0, tree_kill_1.default)(pid, isWindows ? undefined : "SIGKILL", done);
|
|
99
|
-
}, timeoutMs);
|
|
100
|
-
// Listen for exit event if available
|
|
101
|
-
if (hasExitEvent) {
|
|
102
|
-
proc.once("exit", () => {
|
|
103
|
-
clearTimeout(timeout);
|
|
104
|
-
done();
|
|
105
|
-
});
|
|
106
|
-
}
|
|
107
|
-
// Graceful termination: SIGTERM on Unix, default kill on Windows
|
|
108
|
-
(0, tree_kill_1.default)(pid, isWindows ? undefined : "SIGTERM", (err) => {
|
|
109
|
-
// If kill fails (process already gone), we're done
|
|
110
|
-
if (err) {
|
|
111
|
-
clearTimeout(timeout);
|
|
112
|
-
done();
|
|
113
|
-
}
|
|
114
|
-
// If no exit event support, resolve after a brief delay
|
|
115
|
-
// (tree-kill callback fires when signal sent, not when process exits)
|
|
116
|
-
if (!hasExitEvent) {
|
|
117
|
-
// For non-ChildProcess objects, we can't wait for exit event
|
|
118
|
-
// Just wait for the timeout
|
|
119
|
-
}
|
|
120
|
-
});
|
|
121
|
-
});
|
|
122
|
-
}
|
package/dist/run.js
DELETED
|
@@ -1,283 +0,0 @@
|
|
|
1
|
-
"use strict";
|
|
2
|
-
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
-
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
-
};
|
|
5
|
-
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
-
exports.findCachedRelease = findCachedRelease;
|
|
7
|
-
exports.cleanOldReleases = cleanOldReleases;
|
|
8
|
-
exports.attachRingBuffer = attachRingBuffer;
|
|
9
|
-
exports.runRelease = runRelease;
|
|
10
|
-
const node_fs_1 = __importDefault(require("node:fs"));
|
|
11
|
-
const node_path_1 = __importDefault(require("node:path"));
|
|
12
|
-
const bundle_1 = require("./bundle");
|
|
13
|
-
const constants_1 = require("./constants");
|
|
14
|
-
const github_1 = require("./github");
|
|
15
|
-
const health_1 = require("./health");
|
|
16
|
-
const platform_1 = require("./platform");
|
|
17
|
-
const version_1 = require("./version");
|
|
18
|
-
const ports_1 = require("./ports");
|
|
19
|
-
const process_1 = require("./process");
|
|
20
|
-
const runtime_1 = require("./runtime");
|
|
21
|
-
const shared_1 = require("./shared");
|
|
22
|
-
const backend_1 = require("./supervisor/backend");
|
|
23
|
-
const web_1 = require("./web");
|
|
24
|
-
/**
|
|
25
|
-
* Find a cached release binary to use when GitHub is unreachable.
|
|
26
|
-
* If version is specified, checks that exact tag. Otherwise, picks
|
|
27
|
-
* the highest semver tag available in the cache.
|
|
28
|
-
*/
|
|
29
|
-
function findCachedRelease(platformDir, version) {
|
|
30
|
-
const rootCacheDir = (0, constants_1.resolveCacheDir)();
|
|
31
|
-
if (version) {
|
|
32
|
-
const cacheDir = node_path_1.default.join(rootCacheDir, version, platformDir);
|
|
33
|
-
const bundleDir = node_path_1.default.join(cacheDir, "kandev");
|
|
34
|
-
const backendBin = node_path_1.default.join(bundleDir, "bin", (0, platform_1.getBinaryName)("kandev"));
|
|
35
|
-
if (node_fs_1.default.existsSync(backendBin)) {
|
|
36
|
-
return { cacheDir, tag: version };
|
|
37
|
-
}
|
|
38
|
-
return null;
|
|
39
|
-
}
|
|
40
|
-
// No version specified — scan for cached tags and pick the latest.
|
|
41
|
-
if (!node_fs_1.default.existsSync(rootCacheDir))
|
|
42
|
-
return null;
|
|
43
|
-
const entries = node_fs_1.default.readdirSync(rootCacheDir).filter((d) => d.startsWith("v"));
|
|
44
|
-
if (entries.length === 0)
|
|
45
|
-
return null;
|
|
46
|
-
const sorted = (0, version_1.sortVersionsDesc)(entries);
|
|
47
|
-
for (const tag of sorted) {
|
|
48
|
-
const cacheDir = node_path_1.default.join(rootCacheDir, tag, platformDir);
|
|
49
|
-
const bundleDir = node_path_1.default.join(cacheDir, "kandev");
|
|
50
|
-
const backendBin = node_path_1.default.join(bundleDir, "bin", (0, platform_1.getBinaryName)("kandev"));
|
|
51
|
-
if (node_fs_1.default.existsSync(backendBin)) {
|
|
52
|
-
return { cacheDir, tag };
|
|
53
|
-
}
|
|
54
|
-
}
|
|
55
|
-
return null;
|
|
56
|
-
}
|
|
57
|
-
/**
|
|
58
|
-
* Remove old cached releases, keeping only the 2 most recent tags.
|
|
59
|
-
* Runs after a successful download so we don't accumulate stale versions.
|
|
60
|
-
* The previous version is kept as a fallback for offline use.
|
|
61
|
-
*/
|
|
62
|
-
function cleanOldReleases(currentTag) {
|
|
63
|
-
const rootCacheDir = (0, constants_1.resolveCacheDir)();
|
|
64
|
-
try {
|
|
65
|
-
if (!node_fs_1.default.existsSync(rootCacheDir))
|
|
66
|
-
return;
|
|
67
|
-
const entries = node_fs_1.default.readdirSync(rootCacheDir).filter((d) => d.startsWith("v"));
|
|
68
|
-
if (entries.length <= 2)
|
|
69
|
-
return;
|
|
70
|
-
const sorted = (0, version_1.sortVersionsDesc)(entries);
|
|
71
|
-
// Always keep currentTag + the next most recent.
|
|
72
|
-
const keep = new Set([currentTag, sorted[0], sorted[1]]);
|
|
73
|
-
for (const entry of entries) {
|
|
74
|
-
if (!keep.has(entry)) {
|
|
75
|
-
node_fs_1.default.rmSync(node_path_1.default.join(rootCacheDir, entry), { recursive: true, force: true });
|
|
76
|
-
}
|
|
77
|
-
}
|
|
78
|
-
}
|
|
79
|
-
catch {
|
|
80
|
-
// Non-critical — don't fail the launch if cleanup errors.
|
|
81
|
-
}
|
|
82
|
-
}
|
|
83
|
-
/**
|
|
84
|
-
* Download a specific release version into the local cache.
|
|
85
|
-
* Only used when --runtime-version is given explicitly.
|
|
86
|
-
*/
|
|
87
|
-
async function downloadRuntimeVersion(runtimeVersion) {
|
|
88
|
-
const platformDir = (0, platform_1.getPlatformDir)();
|
|
89
|
-
const release = await (0, github_1.getRelease)(runtimeVersion);
|
|
90
|
-
const tag = release.tag_name;
|
|
91
|
-
const assetName = `kandev-${platformDir}.tar.gz`;
|
|
92
|
-
const cacheDir = node_path_1.default.join((0, constants_1.resolveCacheDir)(), tag, platformDir);
|
|
93
|
-
const archivePath = await (0, github_1.ensureAsset)(tag, assetName, cacheDir, (downloaded, total) => {
|
|
94
|
-
const percent = total ? Math.round((downloaded / total) * 100) : 0;
|
|
95
|
-
const mb = (downloaded / (1024 * 1024)).toFixed(1);
|
|
96
|
-
const totalMb = total ? (total / (1024 * 1024)).toFixed(1) : "?";
|
|
97
|
-
process.stderr.write(`\r Downloading: ${mb}MB / ${totalMb}MB (${percent}%)`);
|
|
98
|
-
});
|
|
99
|
-
process.stderr.write("\n");
|
|
100
|
-
(0, bundle_1.ensureExtracted)(archivePath, cacheDir);
|
|
101
|
-
cleanOldReleases(tag);
|
|
102
|
-
return tag;
|
|
103
|
-
}
|
|
104
|
-
async function prepareBundleForLaunch({ runtimeVersion, backendPort, webPort, verbose = false, debug = false, }) {
|
|
105
|
-
let bundleDir;
|
|
106
|
-
let releaseTag;
|
|
107
|
-
if (runtimeVersion) {
|
|
108
|
-
// Explicit version: ensure it is in the cache (downloading if needed), then resolve.
|
|
109
|
-
const platformDir = (0, platform_1.getPlatformDir)();
|
|
110
|
-
const cached = findCachedRelease(platformDir, runtimeVersion);
|
|
111
|
-
let tag;
|
|
112
|
-
if (cached) {
|
|
113
|
-
tag = cached.tag;
|
|
114
|
-
bundleDir = (0, bundle_1.findBundleRoot)(cached.cacheDir);
|
|
115
|
-
}
|
|
116
|
-
else {
|
|
117
|
-
try {
|
|
118
|
-
tag = await downloadRuntimeVersion(runtimeVersion);
|
|
119
|
-
const cacheDir = node_path_1.default.join((0, constants_1.resolveCacheDir)(), tag, platformDir);
|
|
120
|
-
bundleDir = (0, bundle_1.findBundleRoot)(cacheDir);
|
|
121
|
-
}
|
|
122
|
-
catch (err) {
|
|
123
|
-
const reason = err instanceof Error ? err.message : String(err);
|
|
124
|
-
throw new Error(`Failed to fetch runtime version ${runtimeVersion}.\n` +
|
|
125
|
-
` Reason: ${reason}\n` +
|
|
126
|
-
` Run kandev once while online to cache a release for offline use.`);
|
|
127
|
-
}
|
|
128
|
-
}
|
|
129
|
-
// Validate the resolved bundle has all required binaries before launching.
|
|
130
|
-
(0, runtime_1.validateBundle)(bundleDir);
|
|
131
|
-
releaseTag = tag;
|
|
132
|
-
}
|
|
133
|
-
else {
|
|
134
|
-
// Default path: resolve from KANDEV_BUNDLE_DIR or installed npm runtime package.
|
|
135
|
-
const resolved = (0, runtime_1.resolveRuntime)();
|
|
136
|
-
bundleDir = resolved.bundleDir;
|
|
137
|
-
// Use KANDEV_VERSION if set (e.g. by Homebrew wrapper), otherwise show source.
|
|
138
|
-
releaseTag = process.env.KANDEV_VERSION ?? `(${resolved.source})`;
|
|
139
|
-
}
|
|
140
|
-
const actualBackendPort = backendPort ?? (await (0, ports_1.pickAvailablePort)(constants_1.DEFAULT_BACKEND_PORT));
|
|
141
|
-
const actualWebPort = webPort ?? (await (0, ports_1.pickAvailablePort)(constants_1.DEFAULT_WEB_PORT));
|
|
142
|
-
const agentctlPort = await (0, ports_1.pickAvailablePort)(constants_1.DEFAULT_AGENTCTL_PORT);
|
|
143
|
-
const backendUrl = `http://localhost:${actualBackendPort}`;
|
|
144
|
-
const showOutput = verbose || debug;
|
|
145
|
-
const logLevel = process.env.KANDEV_LOG_LEVEL?.trim() || (debug ? "debug" : verbose ? "info" : "warn");
|
|
146
|
-
const dataDir = (0, constants_1.resolveDataDir)();
|
|
147
|
-
node_fs_1.default.mkdirSync(dataDir, { recursive: true });
|
|
148
|
-
const dbPath = (0, constants_1.resolveDatabasePath)();
|
|
149
|
-
const backendBin = node_path_1.default.join(bundleDir, "bin", (0, platform_1.getBinaryName)("kandev"));
|
|
150
|
-
const backendEnv = (0, shared_1.buildBackendEnv)({
|
|
151
|
-
ports: {
|
|
152
|
-
backendPort: actualBackendPort,
|
|
153
|
-
webPort: actualWebPort,
|
|
154
|
-
agentctlPort,
|
|
155
|
-
backendUrl,
|
|
156
|
-
},
|
|
157
|
-
logLevel,
|
|
158
|
-
extra: {
|
|
159
|
-
KANDEV_DATABASE_PATH: dbPath,
|
|
160
|
-
...(debug ? { KANDEV_DEBUG_AGENT_MESSAGES: "true", KANDEV_DEBUG_PPROF_ENABLED: "true" } : {}),
|
|
161
|
-
},
|
|
162
|
-
});
|
|
163
|
-
const webEnv = (0, shared_1.buildWebEnv)({
|
|
164
|
-
ports: {
|
|
165
|
-
backendPort: actualBackendPort,
|
|
166
|
-
webPort: actualWebPort,
|
|
167
|
-
agentctlPort,
|
|
168
|
-
backendUrl,
|
|
169
|
-
},
|
|
170
|
-
production: true,
|
|
171
|
-
debug,
|
|
172
|
-
});
|
|
173
|
-
return {
|
|
174
|
-
bundleDir,
|
|
175
|
-
backendBin,
|
|
176
|
-
backendUrl,
|
|
177
|
-
backendEnv,
|
|
178
|
-
webEnv,
|
|
179
|
-
releaseTag,
|
|
180
|
-
webPort: actualWebPort,
|
|
181
|
-
agentctlPort,
|
|
182
|
-
dbPath,
|
|
183
|
-
logLevel,
|
|
184
|
-
showOutput,
|
|
185
|
-
};
|
|
186
|
-
}
|
|
187
|
-
/**
|
|
188
|
-
* Attach a ring buffer to a readable stream, keeping roughly the last `maxChars`
|
|
189
|
-
* characters. Note: the limit is measured in JS string length (UTF-16 code units),
|
|
190
|
-
* not bytes — fine for log output which is overwhelmingly ASCII.
|
|
191
|
-
*/
|
|
192
|
-
function attachRingBuffer(stream, maxChars = 64 * 1024) {
|
|
193
|
-
let buf = "";
|
|
194
|
-
stream?.on("data", (chunk) => {
|
|
195
|
-
buf += typeof chunk === "string" ? chunk : chunk.toString("utf8");
|
|
196
|
-
if (buf.length > maxChars) {
|
|
197
|
-
buf = buf.slice(buf.length - maxChars);
|
|
198
|
-
}
|
|
199
|
-
});
|
|
200
|
-
return () => buf;
|
|
201
|
-
}
|
|
202
|
-
async function launchBundle(prepared) {
|
|
203
|
-
(0, shared_1.logStartupInfo)({
|
|
204
|
-
header: `release: ${prepared.releaseTag}`,
|
|
205
|
-
ports: {
|
|
206
|
-
backendPort: Number(prepared.backendEnv.KANDEV_SERVER_PORT),
|
|
207
|
-
webPort: prepared.webPort,
|
|
208
|
-
agentctlPort: prepared.agentctlPort,
|
|
209
|
-
backendUrl: prepared.backendUrl,
|
|
210
|
-
},
|
|
211
|
-
dbPath: prepared.dbPath,
|
|
212
|
-
logLevel: prepared.logLevel,
|
|
213
|
-
});
|
|
214
|
-
const supervisor = (0, process_1.createProcessSupervisor)();
|
|
215
|
-
supervisor.attachSignalHandlers();
|
|
216
|
-
const backend = await (0, backend_1.launchRestartableBackend)({
|
|
217
|
-
command: prepared.backendBin,
|
|
218
|
-
args: [],
|
|
219
|
-
cwd: node_path_1.default.dirname(prepared.backendBin),
|
|
220
|
-
env: prepared.backendEnv,
|
|
221
|
-
homeDir: (0, constants_1.resolveKandevHomeDir)(),
|
|
222
|
-
ports: {
|
|
223
|
-
backendPort: Number(prepared.backendEnv.KANDEV_SERVER_PORT),
|
|
224
|
-
webPort: prepared.webPort,
|
|
225
|
-
agentctlPort: prepared.agentctlPort,
|
|
226
|
-
backendUrl: prepared.backendUrl,
|
|
227
|
-
},
|
|
228
|
-
mode: "run",
|
|
229
|
-
stdio: prepared.showOutput ? ["ignore", "inherit", "inherit"] : ["ignore", "pipe", "inherit"],
|
|
230
|
-
supervisor,
|
|
231
|
-
});
|
|
232
|
-
const backendProc = backend.proc;
|
|
233
|
-
const readBuffered = prepared.showOutput ? () => "" : attachRingBuffer(backendProc.stdout);
|
|
234
|
-
let dumped = false;
|
|
235
|
-
const dumpBackendLogs = () => {
|
|
236
|
-
if (dumped)
|
|
237
|
-
return;
|
|
238
|
-
dumped = true;
|
|
239
|
-
const buffered = readBuffered();
|
|
240
|
-
if (buffered.trim().length === 0)
|
|
241
|
-
return;
|
|
242
|
-
console.error("[kandev] --- backend stdout (last captured output) ---");
|
|
243
|
-
console.error(buffered.trimEnd());
|
|
244
|
-
console.error("[kandev] --- end backend stdout ---");
|
|
245
|
-
};
|
|
246
|
-
const webServerPath = (0, bundle_1.resolveWebServerPath)(prepared.bundleDir);
|
|
247
|
-
if (!webServerPath) {
|
|
248
|
-
throw new Error("Web server entry (server.js) not found in bundle");
|
|
249
|
-
}
|
|
250
|
-
return { supervisor, backendProc, webServerPath, dumpBackendLogs };
|
|
251
|
-
}
|
|
252
|
-
async function runRelease({ runtimeVersion, backendPort, webPort, verbose = false, debug = false, headless = false, }) {
|
|
253
|
-
const prepared = await prepareBundleForLaunch({
|
|
254
|
-
runtimeVersion,
|
|
255
|
-
backendPort,
|
|
256
|
-
webPort,
|
|
257
|
-
verbose,
|
|
258
|
-
debug,
|
|
259
|
-
});
|
|
260
|
-
const { supervisor, backendProc, webServerPath, dumpBackendLogs } = await launchBundle(prepared);
|
|
261
|
-
const healthTimeoutMs = (0, health_1.resolveHealthTimeoutMs)(constants_1.HEALTH_TIMEOUT_MS_RELEASE);
|
|
262
|
-
console.log("[kandev] starting backend...");
|
|
263
|
-
await (0, health_1.waitForHealth)(prepared.backendUrl, backendProc, healthTimeoutMs, dumpBackendLogs);
|
|
264
|
-
console.log(`[kandev] backend ready at ${prepared.backendUrl}`);
|
|
265
|
-
const webUrl = `http://localhost:${prepared.webPort}`;
|
|
266
|
-
console.log("[kandev] starting web...");
|
|
267
|
-
const webProc = (0, web_1.launchWebApp)({
|
|
268
|
-
command: "node",
|
|
269
|
-
args: [webServerPath],
|
|
270
|
-
cwd: node_path_1.default.dirname(webServerPath),
|
|
271
|
-
env: prepared.webEnv,
|
|
272
|
-
supervisor,
|
|
273
|
-
label: "web",
|
|
274
|
-
quiet: !prepared.showOutput,
|
|
275
|
-
});
|
|
276
|
-
await (0, health_1.waitForUrlReady)(webUrl, webProc, healthTimeoutMs);
|
|
277
|
-
if (headless) {
|
|
278
|
-
console.log(`[kandev] ready (headless) at ${prepared.backendUrl}`);
|
|
279
|
-
return;
|
|
280
|
-
}
|
|
281
|
-
console.log("[kandev] open: " + prepared.backendUrl);
|
|
282
|
-
(0, web_1.openBrowser)(prepared.backendUrl);
|
|
283
|
-
}
|
package/dist/runtime.js
DELETED
|
@@ -1,76 +0,0 @@
|
|
|
1
|
-
"use strict";
|
|
2
|
-
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
-
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
-
};
|
|
5
|
-
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
-
exports.resolveRuntime = resolveRuntime;
|
|
7
|
-
exports.validateBundle = validateBundle;
|
|
8
|
-
const node_path_1 = __importDefault(require("node:path"));
|
|
9
|
-
const node_fs_1 = __importDefault(require("node:fs"));
|
|
10
|
-
const bundle_1 = require("./bundle");
|
|
11
|
-
const platform_1 = require("./platform");
|
|
12
|
-
const PLATFORM_TO_NPM_PACKAGE = {
|
|
13
|
-
"linux-x64": "@kdlbs/runtime-linux-x64",
|
|
14
|
-
"linux-arm64": "@kdlbs/runtime-linux-arm64",
|
|
15
|
-
"macos-x64": "@kdlbs/runtime-darwin-x64",
|
|
16
|
-
"macos-arm64": "@kdlbs/runtime-darwin-arm64",
|
|
17
|
-
"windows-x64": "@kdlbs/runtime-win32-x64",
|
|
18
|
-
};
|
|
19
|
-
/**
|
|
20
|
-
* Resolve the runtime bundle directory using a two-step priority chain:
|
|
21
|
-
*
|
|
22
|
-
* 1. KANDEV_BUNDLE_DIR env var — set by the Homebrew formula wrapper and
|
|
23
|
-
* useful for local testing. Skips all other resolution.
|
|
24
|
-
* 2. Installed npm runtime package — looks for @kdlbs/runtime-{platform}
|
|
25
|
-
* in node_modules via Node module resolution. Works after
|
|
26
|
-
* `npx kandev@latest` or `npm install -g kandev` (requires npm 7+).
|
|
27
|
-
*
|
|
28
|
-
* The explicit `--runtime-version <tag>` download path is handled directly
|
|
29
|
-
* in run.ts (which manages the GitHub download + cache itself); it does
|
|
30
|
-
* not flow through this function.
|
|
31
|
-
*
|
|
32
|
-
* Throws with an actionable error message if no runtime is found.
|
|
33
|
-
*/
|
|
34
|
-
function resolveRuntime() {
|
|
35
|
-
const envBundleDir = process.env.KANDEV_BUNDLE_DIR;
|
|
36
|
-
if (envBundleDir) {
|
|
37
|
-
validateBundle(envBundleDir);
|
|
38
|
-
return { bundleDir: envBundleDir, source: "env" };
|
|
39
|
-
}
|
|
40
|
-
const platformDir = (0, platform_1.getPlatformDir)();
|
|
41
|
-
const packageName = PLATFORM_TO_NPM_PACKAGE[platformDir];
|
|
42
|
-
let pkgJsonPath = null;
|
|
43
|
-
try {
|
|
44
|
-
pkgJsonPath = require.resolve(`${packageName}/package.json`);
|
|
45
|
-
}
|
|
46
|
-
catch {
|
|
47
|
-
// MODULE_NOT_FOUND — npm runtime package is not installed. Fall through
|
|
48
|
-
// to the actionable error below.
|
|
49
|
-
}
|
|
50
|
-
if (pkgJsonPath) {
|
|
51
|
-
// The package IS installed. If validateBundle throws here, the bundle is
|
|
52
|
-
// present but corrupt — surface the error rather than the generic
|
|
53
|
-
// "no runtime found" message below.
|
|
54
|
-
const packageRoot = node_path_1.default.dirname(pkgJsonPath);
|
|
55
|
-
validateBundle(packageRoot);
|
|
56
|
-
return { bundleDir: packageRoot, source: "npm" };
|
|
57
|
-
}
|
|
58
|
-
throw new Error(`No Kandev runtime found for ${platformDir}.\n` +
|
|
59
|
-
` Install via npm (requires npm 7+): npx kandev@latest\n` +
|
|
60
|
-
` Install via Homebrew: brew install kdlbs/kandev/kandev\n` +
|
|
61
|
-
` Download a specific version (debug): kandev --runtime-version <tag>`);
|
|
62
|
-
}
|
|
63
|
-
function validateBundle(bundleDir) {
|
|
64
|
-
const backendBin = node_path_1.default.join(bundleDir, "bin", (0, platform_1.getBinaryName)("kandev"));
|
|
65
|
-
if (!node_fs_1.default.existsSync(backendBin)) {
|
|
66
|
-
throw new Error(`Backend binary not found in bundle at ${bundleDir}`);
|
|
67
|
-
}
|
|
68
|
-
const agentctlBin = node_path_1.default.join(bundleDir, "bin", (0, platform_1.getBinaryName)("agentctl"));
|
|
69
|
-
if (!node_fs_1.default.existsSync(agentctlBin)) {
|
|
70
|
-
throw new Error(`agentctl binary not found in bundle at ${bundleDir}`);
|
|
71
|
-
}
|
|
72
|
-
const webServerPath = (0, bundle_1.resolveWebServerPath)(bundleDir);
|
|
73
|
-
if (!webServerPath) {
|
|
74
|
-
throw new Error(`Web server (server.js) not found in bundle at ${bundleDir}`);
|
|
75
|
-
}
|
|
76
|
-
}
|