copilot-hub 0.1.7 → 0.1.8
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 +19 -0
- package/package.json +1 -1
- package/scripts/dist/cli.mjs +126 -6
- package/scripts/dist/daemon.mjs +388 -0
- package/scripts/dist/service.mjs +520 -0
- package/scripts/dist/supervisor.mjs +22 -3
- package/scripts/src/cli.mts +148 -6
- package/scripts/src/daemon.mts +458 -0
- package/scripts/src/service.mts +635 -0
- package/scripts/src/supervisor.mts +25 -3
package/scripts/src/cli.mts
CHANGED
|
@@ -10,6 +10,8 @@ const __filename = fileURLToPath(import.meta.url);
|
|
|
10
10
|
const __dirname = path.dirname(__filename);
|
|
11
11
|
const repoRoot = path.resolve(__dirname, "..", "..");
|
|
12
12
|
const packageJsonPath = path.join(repoRoot, "package.json");
|
|
13
|
+
const runtimeDir = path.join(repoRoot, ".copilot-hub");
|
|
14
|
+
const servicePromptStatePath = path.join(runtimeDir, "service-onboarding.json");
|
|
13
15
|
const nodeBin = process.execPath;
|
|
14
16
|
const agentEngineEnvPath = path.join(repoRoot, "apps", "agent-engine", ".env");
|
|
15
17
|
const controlPlaneEnvPath = path.join(repoRoot, "apps", "control-plane", ".env");
|
|
@@ -46,19 +48,37 @@ async function main() {
|
|
|
46
48
|
runNode(["scripts/dist/configure.mjs", "--required-only"]);
|
|
47
49
|
runNode(["scripts/dist/ensure-shared-build.mjs"]);
|
|
48
50
|
await ensureCodexLogin();
|
|
51
|
+
await maybeOfferServiceInstall();
|
|
52
|
+
if (isServiceAlreadyInstalled()) {
|
|
53
|
+
runNode(["scripts/dist/service.mjs", "start"]);
|
|
54
|
+
return;
|
|
55
|
+
}
|
|
49
56
|
runNode(["scripts/dist/supervisor.mjs", "up"]);
|
|
50
57
|
return;
|
|
51
58
|
}
|
|
52
59
|
case "stop": {
|
|
60
|
+
if (isServiceAlreadyInstalled()) {
|
|
61
|
+
runNode(["scripts/dist/service.mjs", "stop"]);
|
|
62
|
+
return;
|
|
63
|
+
}
|
|
53
64
|
runNode(["scripts/dist/supervisor.mjs", "down"]);
|
|
54
65
|
return;
|
|
55
66
|
}
|
|
56
67
|
case "restart": {
|
|
57
68
|
runNode(["scripts/dist/ensure-shared-build.mjs"]);
|
|
69
|
+
if (isServiceAlreadyInstalled()) {
|
|
70
|
+
runNode(["scripts/dist/service.mjs", "stop"]);
|
|
71
|
+
runNode(["scripts/dist/service.mjs", "start"]);
|
|
72
|
+
return;
|
|
73
|
+
}
|
|
58
74
|
runNode(["scripts/dist/supervisor.mjs", "restart"]);
|
|
59
75
|
return;
|
|
60
76
|
}
|
|
61
77
|
case "status": {
|
|
78
|
+
if (isServiceAlreadyInstalled()) {
|
|
79
|
+
runNode(["scripts/dist/daemon.mjs", "status"]);
|
|
80
|
+
return;
|
|
81
|
+
}
|
|
62
82
|
runNode(["scripts/dist/supervisor.mjs", "status"]);
|
|
63
83
|
return;
|
|
64
84
|
}
|
|
@@ -70,6 +90,10 @@ async function main() {
|
|
|
70
90
|
runNode(["scripts/dist/configure.mjs"]);
|
|
71
91
|
return;
|
|
72
92
|
}
|
|
93
|
+
case "service": {
|
|
94
|
+
runNode(["scripts/dist/service.mjs", ...rawArgs.slice(1)]);
|
|
95
|
+
return;
|
|
96
|
+
}
|
|
73
97
|
default: {
|
|
74
98
|
printUsage();
|
|
75
99
|
process.exit(1);
|
|
@@ -78,16 +102,35 @@ async function main() {
|
|
|
78
102
|
}
|
|
79
103
|
|
|
80
104
|
function runNode(scriptArgs) {
|
|
105
|
+
const result = runNodeCapture(scriptArgs, "inherit");
|
|
106
|
+
const code = Number.isInteger(result.status) ? result.status : 1;
|
|
107
|
+
if (code !== 0) {
|
|
108
|
+
process.exit(code);
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
function runNodeCapture(scriptArgs, stdioMode = "pipe") {
|
|
113
|
+
const stdio: any = stdioMode === "inherit" ? "inherit" : ["ignore", "pipe", "pipe"];
|
|
81
114
|
const result = spawnSync(nodeBin, scriptArgs, {
|
|
82
115
|
cwd: repoRoot,
|
|
83
|
-
stdio
|
|
116
|
+
stdio,
|
|
84
117
|
shell: false,
|
|
118
|
+
encoding: "utf8",
|
|
85
119
|
});
|
|
86
120
|
|
|
87
|
-
const
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
121
|
+
const stdout = String(result.stdout ?? "").trim();
|
|
122
|
+
const stderr = String(result.stderr ?? "").trim();
|
|
123
|
+
const combinedOutput = [stdout, stderr].filter(Boolean).join("\n").trim();
|
|
124
|
+
const ok = !result.error && result.status === 0;
|
|
125
|
+
|
|
126
|
+
return {
|
|
127
|
+
ok,
|
|
128
|
+
status: result.status,
|
|
129
|
+
stdout,
|
|
130
|
+
stderr,
|
|
131
|
+
combinedOutput,
|
|
132
|
+
error: result.error,
|
|
133
|
+
};
|
|
91
134
|
}
|
|
92
135
|
|
|
93
136
|
async function ensureCodexLogin() {
|
|
@@ -173,6 +216,101 @@ async function ensureCodexLogin() {
|
|
|
173
216
|
console.log("Codex login configured.");
|
|
174
217
|
}
|
|
175
218
|
|
|
219
|
+
async function maybeOfferServiceInstall() {
|
|
220
|
+
if (!process.stdin.isTTY) {
|
|
221
|
+
return;
|
|
222
|
+
}
|
|
223
|
+
if (!isServiceSupportedOnCurrentPlatform()) {
|
|
224
|
+
return;
|
|
225
|
+
}
|
|
226
|
+
if (isServiceAlreadyInstalled()) {
|
|
227
|
+
return;
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
const state = readServicePromptState();
|
|
231
|
+
if (state?.decision === "declined") {
|
|
232
|
+
return;
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
const rl = createInterface({ input, output });
|
|
236
|
+
let shouldInstall = false;
|
|
237
|
+
try {
|
|
238
|
+
shouldInstall = await askYesNo(
|
|
239
|
+
rl,
|
|
240
|
+
"Enable OS-native auto-start service now? (recommended for reliability)",
|
|
241
|
+
false,
|
|
242
|
+
);
|
|
243
|
+
} finally {
|
|
244
|
+
rl.close();
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
if (!shouldInstall) {
|
|
248
|
+
writeServicePromptState("declined");
|
|
249
|
+
console.log("Service setup skipped. You can run 'copilot-hub service install' anytime.");
|
|
250
|
+
return;
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
const install = runNodeCapture(["scripts/dist/service.mjs", "install"], "inherit");
|
|
254
|
+
if (!install.ok) {
|
|
255
|
+
console.log("Service install failed. Continuing in local mode.");
|
|
256
|
+
return;
|
|
257
|
+
}
|
|
258
|
+
writeServicePromptState("accepted");
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
function isServiceSupportedOnCurrentPlatform() {
|
|
262
|
+
return (
|
|
263
|
+
process.platform === "win32" || process.platform === "linux" || process.platform === "darwin"
|
|
264
|
+
);
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
function isServiceAlreadyInstalled() {
|
|
268
|
+
const status = runNodeCapture(["scripts/dist/service.mjs", "status"], "pipe");
|
|
269
|
+
const message = String(status.combinedOutput ?? "").toLowerCase();
|
|
270
|
+
if (message.includes("service not installed")) {
|
|
271
|
+
return false;
|
|
272
|
+
}
|
|
273
|
+
if (message.includes("not installed")) {
|
|
274
|
+
return false;
|
|
275
|
+
}
|
|
276
|
+
return status.ok;
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
function readServicePromptState() {
|
|
280
|
+
if (!fs.existsSync(servicePromptStatePath)) {
|
|
281
|
+
return null;
|
|
282
|
+
}
|
|
283
|
+
try {
|
|
284
|
+
const raw = fs.readFileSync(servicePromptStatePath, "utf8");
|
|
285
|
+
const parsed = JSON.parse(raw);
|
|
286
|
+
return parsed && typeof parsed === "object" ? parsed : null;
|
|
287
|
+
} catch {
|
|
288
|
+
return null;
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
function writeServicePromptState(decision) {
|
|
293
|
+
try {
|
|
294
|
+
fs.mkdirSync(runtimeDir, { recursive: true });
|
|
295
|
+
fs.writeFileSync(
|
|
296
|
+
servicePromptStatePath,
|
|
297
|
+
`${JSON.stringify(
|
|
298
|
+
{
|
|
299
|
+
decision: String(decision ?? "")
|
|
300
|
+
.trim()
|
|
301
|
+
.toLowerCase(),
|
|
302
|
+
updatedAt: new Date().toISOString(),
|
|
303
|
+
},
|
|
304
|
+
null,
|
|
305
|
+
2,
|
|
306
|
+
)}\n`,
|
|
307
|
+
"utf8",
|
|
308
|
+
);
|
|
309
|
+
} catch {
|
|
310
|
+
// Non-critical state cache only.
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
|
|
176
314
|
async function recoverCodexBinary({ resolved, status }) {
|
|
177
315
|
const detected = findDetectedCodexBin();
|
|
178
316
|
if (detected && detected !== resolved.bin) {
|
|
@@ -553,7 +691,11 @@ function spawnNpm(args, options) {
|
|
|
553
691
|
|
|
554
692
|
function printUsage() {
|
|
555
693
|
console.log(
|
|
556
|
-
|
|
694
|
+
[
|
|
695
|
+
"Usage: node scripts/dist/cli.mjs <start|stop|restart|status|logs|configure|service|version|help>",
|
|
696
|
+
"Service management:",
|
|
697
|
+
" node scripts/dist/cli.mjs service <install|uninstall|status|start|stop|help>",
|
|
698
|
+
].join("\n"),
|
|
557
699
|
);
|
|
558
700
|
}
|
|
559
701
|
|
|
@@ -0,0 +1,458 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import fs from "node:fs";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import process from "node:process";
|
|
5
|
+
import { spawn, spawnSync } from "node:child_process";
|
|
6
|
+
import { fileURLToPath } from "node:url";
|
|
7
|
+
|
|
8
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
9
|
+
const __dirname = path.dirname(__filename);
|
|
10
|
+
const repoRoot = path.resolve(__dirname, "..", "..");
|
|
11
|
+
|
|
12
|
+
const runtimeDir = path.join(repoRoot, ".copilot-hub");
|
|
13
|
+
const pidsDir = path.join(runtimeDir, "pids");
|
|
14
|
+
const logsDir = path.join(repoRoot, "logs");
|
|
15
|
+
|
|
16
|
+
const daemonStatePath = path.join(pidsDir, "daemon.json");
|
|
17
|
+
const daemonLogPath = path.join(logsDir, "service-daemon.log");
|
|
18
|
+
const daemonScriptPath = path.join(repoRoot, "scripts", "dist", "daemon.mjs");
|
|
19
|
+
const supervisorScriptPath = path.join(repoRoot, "scripts", "dist", "supervisor.mjs");
|
|
20
|
+
const nodeBin = process.execPath;
|
|
21
|
+
|
|
22
|
+
const BASE_CHECK_MS = 5000;
|
|
23
|
+
const MAX_BACKOFF_MS = 60000;
|
|
24
|
+
|
|
25
|
+
const action = String(process.argv[2] ?? "status")
|
|
26
|
+
.trim()
|
|
27
|
+
.toLowerCase();
|
|
28
|
+
|
|
29
|
+
try {
|
|
30
|
+
await main();
|
|
31
|
+
} catch (error) {
|
|
32
|
+
console.error(getErrorMessage(error));
|
|
33
|
+
process.exit(1);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
async function main() {
|
|
37
|
+
switch (action) {
|
|
38
|
+
case "start":
|
|
39
|
+
await startDaemonProcess();
|
|
40
|
+
return;
|
|
41
|
+
case "run":
|
|
42
|
+
await runDaemonLoop();
|
|
43
|
+
return;
|
|
44
|
+
case "stop":
|
|
45
|
+
await stopDaemonProcess();
|
|
46
|
+
return;
|
|
47
|
+
case "status":
|
|
48
|
+
showDaemonStatus();
|
|
49
|
+
return;
|
|
50
|
+
case "help":
|
|
51
|
+
printUsage();
|
|
52
|
+
return;
|
|
53
|
+
default:
|
|
54
|
+
printUsage();
|
|
55
|
+
process.exit(1);
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
async function startDaemonProcess() {
|
|
60
|
+
ensureScripts();
|
|
61
|
+
ensureRuntimeDirs();
|
|
62
|
+
|
|
63
|
+
const existingPid = getRunningDaemonPid();
|
|
64
|
+
if (existingPid > 0) {
|
|
65
|
+
console.log(`[daemon] already running (pid ${existingPid})`);
|
|
66
|
+
return;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
removeDaemonState();
|
|
70
|
+
|
|
71
|
+
const logFd = fs.openSync(daemonLogPath, "a");
|
|
72
|
+
let child;
|
|
73
|
+
try {
|
|
74
|
+
child = spawn(nodeBin, [daemonScriptPath, "run"], {
|
|
75
|
+
cwd: repoRoot,
|
|
76
|
+
detached: true,
|
|
77
|
+
stdio: ["ignore", logFd, logFd],
|
|
78
|
+
windowsHide: true,
|
|
79
|
+
shell: false,
|
|
80
|
+
env: process.env,
|
|
81
|
+
});
|
|
82
|
+
} finally {
|
|
83
|
+
fs.closeSync(logFd);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
const pid = normalizePid(child?.pid);
|
|
87
|
+
if (pid <= 0) {
|
|
88
|
+
throw new Error("Failed to spawn daemon process.");
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
child.unref();
|
|
92
|
+
const ready = await waitForExit(pid, 250, false);
|
|
93
|
+
if (ready) {
|
|
94
|
+
throw new Error(`Daemon process exited immediately (pid ${pid}). Check logs: ${daemonLogPath}`);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
console.log(`[daemon] started (pid ${pid})`);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
async function runDaemonLoop() {
|
|
101
|
+
ensureScripts();
|
|
102
|
+
ensureRuntimeDirs();
|
|
103
|
+
|
|
104
|
+
const existingPid = getRunningDaemonPid();
|
|
105
|
+
if (existingPid > 0 && existingPid !== process.pid) {
|
|
106
|
+
console.log(`[daemon] already running (pid ${existingPid})`);
|
|
107
|
+
return;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
writeDaemonState({
|
|
111
|
+
pid: process.pid,
|
|
112
|
+
startedAt: new Date().toISOString(),
|
|
113
|
+
command: `${nodeBin} ${daemonScriptPath} run`,
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
const state = { stopping: false, shuttingDown: false };
|
|
117
|
+
setupSignalHandlers(state);
|
|
118
|
+
|
|
119
|
+
console.log(`[daemon] running (pid ${process.pid})`);
|
|
120
|
+
|
|
121
|
+
let failureCount = 0;
|
|
122
|
+
|
|
123
|
+
while (!state.stopping) {
|
|
124
|
+
const ensureResult = runSupervisor("ensure", { allowFailure: true });
|
|
125
|
+
if (ensureResult.ok) {
|
|
126
|
+
if (failureCount > 0) {
|
|
127
|
+
console.log("[daemon] workers recovered.");
|
|
128
|
+
}
|
|
129
|
+
failureCount = 0;
|
|
130
|
+
await sleepInterruptible(BASE_CHECK_MS, () => state.stopping);
|
|
131
|
+
continue;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
failureCount += 1;
|
|
135
|
+
const delay = computeBackoffDelay(failureCount);
|
|
136
|
+
const reason =
|
|
137
|
+
firstLine(ensureResult.combinedOutput) ||
|
|
138
|
+
`supervisor ensure exited with code ${String(ensureResult.status ?? "unknown")}`;
|
|
139
|
+
console.error(
|
|
140
|
+
`[daemon] worker health check failed: ${reason}. Retrying in ${Math.ceil(delay / 1000)}s.`,
|
|
141
|
+
);
|
|
142
|
+
await sleepInterruptible(delay, () => state.stopping);
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
await shutdownDaemon(state, { reason: "stop-request", exitCode: 0 });
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
async function stopDaemonProcess() {
|
|
149
|
+
ensureRuntimeDirs();
|
|
150
|
+
|
|
151
|
+
const pid = getRunningDaemonPid();
|
|
152
|
+
if (pid <= 0) {
|
|
153
|
+
removeDaemonState();
|
|
154
|
+
runSupervisor("down", { allowFailure: true });
|
|
155
|
+
console.log("[daemon] not running.");
|
|
156
|
+
return;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
await terminateProcess(pid);
|
|
160
|
+
if (isProcessRunning(pid)) {
|
|
161
|
+
throw new Error(`Daemon did not stop cleanly (pid ${pid}).`);
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
removeDaemonState();
|
|
165
|
+
runSupervisor("down", { allowFailure: true });
|
|
166
|
+
console.log("[daemon] stopped.");
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
function showDaemonStatus() {
|
|
170
|
+
ensureRuntimeDirs();
|
|
171
|
+
|
|
172
|
+
const pid = getRunningDaemonPid();
|
|
173
|
+
const running = pid > 0;
|
|
174
|
+
|
|
175
|
+
if (!running) {
|
|
176
|
+
removeDaemonState();
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
console.log("\n=== daemon ===");
|
|
180
|
+
console.log(`running: ${running ? "yes" : "no"}`);
|
|
181
|
+
console.log(`pid: ${running ? String(pid) : "-"}`);
|
|
182
|
+
console.log(`logFile: ${daemonLogPath}`);
|
|
183
|
+
|
|
184
|
+
if (!fs.existsSync(supervisorScriptPath)) {
|
|
185
|
+
console.log("\n(worker status unavailable: supervisor script missing)");
|
|
186
|
+
return;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
console.log("\n=== workers ===");
|
|
190
|
+
runSupervisor("status", { allowFailure: true, stdio: "inherit" });
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
function setupSignalHandlers(state) {
|
|
194
|
+
const requestStop = () => {
|
|
195
|
+
state.stopping = true;
|
|
196
|
+
};
|
|
197
|
+
|
|
198
|
+
process.on("SIGINT", requestStop);
|
|
199
|
+
process.on("SIGTERM", requestStop);
|
|
200
|
+
process.on("SIGHUP", requestStop);
|
|
201
|
+
|
|
202
|
+
process.on("uncaughtException", (error) => {
|
|
203
|
+
if (!state.shuttingDown) {
|
|
204
|
+
console.error(`[daemon] uncaught exception: ${getErrorMessage(error)}`);
|
|
205
|
+
}
|
|
206
|
+
state.stopping = true;
|
|
207
|
+
void shutdownDaemon(state, { reason: "uncaught-exception", exitCode: 1 });
|
|
208
|
+
});
|
|
209
|
+
|
|
210
|
+
process.on("unhandledRejection", (reason) => {
|
|
211
|
+
if (!state.shuttingDown) {
|
|
212
|
+
console.error(`[daemon] unhandled rejection: ${getErrorMessage(reason)}`);
|
|
213
|
+
}
|
|
214
|
+
state.stopping = true;
|
|
215
|
+
void shutdownDaemon(state, { reason: "unhandled-rejection", exitCode: 1 });
|
|
216
|
+
});
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
async function shutdownDaemon(state, { reason, exitCode }) {
|
|
220
|
+
if (state.shuttingDown) {
|
|
221
|
+
return;
|
|
222
|
+
}
|
|
223
|
+
state.shuttingDown = true;
|
|
224
|
+
|
|
225
|
+
console.log(`[daemon] stopping (${reason})...`);
|
|
226
|
+
runSupervisor("down", { allowFailure: true });
|
|
227
|
+
removeDaemonState();
|
|
228
|
+
|
|
229
|
+
process.exit(exitCode);
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
function ensureScripts() {
|
|
233
|
+
if (!fs.existsSync(supervisorScriptPath)) {
|
|
234
|
+
throw new Error(
|
|
235
|
+
[
|
|
236
|
+
"Supervisor script is missing.",
|
|
237
|
+
"Run 'npm run build:scripts' (or reinstall package) and retry.",
|
|
238
|
+
].join("\n"),
|
|
239
|
+
);
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
if (!fs.existsSync(daemonScriptPath)) {
|
|
243
|
+
throw new Error(
|
|
244
|
+
[
|
|
245
|
+
"Daemon script is missing.",
|
|
246
|
+
"Run 'npm run build:scripts' (or reinstall package) and retry.",
|
|
247
|
+
].join("\n"),
|
|
248
|
+
);
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
function ensureRuntimeDirs() {
|
|
253
|
+
fs.mkdirSync(runtimeDir, { recursive: true });
|
|
254
|
+
fs.mkdirSync(pidsDir, { recursive: true });
|
|
255
|
+
fs.mkdirSync(logsDir, { recursive: true });
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
function getRunningDaemonPid() {
|
|
259
|
+
const state = readDaemonState();
|
|
260
|
+
const pid = normalizePid(state?.pid);
|
|
261
|
+
if (pid <= 0) {
|
|
262
|
+
return 0;
|
|
263
|
+
}
|
|
264
|
+
return isProcessRunning(pid) ? pid : 0;
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
function readDaemonState() {
|
|
268
|
+
if (!fs.existsSync(daemonStatePath)) {
|
|
269
|
+
return null;
|
|
270
|
+
}
|
|
271
|
+
try {
|
|
272
|
+
return JSON.parse(fs.readFileSync(daemonStatePath, "utf8"));
|
|
273
|
+
} catch {
|
|
274
|
+
return null;
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
function writeDaemonState(value) {
|
|
279
|
+
fs.mkdirSync(path.dirname(daemonStatePath), { recursive: true });
|
|
280
|
+
fs.writeFileSync(daemonStatePath, `${JSON.stringify(value, null, 2)}\n`, "utf8");
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
function removeDaemonState() {
|
|
284
|
+
if (!fs.existsSync(daemonStatePath)) {
|
|
285
|
+
return;
|
|
286
|
+
}
|
|
287
|
+
fs.rmSync(daemonStatePath, { force: true });
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
function normalizePid(value) {
|
|
291
|
+
const pid = Number.parseInt(String(value ?? ""), 10);
|
|
292
|
+
if (!Number.isFinite(pid) || pid <= 0) {
|
|
293
|
+
return 0;
|
|
294
|
+
}
|
|
295
|
+
return pid;
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
function isProcessRunning(pid) {
|
|
299
|
+
if (!Number.isInteger(pid) || pid <= 0) {
|
|
300
|
+
return false;
|
|
301
|
+
}
|
|
302
|
+
try {
|
|
303
|
+
process.kill(pid, 0);
|
|
304
|
+
return true;
|
|
305
|
+
} catch (error) {
|
|
306
|
+
if (error && typeof error === "object" && "code" in error && error.code === "EPERM") {
|
|
307
|
+
return true;
|
|
308
|
+
}
|
|
309
|
+
return false;
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
async function terminateProcess(pid) {
|
|
314
|
+
if (process.platform === "win32") {
|
|
315
|
+
await killTreeWindows(pid);
|
|
316
|
+
if (!(await waitForExit(pid, 7000))) {
|
|
317
|
+
await killTreeWindows(pid);
|
|
318
|
+
}
|
|
319
|
+
return;
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
sendSignal(pid, "SIGTERM");
|
|
323
|
+
if (await waitForExit(pid, 7000)) {
|
|
324
|
+
return;
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
sendSignal(pid, "SIGKILL");
|
|
328
|
+
await waitForExit(pid, 2000);
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
function killTreeWindows(pid) {
|
|
332
|
+
return new Promise<void>((resolve) => {
|
|
333
|
+
const child = spawn("taskkill", ["/PID", String(pid), "/T", "/F"], {
|
|
334
|
+
stdio: "ignore",
|
|
335
|
+
shell: false,
|
|
336
|
+
windowsHide: true,
|
|
337
|
+
});
|
|
338
|
+
child.once("error", () => resolve());
|
|
339
|
+
child.once("exit", () => resolve());
|
|
340
|
+
});
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
function sendSignal(pid, signal) {
|
|
344
|
+
try {
|
|
345
|
+
process.kill(-pid, signal);
|
|
346
|
+
return;
|
|
347
|
+
} catch {
|
|
348
|
+
// continue
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
try {
|
|
352
|
+
process.kill(pid, signal);
|
|
353
|
+
} catch {
|
|
354
|
+
// ignore
|
|
355
|
+
}
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
async function waitForExit(pid, timeoutMs, expectExit = true) {
|
|
359
|
+
const deadline = Date.now() + timeoutMs;
|
|
360
|
+
while (Date.now() < deadline) {
|
|
361
|
+
const running = isProcessRunning(pid);
|
|
362
|
+
if (expectExit && !running) {
|
|
363
|
+
return true;
|
|
364
|
+
}
|
|
365
|
+
if (!expectExit && running) {
|
|
366
|
+
return false;
|
|
367
|
+
}
|
|
368
|
+
await sleep(100);
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
const stillRunning = isProcessRunning(pid);
|
|
372
|
+
return expectExit ? !stillRunning : stillRunning === false;
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
function runSupervisor(actionValue, { allowFailure = false, stdio = "pipe" } = {}) {
|
|
376
|
+
return runChecked(nodeBin, [supervisorScriptPath, String(actionValue ?? "").trim()], {
|
|
377
|
+
allowFailure,
|
|
378
|
+
stdio,
|
|
379
|
+
});
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
function runChecked(command, args, { stdio = "pipe", allowFailure = false } = {}) {
|
|
383
|
+
const spawnStdio = stdio as "pipe" | "inherit";
|
|
384
|
+
const result = spawnSync(command, args, {
|
|
385
|
+
cwd: repoRoot,
|
|
386
|
+
shell: false,
|
|
387
|
+
stdio: spawnStdio,
|
|
388
|
+
encoding: "utf8",
|
|
389
|
+
env: process.env,
|
|
390
|
+
});
|
|
391
|
+
|
|
392
|
+
const stdout = String(result.stdout ?? "").trim();
|
|
393
|
+
const stderr = String(result.stderr ?? "").trim();
|
|
394
|
+
const combinedOutput = [stdout, stderr].filter(Boolean).join("\n").trim();
|
|
395
|
+
const spawnErrorCode = String((result.error as NodeJS.ErrnoException | undefined)?.code ?? "")
|
|
396
|
+
.trim()
|
|
397
|
+
.toUpperCase();
|
|
398
|
+
const ok = !result.error && result.status === 0;
|
|
399
|
+
|
|
400
|
+
if (!ok && !allowFailure) {
|
|
401
|
+
const errorMessage =
|
|
402
|
+
result.error && spawnErrorCode
|
|
403
|
+
? `${command} failed (${spawnErrorCode}).`
|
|
404
|
+
: combinedOutput || `${command} exited with code ${String(result.status ?? "unknown")}.`;
|
|
405
|
+
throw new Error(errorMessage);
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
return {
|
|
409
|
+
ok,
|
|
410
|
+
status: result.status,
|
|
411
|
+
stdout,
|
|
412
|
+
stderr,
|
|
413
|
+
combinedOutput,
|
|
414
|
+
spawnErrorCode,
|
|
415
|
+
};
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
function computeBackoffDelay(failureCount) {
|
|
419
|
+
const power = Math.max(0, failureCount - 1);
|
|
420
|
+
const calculated = BASE_CHECK_MS * 2 ** power;
|
|
421
|
+
return Math.min(calculated, MAX_BACKOFF_MS);
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
async function sleepInterruptible(ms, shouldStop) {
|
|
425
|
+
const deadline = Date.now() + ms;
|
|
426
|
+
while (Date.now() < deadline) {
|
|
427
|
+
if (shouldStop()) {
|
|
428
|
+
return;
|
|
429
|
+
}
|
|
430
|
+
await sleep(Math.min(250, Math.max(1, deadline - Date.now())));
|
|
431
|
+
}
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
function sleep(ms) {
|
|
435
|
+
return new Promise((resolve) => {
|
|
436
|
+
setTimeout(resolve, ms);
|
|
437
|
+
});
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
function firstLine(value) {
|
|
441
|
+
const text = String(value ?? "").trim();
|
|
442
|
+
if (!text) {
|
|
443
|
+
return "";
|
|
444
|
+
}
|
|
445
|
+
const [line] = text.split(/\r?\n/, 1);
|
|
446
|
+
return String(line ?? "").trim();
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
function getErrorMessage(error) {
|
|
450
|
+
if (error instanceof Error && error.message) {
|
|
451
|
+
return error.message;
|
|
452
|
+
}
|
|
453
|
+
return String(error ?? "Unknown error.");
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
function printUsage() {
|
|
457
|
+
console.log("Usage: node scripts/dist/daemon.mjs <start|run|stop|status|help>");
|
|
458
|
+
}
|