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
|
@@ -0,0 +1,520 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// @ts-nocheck
|
|
3
|
+
import fs from "node:fs";
|
|
4
|
+
import os from "node:os";
|
|
5
|
+
import path from "node:path";
|
|
6
|
+
import process from "node:process";
|
|
7
|
+
import { spawnSync } from "node:child_process";
|
|
8
|
+
import { fileURLToPath } from "node:url";
|
|
9
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
10
|
+
const __dirname = path.dirname(__filename);
|
|
11
|
+
const repoRoot = path.resolve(__dirname, "..", "..");
|
|
12
|
+
const nodeBin = process.execPath;
|
|
13
|
+
const daemonScriptPath = path.join(repoRoot, "scripts", "dist", "daemon.mjs");
|
|
14
|
+
const WINDOWS_TASK_NAME = "CopilotHub";
|
|
15
|
+
const WINDOWS_RUN_KEY_PATH = "HKCU\\Software\\Microsoft\\Windows\\CurrentVersion\\Run";
|
|
16
|
+
const WINDOWS_RUN_VALUE_NAME = "CopilotHub";
|
|
17
|
+
const LINUX_UNIT_NAME = "copilot-hub.service";
|
|
18
|
+
const MACOS_LABEL = "com.copilot-hub.service";
|
|
19
|
+
const action = String(process.argv[2] ?? "status")
|
|
20
|
+
.trim()
|
|
21
|
+
.toLowerCase();
|
|
22
|
+
try {
|
|
23
|
+
await main();
|
|
24
|
+
}
|
|
25
|
+
catch (error) {
|
|
26
|
+
console.error(getErrorMessage(error));
|
|
27
|
+
process.exit(1);
|
|
28
|
+
}
|
|
29
|
+
async function main() {
|
|
30
|
+
switch (action) {
|
|
31
|
+
case "install":
|
|
32
|
+
await installService();
|
|
33
|
+
return;
|
|
34
|
+
case "uninstall":
|
|
35
|
+
await uninstallService();
|
|
36
|
+
return;
|
|
37
|
+
case "status":
|
|
38
|
+
await showStatus();
|
|
39
|
+
return;
|
|
40
|
+
case "start":
|
|
41
|
+
await startService();
|
|
42
|
+
return;
|
|
43
|
+
case "stop":
|
|
44
|
+
await stopService();
|
|
45
|
+
return;
|
|
46
|
+
case "help":
|
|
47
|
+
printUsage();
|
|
48
|
+
return;
|
|
49
|
+
default:
|
|
50
|
+
printUsage();
|
|
51
|
+
process.exit(1);
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
async function installService() {
|
|
55
|
+
ensureDaemonScript();
|
|
56
|
+
if (process.platform === "win32") {
|
|
57
|
+
const mode = installWindowsAutoStart();
|
|
58
|
+
if (mode === "task") {
|
|
59
|
+
console.log("Service installed (Windows Task Scheduler).");
|
|
60
|
+
}
|
|
61
|
+
else {
|
|
62
|
+
console.log("Service installed (Windows startup registry entry).");
|
|
63
|
+
}
|
|
64
|
+
return;
|
|
65
|
+
}
|
|
66
|
+
if (process.platform === "linux") {
|
|
67
|
+
installLinuxService();
|
|
68
|
+
console.log("Service installed (systemd user service).");
|
|
69
|
+
return;
|
|
70
|
+
}
|
|
71
|
+
if (process.platform === "darwin") {
|
|
72
|
+
installMacosService();
|
|
73
|
+
console.log("Service installed (launchd user agent).");
|
|
74
|
+
return;
|
|
75
|
+
}
|
|
76
|
+
throw new Error(`Unsupported platform: ${process.platform}`);
|
|
77
|
+
}
|
|
78
|
+
async function uninstallService() {
|
|
79
|
+
if (process.platform === "win32") {
|
|
80
|
+
const removed = uninstallWindowsAutoStart();
|
|
81
|
+
if (!removed) {
|
|
82
|
+
console.log("Service auto-start is already absent.");
|
|
83
|
+
return;
|
|
84
|
+
}
|
|
85
|
+
console.log("Service uninstalled (Windows auto-start).");
|
|
86
|
+
return;
|
|
87
|
+
}
|
|
88
|
+
if (process.platform === "linux") {
|
|
89
|
+
uninstallLinuxService();
|
|
90
|
+
console.log("Service uninstalled (systemd user service).");
|
|
91
|
+
return;
|
|
92
|
+
}
|
|
93
|
+
if (process.platform === "darwin") {
|
|
94
|
+
uninstallMacosService();
|
|
95
|
+
console.log("Service uninstalled (launchd user agent).");
|
|
96
|
+
return;
|
|
97
|
+
}
|
|
98
|
+
throw new Error(`Unsupported platform: ${process.platform}`);
|
|
99
|
+
}
|
|
100
|
+
async function showStatus() {
|
|
101
|
+
if (process.platform === "win32") {
|
|
102
|
+
showWindowsAutoStartStatus();
|
|
103
|
+
return;
|
|
104
|
+
}
|
|
105
|
+
if (process.platform === "linux") {
|
|
106
|
+
showLinuxServiceStatus();
|
|
107
|
+
return;
|
|
108
|
+
}
|
|
109
|
+
if (process.platform === "darwin") {
|
|
110
|
+
showMacosServiceStatus();
|
|
111
|
+
return;
|
|
112
|
+
}
|
|
113
|
+
throw new Error(`Unsupported platform: ${process.platform}`);
|
|
114
|
+
}
|
|
115
|
+
async function startService() {
|
|
116
|
+
if (process.platform === "win32") {
|
|
117
|
+
startWindowsAutoStart();
|
|
118
|
+
return;
|
|
119
|
+
}
|
|
120
|
+
if (process.platform === "linux") {
|
|
121
|
+
ensureSystemctl();
|
|
122
|
+
runChecked("systemctl", ["--user", "start", LINUX_UNIT_NAME], { stdio: "inherit" });
|
|
123
|
+
return;
|
|
124
|
+
}
|
|
125
|
+
if (process.platform === "darwin") {
|
|
126
|
+
startMacosService();
|
|
127
|
+
return;
|
|
128
|
+
}
|
|
129
|
+
throw new Error(`Unsupported platform: ${process.platform}`);
|
|
130
|
+
}
|
|
131
|
+
async function stopService() {
|
|
132
|
+
if (process.platform === "win32") {
|
|
133
|
+
runDaemon("stop");
|
|
134
|
+
return;
|
|
135
|
+
}
|
|
136
|
+
if (process.platform === "linux") {
|
|
137
|
+
ensureSystemctl();
|
|
138
|
+
runChecked("systemctl", ["--user", "stop", LINUX_UNIT_NAME], { stdio: "inherit" });
|
|
139
|
+
return;
|
|
140
|
+
}
|
|
141
|
+
if (process.platform === "darwin") {
|
|
142
|
+
stopMacosService();
|
|
143
|
+
return;
|
|
144
|
+
}
|
|
145
|
+
throw new Error(`Unsupported platform: ${process.platform}`);
|
|
146
|
+
}
|
|
147
|
+
function installWindowsAutoStart() {
|
|
148
|
+
ensureCommandAvailable("schtasks", ["/?"], "Windows Task Scheduler is not available.");
|
|
149
|
+
ensureCommandAvailable("reg", ["query", WINDOWS_RUN_KEY_PATH], "Windows registry tools are not available.");
|
|
150
|
+
const command = buildWindowsLaunchCommand();
|
|
151
|
+
const taskCreate = runChecked("schtasks", ["/Create", "/TN", WINDOWS_TASK_NAME, "/SC", "ONLOGON", "/RL", "LIMITED", "/F", "/TR", command], { allowFailure: true });
|
|
152
|
+
if (taskCreate.ok) {
|
|
153
|
+
runWindowsTask();
|
|
154
|
+
return "task";
|
|
155
|
+
}
|
|
156
|
+
if (!isAccessDeniedMessage(taskCreate.combinedOutput)) {
|
|
157
|
+
throw new Error(taskCreate.combinedOutput || "Failed to create Windows auto-start task.");
|
|
158
|
+
}
|
|
159
|
+
installWindowsRunKey(command);
|
|
160
|
+
runDaemon("start");
|
|
161
|
+
return "run-key";
|
|
162
|
+
}
|
|
163
|
+
function uninstallWindowsAutoStart() {
|
|
164
|
+
ensureCommandAvailable("schtasks", ["/?"], "Windows Task Scheduler is not available.");
|
|
165
|
+
ensureCommandAvailable("reg", ["query", WINDOWS_RUN_KEY_PATH], "Windows registry tools are not available.");
|
|
166
|
+
runDaemon("stop", { allowFailure: true });
|
|
167
|
+
let removed = false;
|
|
168
|
+
const taskDelete = runChecked("schtasks", ["/Delete", "/TN", WINDOWS_TASK_NAME, "/F"], {
|
|
169
|
+
allowFailure: true,
|
|
170
|
+
});
|
|
171
|
+
if (taskDelete.ok) {
|
|
172
|
+
removed = true;
|
|
173
|
+
}
|
|
174
|
+
else if (!isNotFoundMessage(taskDelete.combinedOutput) &&
|
|
175
|
+
!isAccessDeniedMessage(taskDelete.combinedOutput)) {
|
|
176
|
+
throw new Error(taskDelete.combinedOutput || "Failed to remove Windows Task Scheduler entry.");
|
|
177
|
+
}
|
|
178
|
+
const runKeyDelete = runChecked("reg", ["delete", WINDOWS_RUN_KEY_PATH, "/v", WINDOWS_RUN_VALUE_NAME, "/f"], { allowFailure: true });
|
|
179
|
+
if (runKeyDelete.ok) {
|
|
180
|
+
removed = true;
|
|
181
|
+
}
|
|
182
|
+
else if (!isRegistryValueNotFoundMessage(runKeyDelete.combinedOutput)) {
|
|
183
|
+
throw new Error(runKeyDelete.combinedOutput || "Failed to remove Windows startup registry entry.");
|
|
184
|
+
}
|
|
185
|
+
return removed;
|
|
186
|
+
}
|
|
187
|
+
function showWindowsAutoStartStatus() {
|
|
188
|
+
ensureCommandAvailable("schtasks", ["/?"], "Windows Task Scheduler is not available.");
|
|
189
|
+
ensureCommandAvailable("reg", ["query", WINDOWS_RUN_KEY_PATH], "Windows registry tools are not available.");
|
|
190
|
+
const runKey = queryWindowsRunKey();
|
|
191
|
+
if (runKey.installed) {
|
|
192
|
+
console.log("Service installed (Windows startup registry entry).");
|
|
193
|
+
return;
|
|
194
|
+
}
|
|
195
|
+
const result = runChecked("schtasks", ["/Query", "/TN", WINDOWS_TASK_NAME, "/FO", "LIST", "/V"], {
|
|
196
|
+
allowFailure: true,
|
|
197
|
+
});
|
|
198
|
+
if (!result.ok &&
|
|
199
|
+
(isNotFoundMessage(result.combinedOutput) || isAccessDeniedMessage(result.combinedOutput))) {
|
|
200
|
+
console.log("Service not installed.");
|
|
201
|
+
return;
|
|
202
|
+
}
|
|
203
|
+
if (!result.ok) {
|
|
204
|
+
throw new Error(result.combinedOutput || "Failed to query service task.");
|
|
205
|
+
}
|
|
206
|
+
console.log("Service installed (Windows Task Scheduler).");
|
|
207
|
+
}
|
|
208
|
+
function runWindowsTask() {
|
|
209
|
+
ensureCommandAvailable("schtasks", ["/?"], "Windows Task Scheduler is not available.");
|
|
210
|
+
const result = runChecked("schtasks", ["/Run", "/TN", WINDOWS_TASK_NAME], { allowFailure: true });
|
|
211
|
+
if (!result.ok && isNotFoundMessage(result.combinedOutput)) {
|
|
212
|
+
throw new Error("Service is not installed. Run 'copilot-hub service install' first.");
|
|
213
|
+
}
|
|
214
|
+
if (!result.ok) {
|
|
215
|
+
throw new Error(result.combinedOutput || "Failed to run service task.");
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
function startWindowsAutoStart() {
|
|
219
|
+
const runKey = queryWindowsRunKey();
|
|
220
|
+
if (runKey.installed) {
|
|
221
|
+
runDaemon("start");
|
|
222
|
+
return;
|
|
223
|
+
}
|
|
224
|
+
runWindowsTask();
|
|
225
|
+
}
|
|
226
|
+
function queryWindowsRunKey() {
|
|
227
|
+
const result = runChecked("reg", ["query", WINDOWS_RUN_KEY_PATH, "/v", WINDOWS_RUN_VALUE_NAME], {
|
|
228
|
+
allowFailure: true,
|
|
229
|
+
});
|
|
230
|
+
if (result.ok) {
|
|
231
|
+
return { installed: true };
|
|
232
|
+
}
|
|
233
|
+
if (isRegistryValueNotFoundMessage(result.combinedOutput) || result.status === 1) {
|
|
234
|
+
return { installed: false };
|
|
235
|
+
}
|
|
236
|
+
throw new Error(result.combinedOutput || "Failed to query Windows startup registry entry.");
|
|
237
|
+
}
|
|
238
|
+
function installWindowsRunKey(command) {
|
|
239
|
+
runChecked("reg", [
|
|
240
|
+
"add",
|
|
241
|
+
WINDOWS_RUN_KEY_PATH,
|
|
242
|
+
"/v",
|
|
243
|
+
WINDOWS_RUN_VALUE_NAME,
|
|
244
|
+
"/t",
|
|
245
|
+
"REG_SZ",
|
|
246
|
+
"/d",
|
|
247
|
+
command,
|
|
248
|
+
"/f",
|
|
249
|
+
], { stdio: "pipe" });
|
|
250
|
+
}
|
|
251
|
+
function installLinuxService() {
|
|
252
|
+
ensureSystemctl();
|
|
253
|
+
const unitPath = getLinuxUnitPath();
|
|
254
|
+
fs.mkdirSync(path.dirname(unitPath), { recursive: true });
|
|
255
|
+
fs.writeFileSync(unitPath, buildLinuxUnitContent(), "utf8");
|
|
256
|
+
runChecked("systemctl", ["--user", "daemon-reload"], { stdio: "inherit" });
|
|
257
|
+
runChecked("systemctl", ["--user", "enable", "--now", LINUX_UNIT_NAME], { stdio: "inherit" });
|
|
258
|
+
}
|
|
259
|
+
function uninstallLinuxService() {
|
|
260
|
+
ensureSystemctl();
|
|
261
|
+
runChecked("systemctl", ["--user", "disable", "--now", LINUX_UNIT_NAME], {
|
|
262
|
+
allowFailure: true,
|
|
263
|
+
stdio: "inherit",
|
|
264
|
+
});
|
|
265
|
+
const unitPath = getLinuxUnitPath();
|
|
266
|
+
if (fs.existsSync(unitPath)) {
|
|
267
|
+
fs.rmSync(unitPath, { force: true });
|
|
268
|
+
}
|
|
269
|
+
runChecked("systemctl", ["--user", "daemon-reload"], { stdio: "inherit" });
|
|
270
|
+
}
|
|
271
|
+
function showLinuxServiceStatus() {
|
|
272
|
+
ensureSystemctl();
|
|
273
|
+
const unitPath = getLinuxUnitPath();
|
|
274
|
+
const result = runChecked("systemctl", ["--user", "status", LINUX_UNIT_NAME, "--no-pager", "--lines=40"], { allowFailure: true });
|
|
275
|
+
if (!result.ok && !fs.existsSync(unitPath)) {
|
|
276
|
+
console.log("Service not installed.");
|
|
277
|
+
return;
|
|
278
|
+
}
|
|
279
|
+
console.log((result.stdout || result.stderr || "No status output.").trim());
|
|
280
|
+
}
|
|
281
|
+
function installMacosService() {
|
|
282
|
+
ensureCommandAvailable("launchctl", ["help"], "launchctl is not available.");
|
|
283
|
+
const plistPath = getMacosPlistPath();
|
|
284
|
+
fs.mkdirSync(path.dirname(plistPath), { recursive: true });
|
|
285
|
+
fs.mkdirSync(path.join(repoRoot, "logs"), { recursive: true });
|
|
286
|
+
fs.writeFileSync(plistPath, buildMacosPlist(), "utf8");
|
|
287
|
+
stopMacosService({ allowFailure: true });
|
|
288
|
+
const target = getMacosLaunchTarget();
|
|
289
|
+
runChecked("launchctl", ["bootstrap", target, plistPath], { stdio: "inherit" });
|
|
290
|
+
runChecked("launchctl", ["kickstart", "-k", `${target}/${MACOS_LABEL}`], { stdio: "inherit" });
|
|
291
|
+
}
|
|
292
|
+
function uninstallMacosService() {
|
|
293
|
+
ensureCommandAvailable("launchctl", ["help"], "launchctl is not available.");
|
|
294
|
+
stopMacosService({ allowFailure: true });
|
|
295
|
+
const plistPath = getMacosPlistPath();
|
|
296
|
+
if (fs.existsSync(plistPath)) {
|
|
297
|
+
fs.rmSync(plistPath, { force: true });
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
function showMacosServiceStatus() {
|
|
301
|
+
ensureCommandAvailable("launchctl", ["help"], "launchctl is not available.");
|
|
302
|
+
const target = getMacosLaunchTarget();
|
|
303
|
+
const label = `${target}/${MACOS_LABEL}`;
|
|
304
|
+
const result = runChecked("launchctl", ["print", label], { allowFailure: true });
|
|
305
|
+
if (!result.ok && !fs.existsSync(getMacosPlistPath())) {
|
|
306
|
+
console.log("Service not installed.");
|
|
307
|
+
return;
|
|
308
|
+
}
|
|
309
|
+
console.log((result.stdout || result.stderr || "No status output.").trim());
|
|
310
|
+
}
|
|
311
|
+
function startMacosService() {
|
|
312
|
+
ensureCommandAvailable("launchctl", ["help"], "launchctl is not available.");
|
|
313
|
+
const plistPath = getMacosPlistPath();
|
|
314
|
+
if (!fs.existsSync(plistPath)) {
|
|
315
|
+
throw new Error("Service is not installed. Run 'copilot-hub service install' first.");
|
|
316
|
+
}
|
|
317
|
+
const target = getMacosLaunchTarget();
|
|
318
|
+
const label = `${target}/${MACOS_LABEL}`;
|
|
319
|
+
const kickstart = runChecked("launchctl", ["kickstart", "-k", label], { allowFailure: true });
|
|
320
|
+
if (kickstart.ok) {
|
|
321
|
+
return;
|
|
322
|
+
}
|
|
323
|
+
runChecked("launchctl", ["bootstrap", target, plistPath], { stdio: "inherit" });
|
|
324
|
+
runChecked("launchctl", ["kickstart", "-k", label], { stdio: "inherit" });
|
|
325
|
+
}
|
|
326
|
+
function stopMacosService({ allowFailure = false } = {}) {
|
|
327
|
+
ensureCommandAvailable("launchctl", ["help"], "launchctl is not available.");
|
|
328
|
+
const target = getMacosLaunchTarget();
|
|
329
|
+
runChecked("launchctl", ["bootout", target, getMacosPlistPath()], {
|
|
330
|
+
allowFailure,
|
|
331
|
+
stdio: "inherit",
|
|
332
|
+
});
|
|
333
|
+
}
|
|
334
|
+
function getLinuxUnitPath() {
|
|
335
|
+
return path.join(os.homedir(), ".config", "systemd", "user", LINUX_UNIT_NAME);
|
|
336
|
+
}
|
|
337
|
+
function getMacosPlistPath() {
|
|
338
|
+
return path.join(os.homedir(), "Library", "LaunchAgents", `${MACOS_LABEL}.plist`);
|
|
339
|
+
}
|
|
340
|
+
function getMacosLaunchTarget() {
|
|
341
|
+
if (typeof process.getuid !== "function") {
|
|
342
|
+
throw new Error("Could not resolve macOS user id.");
|
|
343
|
+
}
|
|
344
|
+
return `gui/${process.getuid()}`;
|
|
345
|
+
}
|
|
346
|
+
function buildLinuxUnitContent() {
|
|
347
|
+
return [
|
|
348
|
+
"[Unit]",
|
|
349
|
+
"Description=Copilot Hub Service",
|
|
350
|
+
"After=network-online.target",
|
|
351
|
+
"",
|
|
352
|
+
"[Service]",
|
|
353
|
+
"Type=simple",
|
|
354
|
+
`WorkingDirectory=${repoRoot}`,
|
|
355
|
+
`ExecStart="${nodeBin}" "${daemonScriptPath}" run`,
|
|
356
|
+
`ExecStop="${nodeBin}" "${daemonScriptPath}" stop`,
|
|
357
|
+
"Restart=always",
|
|
358
|
+
"RestartSec=3",
|
|
359
|
+
"KillMode=process",
|
|
360
|
+
"",
|
|
361
|
+
"[Install]",
|
|
362
|
+
"WantedBy=default.target",
|
|
363
|
+
"",
|
|
364
|
+
].join("\n");
|
|
365
|
+
}
|
|
366
|
+
function buildMacosPlist() {
|
|
367
|
+
const stdoutPath = path.join(repoRoot, "logs", "service-launchd.log");
|
|
368
|
+
const stderrPath = path.join(repoRoot, "logs", "service-launchd.error.log");
|
|
369
|
+
const values = {
|
|
370
|
+
label: escapeXml(MACOS_LABEL),
|
|
371
|
+
node: escapeXml(nodeBin),
|
|
372
|
+
script: escapeXml(daemonScriptPath),
|
|
373
|
+
cwd: escapeXml(repoRoot),
|
|
374
|
+
stdoutPath: escapeXml(stdoutPath),
|
|
375
|
+
stderrPath: escapeXml(stderrPath),
|
|
376
|
+
};
|
|
377
|
+
return [
|
|
378
|
+
'<?xml version="1.0" encoding="UTF-8"?>',
|
|
379
|
+
'<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">',
|
|
380
|
+
'<plist version="1.0">',
|
|
381
|
+
"<dict>",
|
|
382
|
+
" <key>Label</key>",
|
|
383
|
+
` <string>${values.label}</string>`,
|
|
384
|
+
" <key>ProgramArguments</key>",
|
|
385
|
+
" <array>",
|
|
386
|
+
` <string>${values.node}</string>`,
|
|
387
|
+
` <string>${values.script}</string>`,
|
|
388
|
+
" <string>run</string>",
|
|
389
|
+
" </array>",
|
|
390
|
+
" <key>WorkingDirectory</key>",
|
|
391
|
+
` <string>${values.cwd}</string>`,
|
|
392
|
+
" <key>RunAtLoad</key>",
|
|
393
|
+
" <true/>",
|
|
394
|
+
" <key>KeepAlive</key>",
|
|
395
|
+
" <true/>",
|
|
396
|
+
" <key>StandardOutPath</key>",
|
|
397
|
+
` <string>${values.stdoutPath}</string>`,
|
|
398
|
+
" <key>StandardErrorPath</key>",
|
|
399
|
+
` <string>${values.stderrPath}</string>`,
|
|
400
|
+
"</dict>",
|
|
401
|
+
"</plist>",
|
|
402
|
+
"",
|
|
403
|
+
].join("\n");
|
|
404
|
+
}
|
|
405
|
+
function ensureDaemonScript() {
|
|
406
|
+
if (!fs.existsSync(daemonScriptPath)) {
|
|
407
|
+
throw new Error([
|
|
408
|
+
"Daemon script is missing.",
|
|
409
|
+
"Run 'npm run build:scripts' (or reinstall package) and retry.",
|
|
410
|
+
].join("\n"));
|
|
411
|
+
}
|
|
412
|
+
}
|
|
413
|
+
function ensureSystemctl() {
|
|
414
|
+
ensureCommandAvailable("systemctl", ["--version"], "systemd is not available. This command requires Linux with systemd user services.");
|
|
415
|
+
}
|
|
416
|
+
function ensureCommandAvailable(command, args, errorMessage) {
|
|
417
|
+
const probe = runChecked(command, args, { allowFailure: true });
|
|
418
|
+
if (!probe.spawnErrorCode || probe.spawnErrorCode !== "ENOENT") {
|
|
419
|
+
return;
|
|
420
|
+
}
|
|
421
|
+
throw new Error(errorMessage);
|
|
422
|
+
}
|
|
423
|
+
function runDaemon(actionValue, { allowFailure = false } = {}) {
|
|
424
|
+
const result = runChecked(nodeBin, [daemonScriptPath, String(actionValue ?? "").trim()], {
|
|
425
|
+
stdio: "inherit",
|
|
426
|
+
allowFailure,
|
|
427
|
+
});
|
|
428
|
+
if (!result.ok && !allowFailure) {
|
|
429
|
+
throw new Error(result.combinedOutput || `Failed to execute daemon action '${actionValue}'.`);
|
|
430
|
+
}
|
|
431
|
+
}
|
|
432
|
+
function runChecked(command, args, { stdio = "pipe", allowFailure = false } = {}) {
|
|
433
|
+
const result = spawnSync(command, args, {
|
|
434
|
+
cwd: repoRoot,
|
|
435
|
+
shell: false,
|
|
436
|
+
stdio,
|
|
437
|
+
encoding: "utf8",
|
|
438
|
+
env: process.env,
|
|
439
|
+
});
|
|
440
|
+
const stdout = String(result.stdout ?? "").trim();
|
|
441
|
+
const stderr = String(result.stderr ?? "").trim();
|
|
442
|
+
const combinedOutput = [stdout, stderr].filter(Boolean).join("\n").trim();
|
|
443
|
+
const spawnErrorCode = String(result.error?.code ?? "")
|
|
444
|
+
.trim()
|
|
445
|
+
.toUpperCase();
|
|
446
|
+
const ok = !result.error && result.status === 0;
|
|
447
|
+
if (!ok && !allowFailure) {
|
|
448
|
+
const errorMessage = result.error && spawnErrorCode
|
|
449
|
+
? `${command} failed (${spawnErrorCode}).`
|
|
450
|
+
: combinedOutput || `${command} exited with code ${String(result.status ?? "unknown")}.`;
|
|
451
|
+
throw new Error(errorMessage);
|
|
452
|
+
}
|
|
453
|
+
return {
|
|
454
|
+
ok,
|
|
455
|
+
status: result.status,
|
|
456
|
+
stdout,
|
|
457
|
+
stderr,
|
|
458
|
+
combinedOutput,
|
|
459
|
+
spawnErrorCode,
|
|
460
|
+
};
|
|
461
|
+
}
|
|
462
|
+
function isNotFoundMessage(value) {
|
|
463
|
+
const message = String(value ?? "").toLowerCase();
|
|
464
|
+
if (!message) {
|
|
465
|
+
return false;
|
|
466
|
+
}
|
|
467
|
+
return (message.includes("cannot find") ||
|
|
468
|
+
message.includes("cannot be found") ||
|
|
469
|
+
message.includes("not found") ||
|
|
470
|
+
message.includes("introuvable") ||
|
|
471
|
+
message.includes("n'existe pas"));
|
|
472
|
+
}
|
|
473
|
+
function isAccessDeniedMessage(value) {
|
|
474
|
+
const message = String(value ?? "").toLowerCase();
|
|
475
|
+
if (!message) {
|
|
476
|
+
return false;
|
|
477
|
+
}
|
|
478
|
+
return message.includes("access is denied") || message.includes("accès refusé");
|
|
479
|
+
}
|
|
480
|
+
function isRegistryValueNotFoundMessage(value) {
|
|
481
|
+
const message = String(value ?? "").toLowerCase();
|
|
482
|
+
if (!message) {
|
|
483
|
+
return false;
|
|
484
|
+
}
|
|
485
|
+
return (message.includes("unable to find the specified registry key or value") ||
|
|
486
|
+
message.includes("the system was unable to find the specified registry key or value") ||
|
|
487
|
+
message.includes("impossible de trouver") ||
|
|
488
|
+
message.includes("n'a pas trouvé") ||
|
|
489
|
+
message.includes("n’a pas trouvé") ||
|
|
490
|
+
message.includes("n a pas trouvé") ||
|
|
491
|
+
message.includes("la clé ou la valeur de registre spécifiée") ||
|
|
492
|
+
message.includes("introuvable"));
|
|
493
|
+
}
|
|
494
|
+
function getErrorMessage(error) {
|
|
495
|
+
if (error instanceof Error && error.message) {
|
|
496
|
+
return error.message;
|
|
497
|
+
}
|
|
498
|
+
return String(error ?? "Unknown error.");
|
|
499
|
+
}
|
|
500
|
+
function buildWindowsLaunchCommand() {
|
|
501
|
+
return `"${nodeBin}" "${daemonScriptPath}" run`;
|
|
502
|
+
}
|
|
503
|
+
function escapeXml(value) {
|
|
504
|
+
return String(value ?? "")
|
|
505
|
+
.replace(/&/g, "&")
|
|
506
|
+
.replace(/</g, "<")
|
|
507
|
+
.replace(/>/g, ">")
|
|
508
|
+
.replace(/"/g, """)
|
|
509
|
+
.replace(/'/g, "'");
|
|
510
|
+
}
|
|
511
|
+
function printUsage() {
|
|
512
|
+
console.log([
|
|
513
|
+
"Usage: node scripts/dist/service.mjs <install|uninstall|status|start|stop|help>",
|
|
514
|
+
"",
|
|
515
|
+
"Platform mapping:",
|
|
516
|
+
"- Windows: Task Scheduler task (fallback: user startup registry entry)",
|
|
517
|
+
"- Linux: systemd user service",
|
|
518
|
+
"- macOS: launchd user agent",
|
|
519
|
+
].join("\n"));
|
|
520
|
+
}
|
|
@@ -36,6 +36,9 @@ async function main() {
|
|
|
36
36
|
case "up":
|
|
37
37
|
await startServices();
|
|
38
38
|
return;
|
|
39
|
+
case "ensure":
|
|
40
|
+
await ensureServices();
|
|
41
|
+
return;
|
|
39
42
|
case "down":
|
|
40
43
|
await stopServices();
|
|
41
44
|
return;
|
|
@@ -69,6 +72,19 @@ async function startServices() {
|
|
|
69
72
|
started.push(service);
|
|
70
73
|
}
|
|
71
74
|
}
|
|
75
|
+
async function ensureServices() {
|
|
76
|
+
ensureRuntimeDirs();
|
|
77
|
+
let hasFailure = false;
|
|
78
|
+
for (const service of SERVICES) {
|
|
79
|
+
const ok = await startService(service, { suppressAlreadyRunning: true });
|
|
80
|
+
if (!ok) {
|
|
81
|
+
hasFailure = true;
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
if (hasFailure) {
|
|
85
|
+
process.exit(1);
|
|
86
|
+
}
|
|
87
|
+
}
|
|
72
88
|
async function stopServices() {
|
|
73
89
|
for (const service of SERVICES) {
|
|
74
90
|
await stopService(service);
|
|
@@ -96,11 +112,14 @@ function showLogs() {
|
|
|
96
112
|
printTail(service.logFile, 120);
|
|
97
113
|
}
|
|
98
114
|
}
|
|
99
|
-
async function startService(service) {
|
|
115
|
+
async function startService(service, options = {}) {
|
|
116
|
+
const suppressAlreadyRunning = options?.suppressAlreadyRunning === true;
|
|
100
117
|
const existing = readState(service);
|
|
101
118
|
const existingPid = normalizePid(existing?.pid);
|
|
102
119
|
if (existingPid > 0 && isProcessRunning(existingPid)) {
|
|
103
|
-
|
|
120
|
+
if (!suppressAlreadyRunning) {
|
|
121
|
+
console.log(`[${service.id}] already running (pid ${existingPid})`);
|
|
122
|
+
}
|
|
104
123
|
return true;
|
|
105
124
|
}
|
|
106
125
|
if (existing) {
|
|
@@ -281,5 +300,5 @@ function sleep(ms) {
|
|
|
281
300
|
});
|
|
282
301
|
}
|
|
283
302
|
function printUsage() {
|
|
284
|
-
console.log("Usage: node scripts/dist/supervisor.mjs <up|down|restart|status|logs>");
|
|
303
|
+
console.log("Usage: node scripts/dist/supervisor.mjs <up|ensure|down|restart|status|logs>");
|
|
285
304
|
}
|