copilot-hub 0.1.32 → 0.1.34

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.
@@ -4,10 +4,11 @@ import fs from "node:fs";
4
4
  import os from "node:os";
5
5
  import path from "node:path";
6
6
  import process from "node:process";
7
- import { spawnSync } from "node:child_process";
7
+ import { spawn, spawnSync } from "node:child_process";
8
8
  import { fileURLToPath } from "node:url";
9
9
  import { initializeCopilotHubLayout, resolveCopilotHubLayout } from "./install-layout.mjs";
10
- import { buildWindowsHiddenLauncherCommand, ensureWindowsHiddenLauncher, getWindowsHiddenLauncherScriptPath, resolveWindowsScriptHost, } from "./windows-hidden-launcher.mjs";
10
+ import { isManagedProcessRunning, normalizePid } from "./process-identity.mjs";
11
+ import { buildWindowsHiddenLauncherCommand, ensureWindowsHiddenLauncher, getWindowsHiddenLauncherScriptPath, getWindowsHiddenLauncherStopSignalPath, resolveWindowsScriptHost, } from "./windows-hidden-launcher.mjs";
11
12
  const __filename = fileURLToPath(import.meta.url);
12
13
  const __dirname = path.dirname(__filename);
13
14
  const repoRoot = path.resolve(__dirname, "..", "..");
@@ -15,12 +16,16 @@ const layout = resolveCopilotHubLayout({ repoRoot });
15
16
  initializeCopilotHubLayout({ repoRoot, layout });
16
17
  const nodeBin = process.execPath;
17
18
  const daemonScriptPath = path.join(repoRoot, "scripts", "dist", "daemon.mjs");
19
+ const daemonStatePath = path.join(layout.runtimeDir, "pids", "daemon.json");
18
20
  const windowsLauncherScriptPath = getWindowsHiddenLauncherScriptPath(layout.runtimeDir);
21
+ const windowsLauncherStopSignalPath = getWindowsHiddenLauncherStopSignalPath(layout.runtimeDir);
19
22
  const WINDOWS_TASK_NAME = "CopilotHub";
20
23
  const WINDOWS_RUN_KEY_PATH = "HKCU\\Software\\Microsoft\\Windows\\CurrentVersion\\Run";
21
24
  const WINDOWS_RUN_VALUE_NAME = "CopilotHub";
22
25
  const LINUX_UNIT_NAME = "copilot-hub.service";
23
26
  const MACOS_LABEL = "com.copilot-hub.service";
27
+ const WINDOWS_LAUNCHER_STOP_TIMEOUT_MS = 12_000;
28
+ const WINDOWS_LAUNCHER_POLL_INTERVAL_MS = 250;
24
29
  const action = String(process.argv[2] ?? "status")
25
30
  .trim()
26
31
  .toLowerCase();
@@ -59,7 +64,7 @@ async function main() {
59
64
  async function installService() {
60
65
  ensureDaemonScript();
61
66
  if (process.platform === "win32") {
62
- const mode = installWindowsAutoStart();
67
+ const mode = await installWindowsAutoStart();
63
68
  if (mode === "task") {
64
69
  console.log("Service installed (Windows Task Scheduler).");
65
70
  }
@@ -82,7 +87,7 @@ async function installService() {
82
87
  }
83
88
  async function uninstallService() {
84
89
  if (process.platform === "win32") {
85
- const removed = uninstallWindowsAutoStart();
90
+ const removed = await uninstallWindowsAutoStart();
86
91
  if (!removed) {
87
92
  console.log("Service auto-start is already absent.");
88
93
  return;
@@ -119,7 +124,7 @@ async function showStatus() {
119
124
  }
120
125
  async function startService() {
121
126
  if (process.platform === "win32") {
122
- const mode = startWindowsAutoStart();
127
+ const mode = await startWindowsAutoStart();
123
128
  if (mode === "run-key") {
124
129
  console.log("Service started in background (Windows startup registry entry).");
125
130
  }
@@ -141,7 +146,7 @@ async function startService() {
141
146
  }
142
147
  async function stopService() {
143
148
  if (process.platform === "win32") {
144
- runDaemon("stop");
149
+ await stopWindowsAutoStart();
145
150
  return;
146
151
  }
147
152
  if (process.platform === "linux") {
@@ -155,26 +160,35 @@ async function stopService() {
155
160
  }
156
161
  throw new Error(`Unsupported platform: ${process.platform}`);
157
162
  }
158
- function installWindowsAutoStart() {
163
+ async function installWindowsAutoStart() {
159
164
  ensureCommandAvailable("schtasks", ["/?"], "Windows Task Scheduler is not available.");
160
165
  ensureCommandAvailable("reg", ["query", WINDOWS_RUN_KEY_PATH], "Windows registry tools are not available.");
161
166
  const command = buildWindowsLaunchCommand();
162
167
  const taskCreate = runChecked("schtasks", ["/Create", "/TN", WINDOWS_TASK_NAME, "/SC", "ONLOGON", "/RL", "LIMITED", "/F", "/TR", command], { allowFailure: true });
163
168
  if (taskCreate.ok) {
164
- runWindowsTask();
169
+ clearWindowsLauncherStopRequest();
170
+ await ensureWindowsSessionRunning("task");
165
171
  return "task";
166
172
  }
167
173
  if (!isAccessDeniedMessage(taskCreate.combinedOutput)) {
168
174
  throw new Error(taskCreate.combinedOutput || "Failed to create Windows auto-start task.");
169
175
  }
170
176
  installWindowsRunKey(command);
171
- runWindowsHiddenLauncher();
177
+ clearWindowsLauncherStopRequest();
178
+ await ensureWindowsSessionRunning("run-key");
172
179
  return "run-key";
173
180
  }
174
- function uninstallWindowsAutoStart() {
181
+ async function uninstallWindowsAutoStart() {
175
182
  ensureCommandAvailable("schtasks", ["/?"], "Windows Task Scheduler is not available.");
176
183
  ensureCommandAvailable("reg", ["query", WINDOWS_RUN_KEY_PATH], "Windows registry tools are not available.");
177
- runDaemon("stop", { allowFailure: true });
184
+ requestWindowsLauncherStop();
185
+ try {
186
+ runDaemon("stop", { allowFailure: true });
187
+ await waitForWindowsLauncherStopAck();
188
+ }
189
+ finally {
190
+ clearWindowsLauncherStopRequest();
191
+ }
178
192
  let removed = false;
179
193
  const taskDelete = runChecked("schtasks", ["/Delete", "/TN", WINDOWS_TASK_NAME, "/F"], {
180
194
  allowFailure: true,
@@ -229,12 +243,13 @@ function runWindowsTask() {
229
243
  throw new Error(result.combinedOutput || "Failed to run service task.");
230
244
  }
231
245
  }
232
- function startWindowsAutoStart() {
246
+ async function startWindowsAutoStart() {
233
247
  const command = buildWindowsLaunchCommand();
234
248
  const runKey = queryWindowsRunKey();
235
249
  if (runKey.installed) {
236
250
  installWindowsRunKey(command);
237
- runWindowsHiddenLauncher();
251
+ clearWindowsLauncherStopRequest();
252
+ await ensureWindowsSessionRunning("run-key");
238
253
  return "run-key";
239
254
  }
240
255
  const task = queryWindowsTask();
@@ -242,7 +257,8 @@ function startWindowsAutoStart() {
242
257
  throw new Error("Service is not installed. Run 'copilot-hub service install' first.");
243
258
  }
244
259
  ensureTaskSchedulerAutoStart(command);
245
- runWindowsTask();
260
+ clearWindowsLauncherStopRequest();
261
+ await ensureWindowsSessionRunning("task");
246
262
  return "task";
247
263
  }
248
264
  function queryWindowsRunKey() {
@@ -460,12 +476,19 @@ function runWindowsHiddenLauncher() {
460
476
  if (!fs.existsSync(scriptHost)) {
461
477
  throw new Error("Windows Script Host is not available.");
462
478
  }
463
- const result = runChecked(scriptHost, ["//B", "//Nologo", launcherScriptPath], {
464
- allowFailure: true,
479
+ const child = spawn(scriptHost, ["//B", "//Nologo", launcherScriptPath], {
480
+ cwd: layout.runtimeDir,
481
+ detached: true,
482
+ stdio: "ignore",
483
+ windowsHide: true,
484
+ shell: false,
485
+ env: process.env,
465
486
  });
466
- if (!result.ok) {
467
- throw new Error(result.combinedOutput || "Failed to launch hidden Windows service starter.");
487
+ const pid = normalizePid(child?.pid);
488
+ if (pid <= 0) {
489
+ throw new Error("Failed to launch hidden Windows service starter.");
468
490
  }
491
+ child.unref();
469
492
  }
470
493
  function ensureSystemctl() {
471
494
  ensureCommandAvailable("systemctl", ["--version"], "systemd is not available. This command requires Linux with systemd user services.");
@@ -485,6 +508,7 @@ function runDaemon(actionValue, { allowFailure = false } = {}) {
485
508
  if (!result.ok && !allowFailure) {
486
509
  throw new Error(result.combinedOutput || `Failed to execute daemon action '${actionValue}'.`);
487
510
  }
511
+ return result;
488
512
  }
489
513
  function runChecked(command, args, { stdio = "pipe", allowFailure = false } = {}) {
490
514
  const result = spawnSync(command, args, {
@@ -583,6 +607,153 @@ function ensureWindowsLauncherScript() {
583
607
  runtimeDir: layout.runtimeDir,
584
608
  });
585
609
  }
610
+ async function ensureWindowsSessionRunning(mode) {
611
+ if (isWindowsHiddenLauncherRunning()) {
612
+ return;
613
+ }
614
+ if (mode === "task") {
615
+ runWindowsTask();
616
+ }
617
+ else {
618
+ runWindowsHiddenLauncher();
619
+ }
620
+ const ready = await waitForWindowsSessionStart();
621
+ if (!ready) {
622
+ throw new Error("Windows background service did not start cleanly.");
623
+ }
624
+ }
625
+ async function stopWindowsAutoStart() {
626
+ requestWindowsLauncherStop();
627
+ try {
628
+ runDaemon("stop", { allowFailure: true });
629
+ await waitForWindowsLauncherStopAck();
630
+ }
631
+ finally {
632
+ clearWindowsLauncherStopRequest();
633
+ }
634
+ }
635
+ async function waitForWindowsSessionStart(timeoutMs = WINDOWS_LAUNCHER_STOP_TIMEOUT_MS) {
636
+ const deadline = Date.now() + timeoutMs;
637
+ while (Date.now() < deadline) {
638
+ if (getRunningDaemonPid() > 0 || isWindowsHiddenLauncherRunning()) {
639
+ return true;
640
+ }
641
+ await sleep(WINDOWS_LAUNCHER_POLL_INTERVAL_MS);
642
+ }
643
+ return getRunningDaemonPid() > 0 || isWindowsHiddenLauncherRunning();
644
+ }
645
+ async function waitForWindowsLauncherStopAck(timeoutMs = WINDOWS_LAUNCHER_STOP_TIMEOUT_MS) {
646
+ const deadline = Date.now() + timeoutMs;
647
+ while (Date.now() < deadline) {
648
+ const daemonRunning = getRunningDaemonPid() > 0;
649
+ const launcherRunning = isWindowsHiddenLauncherRunning();
650
+ const stopRequested = fs.existsSync(windowsLauncherStopSignalPath);
651
+ if (!daemonRunning && !launcherRunning && !stopRequested) {
652
+ return true;
653
+ }
654
+ await sleep(WINDOWS_LAUNCHER_POLL_INTERVAL_MS);
655
+ }
656
+ return getRunningDaemonPid() <= 0 && !isWindowsHiddenLauncherRunning();
657
+ }
658
+ function requestWindowsLauncherStop() {
659
+ fs.mkdirSync(path.dirname(windowsLauncherStopSignalPath), { recursive: true });
660
+ fs.writeFileSync(windowsLauncherStopSignalPath, `${new Date().toISOString()}\n`, "utf8");
661
+ }
662
+ function clearWindowsLauncherStopRequest() {
663
+ if (!fs.existsSync(windowsLauncherStopSignalPath)) {
664
+ return;
665
+ }
666
+ fs.rmSync(windowsLauncherStopSignalPath, { force: true });
667
+ }
668
+ function getRunningDaemonPid() {
669
+ const state = readManagedState(daemonStatePath);
670
+ const pid = normalizePid(state?.pid);
671
+ if (pid <= 0) {
672
+ return 0;
673
+ }
674
+ if (!isManagedProcessRunning(state)) {
675
+ try {
676
+ fs.rmSync(daemonStatePath, { force: true });
677
+ }
678
+ catch {
679
+ // Best effort cleanup only.
680
+ }
681
+ return 0;
682
+ }
683
+ return pid;
684
+ }
685
+ function readManagedState(filePath) {
686
+ if (!fs.existsSync(filePath)) {
687
+ return null;
688
+ }
689
+ try {
690
+ const raw = fs.readFileSync(filePath, "utf8");
691
+ return JSON.parse(raw);
692
+ }
693
+ catch {
694
+ return null;
695
+ }
696
+ }
697
+ function isWindowsHiddenLauncherRunning() {
698
+ return listWindowsHiddenLauncherPids().length > 0;
699
+ }
700
+ function listWindowsHiddenLauncherPids() {
701
+ if (process.platform !== "win32") {
702
+ return [];
703
+ }
704
+ const targetScriptPath = windowsLauncherScriptPath.toLowerCase();
705
+ const script = [
706
+ `$target = '${escapePowerShellSingleQuoted(targetScriptPath)}'`,
707
+ "$matches = @(Get-CimInstance Win32_Process -Filter \"Name = 'wscript.exe'\" -ErrorAction SilentlyContinue | Where-Object {",
708
+ " $cmd = [string]$_.CommandLine",
709
+ " -not [string]::IsNullOrWhiteSpace($cmd) -and $cmd.ToLower().Contains($target)",
710
+ "} | ForEach-Object { [int]$_.ProcessId })",
711
+ "$matches | ConvertTo-Json -Compress",
712
+ ].join("\n");
713
+ for (const shell of resolveWindowsPowerShellCandidates()) {
714
+ const result = spawnSync(shell, ["-NoProfile", "-NonInteractive", "-ExecutionPolicy", "Bypass", "-Command", script], {
715
+ cwd: layout.runtimeDir,
716
+ shell: false,
717
+ windowsHide: true,
718
+ encoding: "utf8",
719
+ env: process.env,
720
+ });
721
+ if (result.error || result.status !== 0) {
722
+ continue;
723
+ }
724
+ return parsePidListJson(result.stdout);
725
+ }
726
+ return [];
727
+ }
728
+ function resolveWindowsPowerShellCandidates() {
729
+ const systemRoot = String(process.env.SystemRoot ?? process.env.SYSTEMROOT ?? "C:\\Windows");
730
+ return [
731
+ path.join(systemRoot, "System32", "WindowsPowerShell", "v1.0", "powershell.exe"),
732
+ "powershell.exe",
733
+ ];
734
+ }
735
+ function parsePidListJson(value) {
736
+ const text = String(value ?? "").trim();
737
+ if (!text) {
738
+ return [];
739
+ }
740
+ try {
741
+ const parsed = JSON.parse(text);
742
+ const values = Array.isArray(parsed) ? parsed : [parsed];
743
+ return values.map((entry) => normalizePid(entry)).filter((pid) => pid > 0);
744
+ }
745
+ catch {
746
+ return [];
747
+ }
748
+ }
749
+ function escapePowerShellSingleQuoted(value) {
750
+ return String(value ?? "").replace(/'/g, "''");
751
+ }
752
+ function sleep(ms) {
753
+ return new Promise((resolve) => {
754
+ setTimeout(resolve, ms);
755
+ });
756
+ }
586
757
  function escapeXml(value) {
587
758
  return String(value ?? "")
588
759
  .replace(/&/g, "&amp;")
@@ -1,5 +1,6 @@
1
1
  import fs from "node:fs";
2
2
  import path from "node:path";
3
+ export const WINDOWS_HIDDEN_LAUNCHER_RESTART_DELAY_MS = 5_000;
3
4
  export function resolveWindowsScriptHost(env = process.env) {
4
5
  const systemRoot = String(env.SystemRoot ?? env.SYSTEMROOT ?? "C:\\Windows").trim();
5
6
  const baseDir = systemRoot || "C:\\Windows";
@@ -8,6 +9,9 @@ export function resolveWindowsScriptHost(env = process.env) {
8
9
  export function getWindowsHiddenLauncherScriptPath(runtimeDir) {
9
10
  return path.win32.join(runtimeDir, "windows-daemon-launcher.vbs");
10
11
  }
12
+ export function getWindowsHiddenLauncherStopSignalPath(runtimeDir) {
13
+ return path.win32.join(runtimeDir, "windows-daemon-launcher.stop");
14
+ }
11
15
  export function ensureWindowsHiddenLauncher({ scriptPath, nodeBin, daemonScriptPath, runtimeDir, }) {
12
16
  const content = buildWindowsHiddenLauncherContent({
13
17
  nodeBin,
@@ -32,13 +36,34 @@ export function buildWindowsHiddenLauncherCommand(scriptPath, env = process.env)
32
36
  return `"${scriptHost}" //B //Nologo "${scriptPath}"`;
33
37
  }
34
38
  export function buildWindowsHiddenLauncherContent({ nodeBin, daemonScriptPath, runtimeDir, }) {
35
- const command = buildWindowsCommandLine([nodeBin, daemonScriptPath, "start"]);
39
+ const command = buildWindowsCommandLine([nodeBin, daemonScriptPath, "run"]);
40
+ const stopSignalPath = getWindowsHiddenLauncherStopSignalPath(runtimeDir);
36
41
  return [
37
42
  "Option Explicit",
38
- "Dim shell",
43
+ "Dim shell, fso, command, stopSignalPath, restartDelayMs",
39
44
  'Set shell = CreateObject("WScript.Shell")',
45
+ 'Set fso = CreateObject("Scripting.FileSystemObject")',
40
46
  `shell.CurrentDirectory = "${escapeVbsString(runtimeDir)}"`,
41
- `shell.Run "${escapeVbsString(command)}", 0, False`,
47
+ `command = "${escapeVbsString(command)}"`,
48
+ `stopSignalPath = "${escapeVbsString(stopSignalPath)}"`,
49
+ `restartDelayMs = ${String(WINDOWS_HIDDEN_LAUNCHER_RESTART_DELAY_MS)}`,
50
+ "Do",
51
+ " If fso.FileExists(stopSignalPath) Then",
52
+ " On Error Resume Next",
53
+ " fso.DeleteFile stopSignalPath, True",
54
+ " On Error GoTo 0",
55
+ " Exit Do",
56
+ " End If",
57
+ " shell.Run command, 0, True",
58
+ " If fso.FileExists(stopSignalPath) Then",
59
+ " On Error Resume Next",
60
+ " fso.DeleteFile stopSignalPath, True",
61
+ " On Error GoTo 0",
62
+ " Exit Do",
63
+ " End If",
64
+ " WScript.Sleep restartDelayMs",
65
+ "Loop",
66
+ "Set fso = Nothing",
42
67
  "Set shell = Nothing",
43
68
  "",
44
69
  ].join("\r\n");
@@ -1,7 +1,8 @@
1
1
  export const codexNpmPackage = "@openai/codex";
2
2
  export const minimumCodexVersion = "0.113.0";
3
- export const maximumCodexVersionExclusive = "0.114.0";
4
- export const codexInstallPackageSpec = `${codexNpmPackage}@${minimumCodexVersion}`;
3
+ export const preferredCodexVersion = "0.117.0";
4
+ export const maximumCodexVersionExclusive = "0.118.0";
5
+ export const codexInstallPackageSpec = `${codexNpmPackage}@${preferredCodexVersion}`;
5
6
  export const codexVersionRequirementLabel = `>= ${minimumCodexVersion} < ${maximumCodexVersionExclusive}`;
6
7
 
7
8
  type Semver = {
@@ -207,9 +207,7 @@ async function runDaemonLoop() {
207
207
 
208
208
  failureCount += 1;
209
209
  const delay = computeBackoffDelay(failureCount);
210
- const reason =
211
- firstLine(ensureResult.combinedOutput) ||
212
- `supervisor ensure exited with code ${String(ensureResult.status ?? "unknown")}`;
210
+ const reason = formatRunCheckedFailureReason(ensureResult, "supervisor ensure");
213
211
  console.error(
214
212
  `[daemon] worker health check failed: ${reason}. Retrying in ${Math.ceil(delay / 1000)}s.`,
215
213
  );
@@ -503,6 +501,26 @@ function firstLine(value) {
503
501
  return String(line ?? "").trim();
504
502
  }
505
503
 
504
+ function formatRunCheckedFailureReason(result, label) {
505
+ const line = firstLine(result?.combinedOutput);
506
+ if (line) {
507
+ return line;
508
+ }
509
+
510
+ const spawnErrorCode = String(result?.spawnErrorCode ?? "")
511
+ .trim()
512
+ .toUpperCase();
513
+ if (spawnErrorCode) {
514
+ return `${label} failed to spawn (${spawnErrorCode})`;
515
+ }
516
+
517
+ if (typeof result?.status === "number") {
518
+ return `${label} exited with code ${result.status}`;
519
+ }
520
+
521
+ return `${label} exited with code unknown`;
522
+ }
523
+
506
524
  function getErrorMessage(error) {
507
525
  if (error instanceof Error && error.message) {
508
526
  return error.message;