copilot-hub 0.1.33 → 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.
- package/README.md +1 -1
- package/package.json +1 -1
- package/scripts/dist/codex-version.mjs +2 -2
- package/scripts/dist/daemon.mjs +17 -2
- package/scripts/dist/service.mjs +189 -18
- package/scripts/dist/windows-hidden-launcher.mjs +28 -3
- package/scripts/src/codex-version.mts +2 -2
- package/scripts/src/daemon.mts +21 -3
- package/scripts/src/service.mts +206 -17
- package/scripts/src/windows-hidden-launcher.mts +30 -3
- package/scripts/test/codex-version.test.mjs +6 -4
- package/scripts/test/windows-hidden-launcher.test.mjs +17 -2
package/README.md
CHANGED
|
@@ -168,7 +168,7 @@ Default values are already applied, and actions start from that agent workspace
|
|
|
168
168
|
- If `npm run start` fails, first read the error and follow the suggested action.
|
|
169
169
|
- `npm run start` now checks that Codex CLI is inside the supported range and can install the validated version automatically if missing or outside that range.
|
|
170
170
|
- For Codex login issues, run `codex login` (or the configured `CODEX_BIN`) and retry `npm run start`.
|
|
171
|
-
- If auto-install is skipped or unavailable, install Codex CLI with `npm install -g @openai/codex@0.
|
|
171
|
+
- If auto-install is skipped or unavailable, install Codex CLI with `npm install -g @openai/codex@0.117.0` or set `CODEX_BIN` in your Copilot Hub config to a binary in the supported `0.113.x` to `0.117.x` range.
|
|
172
172
|
- If you are still stuck, ask your favorite LLM with the exact error output.
|
|
173
173
|
|
|
174
174
|
## Upgrades
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
export const codexNpmPackage = "@openai/codex";
|
|
2
2
|
export const minimumCodexVersion = "0.113.0";
|
|
3
|
-
export const preferredCodexVersion = "0.
|
|
4
|
-
export const maximumCodexVersionExclusive = "0.
|
|
3
|
+
export const preferredCodexVersion = "0.117.0";
|
|
4
|
+
export const maximumCodexVersionExclusive = "0.118.0";
|
|
5
5
|
export const codexInstallPackageSpec = `${codexNpmPackage}@${preferredCodexVersion}`;
|
|
6
6
|
export const codexVersionRequirementLabel = `>= ${minimumCodexVersion} < ${maximumCodexVersionExclusive}`;
|
|
7
7
|
export function extractSemver(value) {
|
package/scripts/dist/daemon.mjs
CHANGED
|
@@ -178,8 +178,7 @@ async function runDaemonLoop() {
|
|
|
178
178
|
}
|
|
179
179
|
failureCount += 1;
|
|
180
180
|
const delay = computeBackoffDelay(failureCount);
|
|
181
|
-
const reason =
|
|
182
|
-
`supervisor ensure exited with code ${String(ensureResult.status ?? "unknown")}`;
|
|
181
|
+
const reason = formatRunCheckedFailureReason(ensureResult, "supervisor ensure");
|
|
183
182
|
console.error(`[daemon] worker health check failed: ${reason}. Retrying in ${Math.ceil(delay / 1000)}s.`);
|
|
184
183
|
await sleepInterruptible(delay, () => state.stopping);
|
|
185
184
|
}
|
|
@@ -426,6 +425,22 @@ function firstLine(value) {
|
|
|
426
425
|
const [line] = text.split(/\r?\n/, 1);
|
|
427
426
|
return String(line ?? "").trim();
|
|
428
427
|
}
|
|
428
|
+
function formatRunCheckedFailureReason(result, label) {
|
|
429
|
+
const line = firstLine(result?.combinedOutput);
|
|
430
|
+
if (line) {
|
|
431
|
+
return line;
|
|
432
|
+
}
|
|
433
|
+
const spawnErrorCode = String(result?.spawnErrorCode ?? "")
|
|
434
|
+
.trim()
|
|
435
|
+
.toUpperCase();
|
|
436
|
+
if (spawnErrorCode) {
|
|
437
|
+
return `${label} failed to spawn (${spawnErrorCode})`;
|
|
438
|
+
}
|
|
439
|
+
if (typeof result?.status === "number") {
|
|
440
|
+
return `${label} exited with code ${result.status}`;
|
|
441
|
+
}
|
|
442
|
+
return `${label} exited with code unknown`;
|
|
443
|
+
}
|
|
429
444
|
function getErrorMessage(error) {
|
|
430
445
|
if (error instanceof Error && error.message) {
|
|
431
446
|
return error.message;
|
package/scripts/dist/service.mjs
CHANGED
|
@@ -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 {
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
464
|
-
|
|
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
|
-
|
|
467
|
-
|
|
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, "&")
|
|
@@ -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, "
|
|
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
|
-
`
|
|
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,7 @@
|
|
|
1
1
|
export const codexNpmPackage = "@openai/codex";
|
|
2
2
|
export const minimumCodexVersion = "0.113.0";
|
|
3
|
-
export const preferredCodexVersion = "0.
|
|
4
|
-
export const maximumCodexVersionExclusive = "0.
|
|
3
|
+
export const preferredCodexVersion = "0.117.0";
|
|
4
|
+
export const maximumCodexVersionExclusive = "0.118.0";
|
|
5
5
|
export const codexInstallPackageSpec = `${codexNpmPackage}@${preferredCodexVersion}`;
|
|
6
6
|
export const codexVersionRequirementLabel = `>= ${minimumCodexVersion} < ${maximumCodexVersionExclusive}`;
|
|
7
7
|
|
package/scripts/src/daemon.mts
CHANGED
|
@@ -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;
|
package/scripts/src/service.mts
CHANGED
|
@@ -4,13 +4,15 @@ 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 { isManagedProcessRunning, normalizePid } from "./process-identity.mjs";
|
|
10
11
|
import {
|
|
11
12
|
buildWindowsHiddenLauncherCommand,
|
|
12
13
|
ensureWindowsHiddenLauncher,
|
|
13
14
|
getWindowsHiddenLauncherScriptPath,
|
|
15
|
+
getWindowsHiddenLauncherStopSignalPath,
|
|
14
16
|
resolveWindowsScriptHost,
|
|
15
17
|
} from "./windows-hidden-launcher.mjs";
|
|
16
18
|
|
|
@@ -21,13 +23,17 @@ const layout = resolveCopilotHubLayout({ repoRoot });
|
|
|
21
23
|
initializeCopilotHubLayout({ repoRoot, layout });
|
|
22
24
|
const nodeBin = process.execPath;
|
|
23
25
|
const daemonScriptPath = path.join(repoRoot, "scripts", "dist", "daemon.mjs");
|
|
26
|
+
const daemonStatePath = path.join(layout.runtimeDir, "pids", "daemon.json");
|
|
24
27
|
const windowsLauncherScriptPath = getWindowsHiddenLauncherScriptPath(layout.runtimeDir);
|
|
28
|
+
const windowsLauncherStopSignalPath = getWindowsHiddenLauncherStopSignalPath(layout.runtimeDir);
|
|
25
29
|
|
|
26
30
|
const WINDOWS_TASK_NAME = "CopilotHub";
|
|
27
31
|
const WINDOWS_RUN_KEY_PATH = "HKCU\\Software\\Microsoft\\Windows\\CurrentVersion\\Run";
|
|
28
32
|
const WINDOWS_RUN_VALUE_NAME = "CopilotHub";
|
|
29
33
|
const LINUX_UNIT_NAME = "copilot-hub.service";
|
|
30
34
|
const MACOS_LABEL = "com.copilot-hub.service";
|
|
35
|
+
const WINDOWS_LAUNCHER_STOP_TIMEOUT_MS = 12_000;
|
|
36
|
+
const WINDOWS_LAUNCHER_POLL_INTERVAL_MS = 250;
|
|
31
37
|
|
|
32
38
|
const action = String(process.argv[2] ?? "status")
|
|
33
39
|
.trim()
|
|
@@ -70,7 +76,7 @@ async function installService() {
|
|
|
70
76
|
ensureDaemonScript();
|
|
71
77
|
|
|
72
78
|
if (process.platform === "win32") {
|
|
73
|
-
const mode = installWindowsAutoStart();
|
|
79
|
+
const mode = await installWindowsAutoStart();
|
|
74
80
|
if (mode === "task") {
|
|
75
81
|
console.log("Service installed (Windows Task Scheduler).");
|
|
76
82
|
} else {
|
|
@@ -96,7 +102,7 @@ async function installService() {
|
|
|
96
102
|
|
|
97
103
|
async function uninstallService() {
|
|
98
104
|
if (process.platform === "win32") {
|
|
99
|
-
const removed = uninstallWindowsAutoStart();
|
|
105
|
+
const removed = await uninstallWindowsAutoStart();
|
|
100
106
|
if (!removed) {
|
|
101
107
|
console.log("Service auto-start is already absent.");
|
|
102
108
|
return;
|
|
@@ -141,7 +147,7 @@ async function showStatus() {
|
|
|
141
147
|
|
|
142
148
|
async function startService() {
|
|
143
149
|
if (process.platform === "win32") {
|
|
144
|
-
const mode = startWindowsAutoStart();
|
|
150
|
+
const mode = await startWindowsAutoStart();
|
|
145
151
|
if (mode === "run-key") {
|
|
146
152
|
console.log("Service started in background (Windows startup registry entry).");
|
|
147
153
|
} else {
|
|
@@ -166,7 +172,7 @@ async function startService() {
|
|
|
166
172
|
|
|
167
173
|
async function stopService() {
|
|
168
174
|
if (process.platform === "win32") {
|
|
169
|
-
|
|
175
|
+
await stopWindowsAutoStart();
|
|
170
176
|
return;
|
|
171
177
|
}
|
|
172
178
|
|
|
@@ -184,7 +190,7 @@ async function stopService() {
|
|
|
184
190
|
throw new Error(`Unsupported platform: ${process.platform}`);
|
|
185
191
|
}
|
|
186
192
|
|
|
187
|
-
function installWindowsAutoStart() {
|
|
193
|
+
async function installWindowsAutoStart() {
|
|
188
194
|
ensureCommandAvailable("schtasks", ["/?"], "Windows Task Scheduler is not available.");
|
|
189
195
|
ensureCommandAvailable(
|
|
190
196
|
"reg",
|
|
@@ -199,7 +205,8 @@ function installWindowsAutoStart() {
|
|
|
199
205
|
{ allowFailure: true },
|
|
200
206
|
);
|
|
201
207
|
if (taskCreate.ok) {
|
|
202
|
-
|
|
208
|
+
clearWindowsLauncherStopRequest();
|
|
209
|
+
await ensureWindowsSessionRunning("task");
|
|
203
210
|
return "task";
|
|
204
211
|
}
|
|
205
212
|
|
|
@@ -208,18 +215,25 @@ function installWindowsAutoStart() {
|
|
|
208
215
|
}
|
|
209
216
|
|
|
210
217
|
installWindowsRunKey(command);
|
|
211
|
-
|
|
218
|
+
clearWindowsLauncherStopRequest();
|
|
219
|
+
await ensureWindowsSessionRunning("run-key");
|
|
212
220
|
return "run-key";
|
|
213
221
|
}
|
|
214
222
|
|
|
215
|
-
function uninstallWindowsAutoStart() {
|
|
223
|
+
async function uninstallWindowsAutoStart() {
|
|
216
224
|
ensureCommandAvailable("schtasks", ["/?"], "Windows Task Scheduler is not available.");
|
|
217
225
|
ensureCommandAvailable(
|
|
218
226
|
"reg",
|
|
219
227
|
["query", WINDOWS_RUN_KEY_PATH],
|
|
220
228
|
"Windows registry tools are not available.",
|
|
221
229
|
);
|
|
222
|
-
|
|
230
|
+
requestWindowsLauncherStop();
|
|
231
|
+
try {
|
|
232
|
+
runDaemon("stop", { allowFailure: true });
|
|
233
|
+
await waitForWindowsLauncherStopAck();
|
|
234
|
+
} finally {
|
|
235
|
+
clearWindowsLauncherStopRequest();
|
|
236
|
+
}
|
|
223
237
|
|
|
224
238
|
let removed = false;
|
|
225
239
|
|
|
@@ -296,12 +310,13 @@ function runWindowsTask() {
|
|
|
296
310
|
}
|
|
297
311
|
}
|
|
298
312
|
|
|
299
|
-
function startWindowsAutoStart() {
|
|
313
|
+
async function startWindowsAutoStart() {
|
|
300
314
|
const command = buildWindowsLaunchCommand();
|
|
301
315
|
const runKey = queryWindowsRunKey();
|
|
302
316
|
if (runKey.installed) {
|
|
303
317
|
installWindowsRunKey(command);
|
|
304
|
-
|
|
318
|
+
clearWindowsLauncherStopRequest();
|
|
319
|
+
await ensureWindowsSessionRunning("run-key");
|
|
305
320
|
return "run-key";
|
|
306
321
|
}
|
|
307
322
|
const task = queryWindowsTask();
|
|
@@ -309,7 +324,8 @@ function startWindowsAutoStart() {
|
|
|
309
324
|
throw new Error("Service is not installed. Run 'copilot-hub service install' first.");
|
|
310
325
|
}
|
|
311
326
|
ensureTaskSchedulerAutoStart(command);
|
|
312
|
-
|
|
327
|
+
clearWindowsLauncherStopRequest();
|
|
328
|
+
await ensureWindowsSessionRunning("task");
|
|
313
329
|
return "task";
|
|
314
330
|
}
|
|
315
331
|
|
|
@@ -562,12 +578,19 @@ function runWindowsHiddenLauncher() {
|
|
|
562
578
|
if (!fs.existsSync(scriptHost)) {
|
|
563
579
|
throw new Error("Windows Script Host is not available.");
|
|
564
580
|
}
|
|
565
|
-
const
|
|
566
|
-
|
|
581
|
+
const child = spawn(scriptHost, ["//B", "//Nologo", launcherScriptPath], {
|
|
582
|
+
cwd: layout.runtimeDir,
|
|
583
|
+
detached: true,
|
|
584
|
+
stdio: "ignore",
|
|
585
|
+
windowsHide: true,
|
|
586
|
+
shell: false,
|
|
587
|
+
env: process.env,
|
|
567
588
|
});
|
|
568
|
-
|
|
569
|
-
|
|
589
|
+
const pid = normalizePid(child?.pid);
|
|
590
|
+
if (pid <= 0) {
|
|
591
|
+
throw new Error("Failed to launch hidden Windows service starter.");
|
|
570
592
|
}
|
|
593
|
+
child.unref();
|
|
571
594
|
}
|
|
572
595
|
|
|
573
596
|
function ensureSystemctl() {
|
|
@@ -594,6 +617,7 @@ function runDaemon(actionValue, { allowFailure = false } = {}) {
|
|
|
594
617
|
if (!result.ok && !allowFailure) {
|
|
595
618
|
throw new Error(result.combinedOutput || `Failed to execute daemon action '${actionValue}'.`);
|
|
596
619
|
}
|
|
620
|
+
return result;
|
|
597
621
|
}
|
|
598
622
|
|
|
599
623
|
function runChecked(command, args, { stdio = "pipe", allowFailure = false } = {}) {
|
|
@@ -709,6 +733,171 @@ function ensureWindowsLauncherScript() {
|
|
|
709
733
|
});
|
|
710
734
|
}
|
|
711
735
|
|
|
736
|
+
async function ensureWindowsSessionRunning(mode) {
|
|
737
|
+
if (isWindowsHiddenLauncherRunning()) {
|
|
738
|
+
return;
|
|
739
|
+
}
|
|
740
|
+
|
|
741
|
+
if (mode === "task") {
|
|
742
|
+
runWindowsTask();
|
|
743
|
+
} else {
|
|
744
|
+
runWindowsHiddenLauncher();
|
|
745
|
+
}
|
|
746
|
+
|
|
747
|
+
const ready = await waitForWindowsSessionStart();
|
|
748
|
+
if (!ready) {
|
|
749
|
+
throw new Error("Windows background service did not start cleanly.");
|
|
750
|
+
}
|
|
751
|
+
}
|
|
752
|
+
|
|
753
|
+
async function stopWindowsAutoStart() {
|
|
754
|
+
requestWindowsLauncherStop();
|
|
755
|
+
try {
|
|
756
|
+
runDaemon("stop", { allowFailure: true });
|
|
757
|
+
await waitForWindowsLauncherStopAck();
|
|
758
|
+
} finally {
|
|
759
|
+
clearWindowsLauncherStopRequest();
|
|
760
|
+
}
|
|
761
|
+
}
|
|
762
|
+
|
|
763
|
+
async function waitForWindowsSessionStart(timeoutMs = WINDOWS_LAUNCHER_STOP_TIMEOUT_MS) {
|
|
764
|
+
const deadline = Date.now() + timeoutMs;
|
|
765
|
+
while (Date.now() < deadline) {
|
|
766
|
+
if (getRunningDaemonPid() > 0 || isWindowsHiddenLauncherRunning()) {
|
|
767
|
+
return true;
|
|
768
|
+
}
|
|
769
|
+
await sleep(WINDOWS_LAUNCHER_POLL_INTERVAL_MS);
|
|
770
|
+
}
|
|
771
|
+
return getRunningDaemonPid() > 0 || isWindowsHiddenLauncherRunning();
|
|
772
|
+
}
|
|
773
|
+
|
|
774
|
+
async function waitForWindowsLauncherStopAck(timeoutMs = WINDOWS_LAUNCHER_STOP_TIMEOUT_MS) {
|
|
775
|
+
const deadline = Date.now() + timeoutMs;
|
|
776
|
+
while (Date.now() < deadline) {
|
|
777
|
+
const daemonRunning = getRunningDaemonPid() > 0;
|
|
778
|
+
const launcherRunning = isWindowsHiddenLauncherRunning();
|
|
779
|
+
const stopRequested = fs.existsSync(windowsLauncherStopSignalPath);
|
|
780
|
+
if (!daemonRunning && !launcherRunning && !stopRequested) {
|
|
781
|
+
return true;
|
|
782
|
+
}
|
|
783
|
+
await sleep(WINDOWS_LAUNCHER_POLL_INTERVAL_MS);
|
|
784
|
+
}
|
|
785
|
+
return getRunningDaemonPid() <= 0 && !isWindowsHiddenLauncherRunning();
|
|
786
|
+
}
|
|
787
|
+
|
|
788
|
+
function requestWindowsLauncherStop() {
|
|
789
|
+
fs.mkdirSync(path.dirname(windowsLauncherStopSignalPath), { recursive: true });
|
|
790
|
+
fs.writeFileSync(windowsLauncherStopSignalPath, `${new Date().toISOString()}\n`, "utf8");
|
|
791
|
+
}
|
|
792
|
+
|
|
793
|
+
function clearWindowsLauncherStopRequest() {
|
|
794
|
+
if (!fs.existsSync(windowsLauncherStopSignalPath)) {
|
|
795
|
+
return;
|
|
796
|
+
}
|
|
797
|
+
fs.rmSync(windowsLauncherStopSignalPath, { force: true });
|
|
798
|
+
}
|
|
799
|
+
|
|
800
|
+
function getRunningDaemonPid() {
|
|
801
|
+
const state = readManagedState(daemonStatePath);
|
|
802
|
+
const pid = normalizePid(state?.pid);
|
|
803
|
+
if (pid <= 0) {
|
|
804
|
+
return 0;
|
|
805
|
+
}
|
|
806
|
+
if (!isManagedProcessRunning(state)) {
|
|
807
|
+
try {
|
|
808
|
+
fs.rmSync(daemonStatePath, { force: true });
|
|
809
|
+
} catch {
|
|
810
|
+
// Best effort cleanup only.
|
|
811
|
+
}
|
|
812
|
+
return 0;
|
|
813
|
+
}
|
|
814
|
+
return pid;
|
|
815
|
+
}
|
|
816
|
+
|
|
817
|
+
function readManagedState(filePath) {
|
|
818
|
+
if (!fs.existsSync(filePath)) {
|
|
819
|
+
return null;
|
|
820
|
+
}
|
|
821
|
+
try {
|
|
822
|
+
const raw = fs.readFileSync(filePath, "utf8");
|
|
823
|
+
return JSON.parse(raw);
|
|
824
|
+
} catch {
|
|
825
|
+
return null;
|
|
826
|
+
}
|
|
827
|
+
}
|
|
828
|
+
|
|
829
|
+
function isWindowsHiddenLauncherRunning() {
|
|
830
|
+
return listWindowsHiddenLauncherPids().length > 0;
|
|
831
|
+
}
|
|
832
|
+
|
|
833
|
+
function listWindowsHiddenLauncherPids() {
|
|
834
|
+
if (process.platform !== "win32") {
|
|
835
|
+
return [];
|
|
836
|
+
}
|
|
837
|
+
|
|
838
|
+
const targetScriptPath = windowsLauncherScriptPath.toLowerCase();
|
|
839
|
+
const script = [
|
|
840
|
+
`$target = '${escapePowerShellSingleQuoted(targetScriptPath)}'`,
|
|
841
|
+
"$matches = @(Get-CimInstance Win32_Process -Filter \"Name = 'wscript.exe'\" -ErrorAction SilentlyContinue | Where-Object {",
|
|
842
|
+
" $cmd = [string]$_.CommandLine",
|
|
843
|
+
" -not [string]::IsNullOrWhiteSpace($cmd) -and $cmd.ToLower().Contains($target)",
|
|
844
|
+
"} | ForEach-Object { [int]$_.ProcessId })",
|
|
845
|
+
"$matches | ConvertTo-Json -Compress",
|
|
846
|
+
].join("\n");
|
|
847
|
+
|
|
848
|
+
for (const shell of resolveWindowsPowerShellCandidates()) {
|
|
849
|
+
const result = spawnSync(
|
|
850
|
+
shell,
|
|
851
|
+
["-NoProfile", "-NonInteractive", "-ExecutionPolicy", "Bypass", "-Command", script],
|
|
852
|
+
{
|
|
853
|
+
cwd: layout.runtimeDir,
|
|
854
|
+
shell: false,
|
|
855
|
+
windowsHide: true,
|
|
856
|
+
encoding: "utf8",
|
|
857
|
+
env: process.env,
|
|
858
|
+
},
|
|
859
|
+
);
|
|
860
|
+
if (result.error || result.status !== 0) {
|
|
861
|
+
continue;
|
|
862
|
+
}
|
|
863
|
+
return parsePidListJson(result.stdout);
|
|
864
|
+
}
|
|
865
|
+
|
|
866
|
+
return [];
|
|
867
|
+
}
|
|
868
|
+
|
|
869
|
+
function resolveWindowsPowerShellCandidates() {
|
|
870
|
+
const systemRoot = String(process.env.SystemRoot ?? process.env.SYSTEMROOT ?? "C:\\Windows");
|
|
871
|
+
return [
|
|
872
|
+
path.join(systemRoot, "System32", "WindowsPowerShell", "v1.0", "powershell.exe"),
|
|
873
|
+
"powershell.exe",
|
|
874
|
+
];
|
|
875
|
+
}
|
|
876
|
+
|
|
877
|
+
function parsePidListJson(value) {
|
|
878
|
+
const text = String(value ?? "").trim();
|
|
879
|
+
if (!text) {
|
|
880
|
+
return [];
|
|
881
|
+
}
|
|
882
|
+
try {
|
|
883
|
+
const parsed = JSON.parse(text);
|
|
884
|
+
const values = Array.isArray(parsed) ? parsed : [parsed];
|
|
885
|
+
return values.map((entry) => normalizePid(entry)).filter((pid) => pid > 0);
|
|
886
|
+
} catch {
|
|
887
|
+
return [];
|
|
888
|
+
}
|
|
889
|
+
}
|
|
890
|
+
|
|
891
|
+
function escapePowerShellSingleQuoted(value) {
|
|
892
|
+
return String(value ?? "").replace(/'/g, "''");
|
|
893
|
+
}
|
|
894
|
+
|
|
895
|
+
function sleep(ms) {
|
|
896
|
+
return new Promise((resolve) => {
|
|
897
|
+
setTimeout(resolve, ms);
|
|
898
|
+
});
|
|
899
|
+
}
|
|
900
|
+
|
|
712
901
|
function escapeXml(value) {
|
|
713
902
|
return String(value ?? "")
|
|
714
903
|
.replace(/&/g, "&")
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import fs from "node:fs";
|
|
2
2
|
import path from "node:path";
|
|
3
3
|
|
|
4
|
+
export const WINDOWS_HIDDEN_LAUNCHER_RESTART_DELAY_MS = 5_000;
|
|
5
|
+
|
|
4
6
|
export function resolveWindowsScriptHost(env: NodeJS.ProcessEnv = process.env): string {
|
|
5
7
|
const systemRoot = String(env.SystemRoot ?? env.SYSTEMROOT ?? "C:\\Windows").trim();
|
|
6
8
|
const baseDir = systemRoot || "C:\\Windows";
|
|
@@ -11,6 +13,10 @@ export function getWindowsHiddenLauncherScriptPath(runtimeDir: string): string {
|
|
|
11
13
|
return path.win32.join(runtimeDir, "windows-daemon-launcher.vbs");
|
|
12
14
|
}
|
|
13
15
|
|
|
16
|
+
export function getWindowsHiddenLauncherStopSignalPath(runtimeDir: string): string {
|
|
17
|
+
return path.win32.join(runtimeDir, "windows-daemon-launcher.stop");
|
|
18
|
+
}
|
|
19
|
+
|
|
14
20
|
export function ensureWindowsHiddenLauncher({
|
|
15
21
|
scriptPath,
|
|
16
22
|
nodeBin,
|
|
@@ -60,13 +66,34 @@ export function buildWindowsHiddenLauncherContent({
|
|
|
60
66
|
daemonScriptPath: string;
|
|
61
67
|
runtimeDir: string;
|
|
62
68
|
}): string {
|
|
63
|
-
const command = buildWindowsCommandLine([nodeBin, daemonScriptPath, "
|
|
69
|
+
const command = buildWindowsCommandLine([nodeBin, daemonScriptPath, "run"]);
|
|
70
|
+
const stopSignalPath = getWindowsHiddenLauncherStopSignalPath(runtimeDir);
|
|
64
71
|
return [
|
|
65
72
|
"Option Explicit",
|
|
66
|
-
"Dim shell",
|
|
73
|
+
"Dim shell, fso, command, stopSignalPath, restartDelayMs",
|
|
67
74
|
'Set shell = CreateObject("WScript.Shell")',
|
|
75
|
+
'Set fso = CreateObject("Scripting.FileSystemObject")',
|
|
68
76
|
`shell.CurrentDirectory = "${escapeVbsString(runtimeDir)}"`,
|
|
69
|
-
`
|
|
77
|
+
`command = "${escapeVbsString(command)}"`,
|
|
78
|
+
`stopSignalPath = "${escapeVbsString(stopSignalPath)}"`,
|
|
79
|
+
`restartDelayMs = ${String(WINDOWS_HIDDEN_LAUNCHER_RESTART_DELAY_MS)}`,
|
|
80
|
+
"Do",
|
|
81
|
+
" If fso.FileExists(stopSignalPath) Then",
|
|
82
|
+
" On Error Resume Next",
|
|
83
|
+
" fso.DeleteFile stopSignalPath, True",
|
|
84
|
+
" On Error GoTo 0",
|
|
85
|
+
" Exit Do",
|
|
86
|
+
" End If",
|
|
87
|
+
" shell.Run command, 0, True",
|
|
88
|
+
" If fso.FileExists(stopSignalPath) Then",
|
|
89
|
+
" On Error Resume Next",
|
|
90
|
+
" fso.DeleteFile stopSignalPath, True",
|
|
91
|
+
" On Error GoTo 0",
|
|
92
|
+
" Exit Do",
|
|
93
|
+
" End If",
|
|
94
|
+
" WScript.Sleep restartDelayMs",
|
|
95
|
+
"Loop",
|
|
96
|
+
"Set fso = Nothing",
|
|
70
97
|
"Set shell = Nothing",
|
|
71
98
|
"",
|
|
72
99
|
].join("\r\n");
|
|
@@ -7,24 +7,26 @@ import {
|
|
|
7
7
|
preferredCodexVersion,
|
|
8
8
|
} from "../dist/codex-version.mjs";
|
|
9
9
|
|
|
10
|
-
test("isCodexVersionCompatible accepts validated stable 0.113.x through 0.
|
|
10
|
+
test("isCodexVersionCompatible accepts validated stable 0.113.x through 0.117.x releases", () => {
|
|
11
11
|
assert.equal(isCodexVersionCompatible("0.113.0"), true);
|
|
12
12
|
assert.equal(isCodexVersionCompatible("0.114.0"), true);
|
|
13
13
|
assert.equal(isCodexVersionCompatible("0.115.3"), true);
|
|
14
14
|
assert.equal(isCodexVersionCompatible("0.116.9"), true);
|
|
15
|
+
assert.equal(isCodexVersionCompatible("0.117.0"), true);
|
|
15
16
|
assert.equal(isCodexVersionCompatible("0.112.9"), false);
|
|
16
|
-
assert.equal(isCodexVersionCompatible("0.
|
|
17
|
+
assert.equal(isCodexVersionCompatible("0.118.0"), false);
|
|
17
18
|
});
|
|
18
19
|
|
|
19
20
|
test("isCodexVersionCompatible rejects prerelease builds outside the validated lane", () => {
|
|
20
21
|
assert.equal(isCodexVersionCompatible("0.113.1-alpha.1"), false);
|
|
21
22
|
assert.equal(isCodexVersionCompatible("0.116.0-alpha.1"), false);
|
|
22
23
|
assert.equal(isCodexVersionCompatible("0.117.0-alpha.1"), false);
|
|
24
|
+
assert.equal(isCodexVersionCompatible("0.118.0-alpha.1"), false);
|
|
23
25
|
});
|
|
24
26
|
|
|
25
27
|
test("preferred install target stays inside the validated compatibility lane", () => {
|
|
26
|
-
assert.equal(preferredCodexVersion, "0.
|
|
27
|
-
assert.equal(codexInstallPackageSpec, "@openai/codex@0.
|
|
28
|
+
assert.equal(preferredCodexVersion, "0.117.0");
|
|
29
|
+
assert.equal(codexInstallPackageSpec, "@openai/codex@0.117.0");
|
|
28
30
|
assert.equal(isCodexVersionCompatible(preferredCodexVersion), true);
|
|
29
31
|
});
|
|
30
32
|
|
|
@@ -7,6 +7,7 @@ import {
|
|
|
7
7
|
buildWindowsHiddenLauncherCommand,
|
|
8
8
|
buildWindowsHiddenLauncherContent,
|
|
9
9
|
ensureWindowsHiddenLauncher,
|
|
10
|
+
getWindowsHiddenLauncherStopSignalPath,
|
|
10
11
|
resolveWindowsScriptHost,
|
|
11
12
|
} from "../dist/windows-hidden-launcher.mjs";
|
|
12
13
|
|
|
@@ -33,9 +34,13 @@ test("buildWindowsHiddenLauncherContent starts daemon in hidden mode", () => {
|
|
|
33
34
|
});
|
|
34
35
|
|
|
35
36
|
assert.match(content, /CreateObject\("WScript\.Shell"\)/);
|
|
37
|
+
assert.match(content, /CreateObject\("Scripting\.FileSystemObject"\)/);
|
|
36
38
|
assert.match(content, /shell\.Run/);
|
|
37
|
-
assert.match(content, /, 0,
|
|
38
|
-
assert.match(content, /
|
|
39
|
+
assert.match(content, /, 0, True/);
|
|
40
|
+
assert.match(content, /restartDelayMs = 5000/);
|
|
41
|
+
assert.match(content, /WScript\.Sleep restartDelayMs/);
|
|
42
|
+
assert.match(content, /"run"/);
|
|
43
|
+
assert.match(content, /windows-daemon-launcher\.stop/);
|
|
39
44
|
});
|
|
40
45
|
|
|
41
46
|
test("ensureWindowsHiddenLauncher writes and preserves launcher content", () => {
|
|
@@ -64,3 +69,13 @@ test("ensureWindowsHiddenLauncher writes and preserves launcher content", () =>
|
|
|
64
69
|
fs.rmSync(tempDir, { recursive: true, force: true });
|
|
65
70
|
}
|
|
66
71
|
});
|
|
72
|
+
|
|
73
|
+
test("getWindowsHiddenLauncherStopSignalPath uses the runtime directory", () => {
|
|
74
|
+
const actual = getWindowsHiddenLauncherStopSignalPath(
|
|
75
|
+
"C:\\Users\\amine\\AppData\\Roaming\\copilot-hub\\runtime",
|
|
76
|
+
);
|
|
77
|
+
assert.equal(
|
|
78
|
+
actual,
|
|
79
|
+
"C:\\Users\\amine\\AppData\\Roaming\\copilot-hub\\runtime\\windows-daemon-launcher.stop",
|
|
80
|
+
);
|
|
81
|
+
});
|