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