kandev 0.58.0 → 0.60.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/cli.js +1 -0
- package/dist/dev.js +9 -5
- package/dist/process.js +33 -0
- package/dist/run.js +16 -6
- package/dist/shared.js +4 -1
- package/dist/start.js +14 -6
- package/dist/supervisor/backend.js +83 -0
- package/dist/supervisor/child.js +64 -0
- package/dist/supervisor/control.js +97 -0
- package/dist/supervisor/manifest.js +74 -0
- package/dist/supervisor/paths.js +33 -0
- package/package.json +6 -6
package/dist/cli.js
CHANGED
package/dist/dev.js
CHANGED
|
@@ -5,7 +5,6 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
5
5
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
6
|
exports.runDev = runDev;
|
|
7
7
|
exports.resolveDevBackendEnv = resolveDevBackendEnv;
|
|
8
|
-
const node_child_process_1 = require("node:child_process");
|
|
9
8
|
const node_fs_1 = __importDefault(require("node:fs"));
|
|
10
9
|
const node_path_1 = __importDefault(require("node:path"));
|
|
11
10
|
const backup_1 = require("./backup");
|
|
@@ -14,6 +13,7 @@ const health_1 = require("./health");
|
|
|
14
13
|
const kandev_env_1 = require("./kandev-env");
|
|
15
14
|
const process_1 = require("./process");
|
|
16
15
|
const shared_1 = require("./shared");
|
|
16
|
+
const backend_1 = require("./supervisor/backend");
|
|
17
17
|
const web_1 = require("./web");
|
|
18
18
|
async function runDev({ repoRoot, backendPort, webPort }) {
|
|
19
19
|
const ports = await (0, shared_1.pickPorts)(backendPort, webPort);
|
|
@@ -52,16 +52,20 @@ async function runDev({ repoRoot, backendPort, webPort }) {
|
|
|
52
52
|
node_path_1.default.join("apps", "backend"),
|
|
53
53
|
"dev",
|
|
54
54
|
]);
|
|
55
|
-
const
|
|
55
|
+
const backend = await (0, backend_1.launchRestartableBackend)({
|
|
56
|
+
command: backendCmd,
|
|
57
|
+
args: backendArgs,
|
|
56
58
|
cwd: repoRoot,
|
|
57
59
|
env: backendEnv,
|
|
60
|
+
homeDir: backendEnv.KANDEV_HOME_DIR ?? (0, constants_1.devKandevHome)(repoRoot),
|
|
61
|
+
ports,
|
|
62
|
+
mode: "dev",
|
|
58
63
|
stdio: "inherit",
|
|
64
|
+
supervisor,
|
|
59
65
|
});
|
|
60
|
-
supervisor.children.push(backendProc);
|
|
61
|
-
(0, shared_1.attachBackendExitHandler)(backendProc, supervisor);
|
|
62
66
|
const healthTimeoutMs = (0, health_1.resolveHealthTimeoutMs)(constants_1.HEALTH_TIMEOUT_MS_DEV);
|
|
63
67
|
console.log("[kandev] starting backend...");
|
|
64
|
-
await (0, health_1.waitForHealth)(ports.backendUrl,
|
|
68
|
+
await (0, health_1.waitForHealth)(ports.backendUrl, backend.proc, healthTimeoutMs);
|
|
65
69
|
console.log(`[kandev] backend ready at ${ports.backendUrl}`);
|
|
66
70
|
console.log("[kandev] starting web...");
|
|
67
71
|
const webProc = (0, web_1.launchWebApp)({
|
package/dist/process.js
CHANGED
|
@@ -4,6 +4,8 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
4
4
|
};
|
|
5
5
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
6
|
exports.createProcessSupervisor = createProcessSupervisor;
|
|
7
|
+
exports.ignoreBrokenPipe = ignoreBrokenPipe;
|
|
8
|
+
exports.__resetBrokenPipeGuard = __resetBrokenPipeGuard;
|
|
7
9
|
const tree_kill_1 = __importDefault(require("tree-kill"));
|
|
8
10
|
const SHUTDOWN_TIMEOUT_MS = 10000;
|
|
9
11
|
function createProcessSupervisor() {
|
|
@@ -25,6 +27,12 @@ function createProcessSupervisor() {
|
|
|
25
27
|
void shutdown(`signal ${signal}`).then(() => process.exit(0));
|
|
26
28
|
};
|
|
27
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();
|
|
28
36
|
process.on("SIGINT", onSignal);
|
|
29
37
|
// SIGTERM is not available on Windows — only attach where supported
|
|
30
38
|
if (process.platform !== "win32") {
|
|
@@ -33,6 +41,31 @@ function createProcessSupervisor() {
|
|
|
33
41
|
};
|
|
34
42
|
return { children, shutdown, attachSignalHandlers };
|
|
35
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
|
+
}
|
|
36
69
|
/**
|
|
37
70
|
* Terminate a process and wait for it to exit.
|
|
38
71
|
* On Unix: sends SIGTERM, falls back to SIGKILL after timeout.
|
package/dist/run.js
CHANGED
|
@@ -7,7 +7,6 @@ exports.findCachedRelease = findCachedRelease;
|
|
|
7
7
|
exports.cleanOldReleases = cleanOldReleases;
|
|
8
8
|
exports.attachRingBuffer = attachRingBuffer;
|
|
9
9
|
exports.runRelease = runRelease;
|
|
10
|
-
const node_child_process_1 = require("node:child_process");
|
|
11
10
|
const node_fs_1 = __importDefault(require("node:fs"));
|
|
12
11
|
const node_path_1 = __importDefault(require("node:path"));
|
|
13
12
|
const bundle_1 = require("./bundle");
|
|
@@ -20,6 +19,7 @@ const ports_1 = require("./ports");
|
|
|
20
19
|
const process_1 = require("./process");
|
|
21
20
|
const runtime_1 = require("./runtime");
|
|
22
21
|
const shared_1 = require("./shared");
|
|
22
|
+
const backend_1 = require("./supervisor/backend");
|
|
23
23
|
const web_1 = require("./web");
|
|
24
24
|
/**
|
|
25
25
|
* Find a cached release binary to use when GitHub is unreachable.
|
|
@@ -199,7 +199,7 @@ function attachRingBuffer(stream, maxChars = 64 * 1024) {
|
|
|
199
199
|
});
|
|
200
200
|
return () => buf;
|
|
201
201
|
}
|
|
202
|
-
function launchBundle(prepared) {
|
|
202
|
+
async function launchBundle(prepared) {
|
|
203
203
|
(0, shared_1.logStartupInfo)({
|
|
204
204
|
header: `release: ${prepared.releaseTag}`,
|
|
205
205
|
ports: {
|
|
@@ -213,12 +213,23 @@ function launchBundle(prepared) {
|
|
|
213
213
|
});
|
|
214
214
|
const supervisor = (0, process_1.createProcessSupervisor)();
|
|
215
215
|
supervisor.attachSignalHandlers();
|
|
216
|
-
const
|
|
216
|
+
const backend = await (0, backend_1.launchRestartableBackend)({
|
|
217
|
+
command: prepared.backendBin,
|
|
218
|
+
args: [],
|
|
217
219
|
cwd: node_path_1.default.dirname(prepared.backendBin),
|
|
218
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",
|
|
219
229
|
stdio: prepared.showOutput ? ["ignore", "inherit", "inherit"] : ["ignore", "pipe", "inherit"],
|
|
230
|
+
supervisor,
|
|
220
231
|
});
|
|
221
|
-
|
|
232
|
+
const backendProc = backend.proc;
|
|
222
233
|
const readBuffered = prepared.showOutput ? () => "" : attachRingBuffer(backendProc.stdout);
|
|
223
234
|
let dumped = false;
|
|
224
235
|
const dumpBackendLogs = () => {
|
|
@@ -232,7 +243,6 @@ function launchBundle(prepared) {
|
|
|
232
243
|
console.error(buffered.trimEnd());
|
|
233
244
|
console.error("[kandev] --- end backend stdout ---");
|
|
234
245
|
};
|
|
235
|
-
(0, shared_1.attachBackendExitHandler)(backendProc, supervisor);
|
|
236
246
|
const webServerPath = (0, bundle_1.resolveWebServerPath)(prepared.bundleDir);
|
|
237
247
|
if (!webServerPath) {
|
|
238
248
|
throw new Error("Web server entry (server.js) not found in bundle");
|
|
@@ -247,7 +257,7 @@ async function runRelease({ runtimeVersion, backendPort, webPort, verbose = fals
|
|
|
247
257
|
verbose,
|
|
248
258
|
debug,
|
|
249
259
|
});
|
|
250
|
-
const { supervisor, backendProc, webServerPath, dumpBackendLogs } = launchBundle(prepared);
|
|
260
|
+
const { supervisor, backendProc, webServerPath, dumpBackendLogs } = await launchBundle(prepared);
|
|
251
261
|
const healthTimeoutMs = (0, health_1.resolveHealthTimeoutMs)(constants_1.HEALTH_TIMEOUT_MS_RELEASE);
|
|
252
262
|
console.log("[kandev] starting backend...");
|
|
253
263
|
await (0, health_1.waitForHealth)(prepared.backendUrl, backendProc, healthTimeoutMs, dumpBackendLogs);
|
package/dist/shared.js
CHANGED
|
@@ -202,9 +202,12 @@ function networkUrlsForPort(port, hosts) {
|
|
|
202
202
|
* @param backendProc - The backend child process
|
|
203
203
|
* @param supervisor - The process supervisor managing child processes
|
|
204
204
|
*/
|
|
205
|
-
function attachBackendExitHandler(backendProc, supervisor) {
|
|
205
|
+
function attachBackendExitHandler(backendProc, supervisor, options = {}) {
|
|
206
206
|
backendProc.on("exit", (code, signal) => {
|
|
207
207
|
console.error(`[kandev] backend exited (code=${code}, signal=${signal})`);
|
|
208
|
+
if (options.shouldShutdown && !options.shouldShutdown()) {
|
|
209
|
+
return;
|
|
210
|
+
}
|
|
208
211
|
const exitCode = signal ? 0 : (code ?? 1);
|
|
209
212
|
void supervisor.shutdown("backend exit").then(() => process.exit(exitCode));
|
|
210
213
|
});
|
package/dist/start.js
CHANGED
|
@@ -17,7 +17,6 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
17
17
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
18
18
|
exports.resolveStandaloneServerPath = resolveStandaloneServerPath;
|
|
19
19
|
exports.runStart = runStart;
|
|
20
|
-
const node_child_process_1 = require("node:child_process");
|
|
21
20
|
const node_fs_1 = __importDefault(require("node:fs"));
|
|
22
21
|
const node_path_1 = __importDefault(require("node:path"));
|
|
23
22
|
const constants_1 = require("./constants");
|
|
@@ -25,6 +24,7 @@ const health_1 = require("./health");
|
|
|
25
24
|
const platform_1 = require("./platform");
|
|
26
25
|
const process_1 = require("./process");
|
|
27
26
|
const shared_1 = require("./shared");
|
|
27
|
+
const backend_1 = require("./supervisor/backend");
|
|
28
28
|
const web_1 = require("./web");
|
|
29
29
|
/**
|
|
30
30
|
* Locates the standalone Next.js `server.js` inside `apps/web/.next/standalone/`.
|
|
@@ -83,7 +83,7 @@ function findWebServerJs(dir) {
|
|
|
83
83
|
* @param options - Configuration for the start command
|
|
84
84
|
* @throws Error if backend binary or web build is not found
|
|
85
85
|
*/
|
|
86
|
-
async function runStart({ repoRoot, backendPort, webPort, verbose = false, debug = false, }) {
|
|
86
|
+
async function runStart({ repoRoot, backendPort, webPort, verbose = false, debug = false, headless = false, }) {
|
|
87
87
|
const ports = await (0, shared_1.pickPorts)(backendPort, webPort);
|
|
88
88
|
const backendBin = node_path_1.default.join(repoRoot, "apps", "backend", "bin", (0, platform_1.getBinaryName)("kandev"));
|
|
89
89
|
if (!node_fs_1.default.existsSync(backendBin)) {
|
|
@@ -151,16 +151,20 @@ async function runStart({ repoRoot, backendPort, webPort, verbose = false, debug
|
|
|
151
151
|
supervisor.attachSignalHandlers();
|
|
152
152
|
// Start backend: ignore stdin, show stdout only in verbose/debug mode, always show stderr
|
|
153
153
|
// Stderr is always inherited to ensure error messages are visible immediately (no pipe buffering)
|
|
154
|
-
const
|
|
154
|
+
const backend = await (0, backend_1.launchRestartableBackend)({
|
|
155
|
+
command: backendBin,
|
|
156
|
+
args: [],
|
|
155
157
|
cwd: node_path_1.default.dirname(backendBin),
|
|
156
158
|
env: backendEnv,
|
|
159
|
+
homeDir: (0, constants_1.resolveKandevHomeDir)(),
|
|
160
|
+
ports,
|
|
161
|
+
mode: "start",
|
|
157
162
|
stdio: showOutput ? ["ignore", "inherit", "inherit"] : ["ignore", "ignore", "inherit"],
|
|
163
|
+
supervisor,
|
|
158
164
|
});
|
|
159
|
-
supervisor.children.push(backendProc);
|
|
160
|
-
(0, shared_1.attachBackendExitHandler)(backendProc, supervisor);
|
|
161
165
|
const healthTimeoutMs = (0, health_1.resolveHealthTimeoutMs)(constants_1.HEALTH_TIMEOUT_MS_RELEASE);
|
|
162
166
|
console.log("[kandev] starting backend...");
|
|
163
|
-
await (0, health_1.waitForHealth)(ports.backendUrl,
|
|
167
|
+
await (0, health_1.waitForHealth)(ports.backendUrl, backend.proc, healthTimeoutMs);
|
|
164
168
|
console.log(`[kandev] backend ready at ${ports.backendUrl}`);
|
|
165
169
|
// Use standalone server.js directly (not pnpm start)
|
|
166
170
|
const webUrl = `http://localhost:${ports.webPort}`;
|
|
@@ -176,5 +180,9 @@ async function runStart({ repoRoot, backendPort, webPort, verbose = false, debug
|
|
|
176
180
|
});
|
|
177
181
|
await (0, health_1.waitForUrlReady)(webUrl, webProc, healthTimeoutMs);
|
|
178
182
|
console.log("[kandev] open: " + ports.backendUrl);
|
|
183
|
+
if (headless) {
|
|
184
|
+
console.log(`[kandev] ready (headless) at ${ports.backendUrl}`);
|
|
185
|
+
return;
|
|
186
|
+
}
|
|
179
187
|
(0, web_1.openBrowser)(ports.backendUrl);
|
|
180
188
|
}
|
|
@@ -0,0 +1,83 @@
|
|
|
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.launchRestartableBackend = launchRestartableBackend;
|
|
7
|
+
exports.shouldUseSupervisor = shouldUseSupervisor;
|
|
8
|
+
exports.withSupervisorEnv = withSupervisorEnv;
|
|
9
|
+
const node_child_process_1 = require("node:child_process");
|
|
10
|
+
const node_path_1 = __importDefault(require("node:path"));
|
|
11
|
+
const shared_1 = require("../shared");
|
|
12
|
+
const manifest_1 = require("./manifest");
|
|
13
|
+
const paths_1 = require("./paths");
|
|
14
|
+
const child_1 = require("./child");
|
|
15
|
+
const control_1 = require("./control");
|
|
16
|
+
async function launchRestartableBackend({ command, args, cwd, env, homeDir, ports, mode, stdio, supervisor, }) {
|
|
17
|
+
if (!shouldUseSupervisor(env)) {
|
|
18
|
+
const proc = (0, node_child_process_1.spawn)(command, args, { cwd, env, stdio });
|
|
19
|
+
supervisor.children.push(proc);
|
|
20
|
+
(0, shared_1.attachBackendExitHandler)(proc, supervisor);
|
|
21
|
+
return { proc, control: null, env };
|
|
22
|
+
}
|
|
23
|
+
const supervisorEnv = withSupervisorEnv(env, homeDir);
|
|
24
|
+
const manifest = (0, manifest_1.buildLaunchManifest)({
|
|
25
|
+
backend_executable: resolveExecutable(command),
|
|
26
|
+
argv: args,
|
|
27
|
+
cwd,
|
|
28
|
+
env: supervisorEnv,
|
|
29
|
+
home_dir: homeDir,
|
|
30
|
+
port: ports.backendPort,
|
|
31
|
+
mode,
|
|
32
|
+
});
|
|
33
|
+
(0, manifest_1.writeLaunchManifest)(manifest, supervisorEnv.KANDEV_SUPERVISOR_MANIFEST);
|
|
34
|
+
const child = (0, child_1.createRestartableChild)(manifest, { stdio, extraEnv: supervisorEnv });
|
|
35
|
+
let restarting = false;
|
|
36
|
+
const attachExit = (proc) => {
|
|
37
|
+
(0, shared_1.attachBackendExitHandler)(proc, supervisor, {
|
|
38
|
+
shouldShutdown: () => !restarting,
|
|
39
|
+
});
|
|
40
|
+
};
|
|
41
|
+
const proc = child.start();
|
|
42
|
+
supervisor.children.push(proc);
|
|
43
|
+
attachExit(proc);
|
|
44
|
+
const control = await (0, control_1.startControlServer)(supervisorEnv.KANDEV_SUPERVISOR_SOCKET, async () => {
|
|
45
|
+
restarting = true;
|
|
46
|
+
try {
|
|
47
|
+
const next = await child.restart();
|
|
48
|
+
supervisor.children.push(next);
|
|
49
|
+
attachExit(next);
|
|
50
|
+
}
|
|
51
|
+
finally {
|
|
52
|
+
restarting = false;
|
|
53
|
+
}
|
|
54
|
+
});
|
|
55
|
+
return { proc, control, env: supervisorEnv };
|
|
56
|
+
}
|
|
57
|
+
function shouldUseSupervisor(env = process.env) {
|
|
58
|
+
return env.KANDEV_NO_SUPERVISOR !== "true";
|
|
59
|
+
}
|
|
60
|
+
function withSupervisorEnv(env, homeDir) {
|
|
61
|
+
(0, paths_1.prepareSupervisorDir)(homeDir);
|
|
62
|
+
return {
|
|
63
|
+
...env,
|
|
64
|
+
KANDEV_SUPERVISOR_SOCKET: (0, paths_1.socketPath)(homeDir),
|
|
65
|
+
KANDEV_SUPERVISOR_MANIFEST: (0, paths_1.manifestPath)(homeDir),
|
|
66
|
+
KANDEV_RESTART_ADAPTER: "supervisor",
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
function resolveExecutable(command) {
|
|
70
|
+
if (node_path_1.default.isAbsolute(command))
|
|
71
|
+
return command;
|
|
72
|
+
const found = (0, node_child_process_1.spawnSync)(process.platform === "win32" ? "where" : "which", [command], {
|
|
73
|
+
encoding: "utf8",
|
|
74
|
+
});
|
|
75
|
+
const first = found.stdout
|
|
76
|
+
?.split(/\r?\n/)
|
|
77
|
+
.map((line) => line.trim())
|
|
78
|
+
.find(Boolean);
|
|
79
|
+
if (!first) {
|
|
80
|
+
throw new Error(`Unable to resolve executable ${command}`);
|
|
81
|
+
}
|
|
82
|
+
return first;
|
|
83
|
+
}
|
|
@@ -0,0 +1,64 @@
|
|
|
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.createRestartableChild = createRestartableChild;
|
|
7
|
+
const node_child_process_1 = require("node:child_process");
|
|
8
|
+
const tree_kill_1 = __importDefault(require("tree-kill"));
|
|
9
|
+
const RESTART_TIMEOUT_MS = 10000;
|
|
10
|
+
function createRestartableChild(manifest, options = {}) {
|
|
11
|
+
let child = null;
|
|
12
|
+
const start = () => {
|
|
13
|
+
child = (0, node_child_process_1.spawn)(manifest.backend_executable, manifest.argv, {
|
|
14
|
+
cwd: manifest.cwd,
|
|
15
|
+
env: {
|
|
16
|
+
...process.env,
|
|
17
|
+
...manifest.env,
|
|
18
|
+
...options.extraEnv,
|
|
19
|
+
},
|
|
20
|
+
stdio: options.stdio ?? "inherit",
|
|
21
|
+
});
|
|
22
|
+
return child;
|
|
23
|
+
};
|
|
24
|
+
const stop = async () => {
|
|
25
|
+
if (!child?.pid || child.exitCode !== null)
|
|
26
|
+
return;
|
|
27
|
+
await terminate(child, RESTART_TIMEOUT_MS);
|
|
28
|
+
};
|
|
29
|
+
const restart = async () => {
|
|
30
|
+
await stop();
|
|
31
|
+
return start();
|
|
32
|
+
};
|
|
33
|
+
return {
|
|
34
|
+
current: () => child,
|
|
35
|
+
start,
|
|
36
|
+
restart,
|
|
37
|
+
stop,
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
function terminate(proc, timeoutMs) {
|
|
41
|
+
return new Promise((resolve) => {
|
|
42
|
+
const pid = proc.pid;
|
|
43
|
+
if (!pid) {
|
|
44
|
+
resolve();
|
|
45
|
+
return;
|
|
46
|
+
}
|
|
47
|
+
let done = false;
|
|
48
|
+
const finish = () => {
|
|
49
|
+
if (done)
|
|
50
|
+
return;
|
|
51
|
+
done = true;
|
|
52
|
+
clearTimeout(timeout);
|
|
53
|
+
resolve();
|
|
54
|
+
};
|
|
55
|
+
const timeout = setTimeout(() => {
|
|
56
|
+
(0, tree_kill_1.default)(pid, process.platform === "win32" ? undefined : "SIGKILL", finish);
|
|
57
|
+
}, timeoutMs);
|
|
58
|
+
proc.once("exit", finish);
|
|
59
|
+
(0, tree_kill_1.default)(pid, process.platform === "win32" ? undefined : "SIGTERM", (err) => {
|
|
60
|
+
if (err)
|
|
61
|
+
finish();
|
|
62
|
+
});
|
|
63
|
+
});
|
|
64
|
+
}
|
|
@@ -0,0 +1,97 @@
|
|
|
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.startControlServer = startControlServer;
|
|
7
|
+
exports.requestRestart = requestRestart;
|
|
8
|
+
const node_net_1 = __importDefault(require("node:net"));
|
|
9
|
+
const node_fs_1 = __importDefault(require("node:fs"));
|
|
10
|
+
function startControlServer(socket, onRestart) {
|
|
11
|
+
let restartInProgress = false;
|
|
12
|
+
const scheduleRestart = () => {
|
|
13
|
+
if (restartInProgress)
|
|
14
|
+
return false;
|
|
15
|
+
restartInProgress = true;
|
|
16
|
+
setTimeout(() => {
|
|
17
|
+
void onRestart().finally(() => {
|
|
18
|
+
restartInProgress = false;
|
|
19
|
+
});
|
|
20
|
+
}, 0);
|
|
21
|
+
return true;
|
|
22
|
+
};
|
|
23
|
+
if (process.platform !== "win32" && node_fs_1.default.existsSync(socket)) {
|
|
24
|
+
node_fs_1.default.unlinkSync(socket);
|
|
25
|
+
}
|
|
26
|
+
const server = node_net_1.default.createServer((conn) => {
|
|
27
|
+
let buf = "";
|
|
28
|
+
conn.setEncoding("utf8");
|
|
29
|
+
conn.on("data", (chunk) => {
|
|
30
|
+
buf += chunk;
|
|
31
|
+
if (!buf.includes("\n"))
|
|
32
|
+
return;
|
|
33
|
+
const line = buf.slice(0, buf.indexOf("\n"));
|
|
34
|
+
void handleControlLine(line, scheduleRestart)
|
|
35
|
+
.then((resp) => {
|
|
36
|
+
conn.end(`${JSON.stringify(resp)}\n`);
|
|
37
|
+
})
|
|
38
|
+
.catch((err) => {
|
|
39
|
+
conn.end(`${JSON.stringify({
|
|
40
|
+
accepted: false,
|
|
41
|
+
message: err instanceof Error ? err.message : String(err),
|
|
42
|
+
})}\n`);
|
|
43
|
+
});
|
|
44
|
+
});
|
|
45
|
+
});
|
|
46
|
+
return new Promise((resolve, reject) => {
|
|
47
|
+
server.once("error", reject);
|
|
48
|
+
server.listen(socket, () => {
|
|
49
|
+
server.off("error", reject);
|
|
50
|
+
resolve({
|
|
51
|
+
close: () => new Promise((closeResolve, closeReject) => {
|
|
52
|
+
server.close((err) => {
|
|
53
|
+
if (process.platform !== "win32" && node_fs_1.default.existsSync(socket)) {
|
|
54
|
+
node_fs_1.default.unlinkSync(socket);
|
|
55
|
+
}
|
|
56
|
+
if (err)
|
|
57
|
+
closeReject(err);
|
|
58
|
+
else
|
|
59
|
+
closeResolve();
|
|
60
|
+
});
|
|
61
|
+
}),
|
|
62
|
+
});
|
|
63
|
+
});
|
|
64
|
+
});
|
|
65
|
+
}
|
|
66
|
+
function requestRestart(socket) {
|
|
67
|
+
return new Promise((resolve, reject) => {
|
|
68
|
+
const conn = node_net_1.default.createConnection(socket);
|
|
69
|
+
let buf = "";
|
|
70
|
+
conn.setEncoding("utf8");
|
|
71
|
+
conn.on("connect", () => {
|
|
72
|
+
conn.write(`${JSON.stringify({ action: "restart" })}\n`);
|
|
73
|
+
});
|
|
74
|
+
conn.on("data", (chunk) => {
|
|
75
|
+
buf += chunk;
|
|
76
|
+
});
|
|
77
|
+
conn.on("error", reject);
|
|
78
|
+
conn.on("end", () => {
|
|
79
|
+
try {
|
|
80
|
+
resolve(JSON.parse(buf.trim()));
|
|
81
|
+
}
|
|
82
|
+
catch (err) {
|
|
83
|
+
reject(err);
|
|
84
|
+
}
|
|
85
|
+
});
|
|
86
|
+
});
|
|
87
|
+
}
|
|
88
|
+
async function handleControlLine(line, scheduleRestart) {
|
|
89
|
+
const req = JSON.parse(line);
|
|
90
|
+
if (req.action !== "restart") {
|
|
91
|
+
return { accepted: false, message: `unsupported action ${String(req.action)}` };
|
|
92
|
+
}
|
|
93
|
+
if (!scheduleRestart()) {
|
|
94
|
+
return { accepted: false, message: "Restart already in progress" };
|
|
95
|
+
}
|
|
96
|
+
return { accepted: true, message: "Restart accepted" };
|
|
97
|
+
}
|
|
@@ -0,0 +1,74 @@
|
|
|
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.buildLaunchManifest = buildLaunchManifest;
|
|
7
|
+
exports.writeLaunchManifest = writeLaunchManifest;
|
|
8
|
+
exports.readLaunchManifest = readLaunchManifest;
|
|
9
|
+
exports.allowedEnv = allowedEnv;
|
|
10
|
+
const node_fs_1 = __importDefault(require("node:fs"));
|
|
11
|
+
const node_path_1 = __importDefault(require("node:path"));
|
|
12
|
+
const paths_1 = require("./paths");
|
|
13
|
+
const ENV_ALLOWLIST = new Set([
|
|
14
|
+
"KANDEV_HOME_DIR",
|
|
15
|
+
"KANDEV_DATABASE_PATH",
|
|
16
|
+
"KANDEV_SERVER_PORT",
|
|
17
|
+
"KANDEV_WEB_INTERNAL_URL",
|
|
18
|
+
"KANDEV_AGENT_STANDALONE_PORT",
|
|
19
|
+
"KANDEV_LOG_LEVEL",
|
|
20
|
+
"KANDEV_DEBUG_DEV_MODE",
|
|
21
|
+
"KANDEV_DEBUG_AGENT_MESSAGES",
|
|
22
|
+
"KANDEV_DEBUG_PPROF_ENABLED",
|
|
23
|
+
"KANDEV_E2E_MOCK",
|
|
24
|
+
"KANDEV_MOCK_AGENT",
|
|
25
|
+
"KANDEV_MOCK_GITHUB",
|
|
26
|
+
"KANDEV_MOCK_JIRA",
|
|
27
|
+
"KANDEV_MOCK_LINEAR",
|
|
28
|
+
"KANDEV_SUPERVISOR_SOCKET",
|
|
29
|
+
"KANDEV_SUPERVISOR_MANIFEST",
|
|
30
|
+
"KANDEV_RESTART_ADAPTER",
|
|
31
|
+
]);
|
|
32
|
+
function buildLaunchManifest(input) {
|
|
33
|
+
if (!node_path_1.default.isAbsolute(input.backend_executable)) {
|
|
34
|
+
throw new Error("backend_executable must be absolute");
|
|
35
|
+
}
|
|
36
|
+
if (!node_path_1.default.isAbsolute(input.cwd)) {
|
|
37
|
+
throw new Error("cwd must be absolute");
|
|
38
|
+
}
|
|
39
|
+
if (!node_path_1.default.isAbsolute(input.home_dir)) {
|
|
40
|
+
throw new Error("home_dir must be absolute");
|
|
41
|
+
}
|
|
42
|
+
return {
|
|
43
|
+
version: 1,
|
|
44
|
+
backend_executable: input.backend_executable,
|
|
45
|
+
argv: [...input.argv],
|
|
46
|
+
cwd: input.cwd,
|
|
47
|
+
env: allowedEnv(input.env),
|
|
48
|
+
home_dir: input.home_dir,
|
|
49
|
+
port: input.port,
|
|
50
|
+
mode: input.mode,
|
|
51
|
+
created_at: (input.now ?? new Date()).toISOString(),
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
function writeLaunchManifest(manifest, targetPath = (0, paths_1.manifestPath)(manifest.home_dir)) {
|
|
55
|
+
(0, paths_1.prepareSupervisorDir)(manifest.home_dir);
|
|
56
|
+
node_fs_1.default.writeFileSync(targetPath, `${JSON.stringify(manifest, null, 2)}\n`, { mode: 0o600 });
|
|
57
|
+
if (process.platform !== "win32") {
|
|
58
|
+
node_fs_1.default.chmodSync(targetPath, 0o600);
|
|
59
|
+
}
|
|
60
|
+
return targetPath;
|
|
61
|
+
}
|
|
62
|
+
function readLaunchManifest(targetPath) {
|
|
63
|
+
return JSON.parse(node_fs_1.default.readFileSync(targetPath, "utf8"));
|
|
64
|
+
}
|
|
65
|
+
function allowedEnv(env) {
|
|
66
|
+
const out = {};
|
|
67
|
+
for (const key of ENV_ALLOWLIST) {
|
|
68
|
+
const value = env[key];
|
|
69
|
+
if (value !== undefined) {
|
|
70
|
+
out[key] = value;
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
return out;
|
|
74
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
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.supervisorDir = supervisorDir;
|
|
7
|
+
exports.manifestPath = manifestPath;
|
|
8
|
+
exports.socketPath = socketPath;
|
|
9
|
+
exports.prepareSupervisorDir = prepareSupervisorDir;
|
|
10
|
+
const node_fs_1 = __importDefault(require("node:fs"));
|
|
11
|
+
const node_path_1 = __importDefault(require("node:path"));
|
|
12
|
+
const constants_1 = require("../constants");
|
|
13
|
+
function supervisorDir(homeDir = (0, constants_1.resolveKandevHomeDir)()) {
|
|
14
|
+
return node_path_1.default.join(homeDir, "supervisor");
|
|
15
|
+
}
|
|
16
|
+
function manifestPath(homeDir = (0, constants_1.resolveKandevHomeDir)()) {
|
|
17
|
+
return node_path_1.default.join(supervisorDir(homeDir), "launch.json");
|
|
18
|
+
}
|
|
19
|
+
function socketPath(homeDir = (0, constants_1.resolveKandevHomeDir)()) {
|
|
20
|
+
if (process.platform === "win32") {
|
|
21
|
+
const safe = homeDir.replace(/[^a-zA-Z0-9_.-]/g, "-");
|
|
22
|
+
return `\\\\.\\pipe\\kandev-${safe}-supervisor`;
|
|
23
|
+
}
|
|
24
|
+
return node_path_1.default.join(supervisorDir(homeDir), "control.sock");
|
|
25
|
+
}
|
|
26
|
+
function prepareSupervisorDir(homeDir = (0, constants_1.resolveKandevHomeDir)()) {
|
|
27
|
+
const dir = supervisorDir(homeDir);
|
|
28
|
+
node_fs_1.default.mkdirSync(dir, { recursive: true, mode: 0o700 });
|
|
29
|
+
if (process.platform !== "win32") {
|
|
30
|
+
node_fs_1.default.chmodSync(dir, 0o700);
|
|
31
|
+
}
|
|
32
|
+
return dir;
|
|
33
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "kandev",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.60.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.60.0",
|
|
26
|
+
"@kdlbs/runtime-linux-arm64": "0.60.0",
|
|
27
|
+
"@kdlbs/runtime-darwin-x64": "0.60.0",
|
|
28
|
+
"@kdlbs/runtime-darwin-arm64": "0.60.0",
|
|
29
|
+
"@kdlbs/runtime-win32-x64": "0.60.0"
|
|
30
30
|
},
|
|
31
31
|
"dependencies": {
|
|
32
32
|
"tar": "^7.5.11",
|