copilot-hub 0.1.23 → 0.1.24
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/package.json +1 -1
- package/scripts/dist/daemon.mjs +32 -4
- package/scripts/dist/service.mjs +69 -5
- package/scripts/dist/windows-hidden-launcher.mjs +51 -0
- package/scripts/src/daemon.mts +39 -4
- package/scripts/src/service.mts +82 -5
- package/scripts/src/windows-hidden-launcher.mts +81 -0
- package/scripts/test/windows-hidden-launcher.test.mjs +66 -0
package/package.json
CHANGED
package/scripts/dist/daemon.mjs
CHANGED
|
@@ -27,6 +27,7 @@ const nodeBin = process.execPath;
|
|
|
27
27
|
const agentEngineEnvPath = layout.agentEngineEnvPath;
|
|
28
28
|
const controlPlaneEnvPath = layout.controlPlaneEnvPath;
|
|
29
29
|
const codexInstallCommand = `npm install -g ${codexInstallPackageSpec}`;
|
|
30
|
+
const WINDOWS_BACKGROUND_ENV = "COPILOT_HUB_DAEMON_BACKGROUND";
|
|
30
31
|
const BASE_CHECK_MS = 5000;
|
|
31
32
|
const MAX_BACKOFF_MS = 60000;
|
|
32
33
|
const action = String(process.argv[2] ?? "status")
|
|
@@ -62,13 +63,21 @@ async function main() {
|
|
|
62
63
|
}
|
|
63
64
|
}
|
|
64
65
|
async function startDaemonProcess() {
|
|
65
|
-
ensureScripts();
|
|
66
|
-
ensureRuntimeDirs();
|
|
67
66
|
const existingPid = getRunningDaemonPid();
|
|
68
67
|
if (existingPid > 0) {
|
|
69
68
|
console.log(`[daemon] already running (pid ${existingPid})`);
|
|
70
69
|
return;
|
|
71
70
|
}
|
|
71
|
+
const pid = await spawnDetachedDaemonProcess();
|
|
72
|
+
console.log(`[daemon] started (pid ${pid})`);
|
|
73
|
+
}
|
|
74
|
+
async function spawnDetachedDaemonProcess() {
|
|
75
|
+
ensureScripts();
|
|
76
|
+
ensureRuntimeDirs();
|
|
77
|
+
const existingPid = getRunningDaemonPid();
|
|
78
|
+
if (existingPid > 0) {
|
|
79
|
+
return existingPid;
|
|
80
|
+
}
|
|
72
81
|
removeDaemonState();
|
|
73
82
|
const logFd = fs.openSync(daemonLogPath, "a");
|
|
74
83
|
let child;
|
|
@@ -79,7 +88,10 @@ async function startDaemonProcess() {
|
|
|
79
88
|
stdio: ["ignore", logFd, logFd],
|
|
80
89
|
windowsHide: true,
|
|
81
90
|
shell: false,
|
|
82
|
-
env:
|
|
91
|
+
env: {
|
|
92
|
+
...process.env,
|
|
93
|
+
[WINDOWS_BACKGROUND_ENV]: "1",
|
|
94
|
+
},
|
|
83
95
|
});
|
|
84
96
|
}
|
|
85
97
|
finally {
|
|
@@ -94,9 +106,13 @@ async function startDaemonProcess() {
|
|
|
94
106
|
if (ready) {
|
|
95
107
|
throw new Error(`Daemon process exited immediately (pid ${pid}). Check logs: ${daemonLogPath}`);
|
|
96
108
|
}
|
|
97
|
-
|
|
109
|
+
return pid;
|
|
98
110
|
}
|
|
99
111
|
async function runDaemonLoop() {
|
|
112
|
+
if (shouldDetachInteractiveWindowsDaemon()) {
|
|
113
|
+
await spawnDetachedDaemonProcess();
|
|
114
|
+
return;
|
|
115
|
+
}
|
|
100
116
|
ensureScripts();
|
|
101
117
|
ensureRuntimeDirs();
|
|
102
118
|
const existingPid = getRunningDaemonPid();
|
|
@@ -465,6 +481,18 @@ function shouldPauseBeforeExit() {
|
|
|
465
481
|
}
|
|
466
482
|
return true;
|
|
467
483
|
}
|
|
484
|
+
function shouldDetachInteractiveWindowsDaemon() {
|
|
485
|
+
if (process.platform !== "win32") {
|
|
486
|
+
return false;
|
|
487
|
+
}
|
|
488
|
+
if (String(process.env[WINDOWS_BACKGROUND_ENV] ?? "").trim() === "1") {
|
|
489
|
+
return false;
|
|
490
|
+
}
|
|
491
|
+
if (!process.stdin || !process.stdout) {
|
|
492
|
+
return false;
|
|
493
|
+
}
|
|
494
|
+
return Boolean(process.stdin.isTTY && process.stdout.isTTY);
|
|
495
|
+
}
|
|
468
496
|
function detectFatalStartupError(ensureResult) {
|
|
469
497
|
const evidenceChunks = [
|
|
470
498
|
String(ensureResult?.combinedOutput ?? ""),
|
package/scripts/dist/service.mjs
CHANGED
|
@@ -7,6 +7,7 @@ import process from "node:process";
|
|
|
7
7
|
import { 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
11
|
const __filename = fileURLToPath(import.meta.url);
|
|
11
12
|
const __dirname = path.dirname(__filename);
|
|
12
13
|
const repoRoot = path.resolve(__dirname, "..", "..");
|
|
@@ -14,6 +15,7 @@ const layout = resolveCopilotHubLayout({ repoRoot });
|
|
|
14
15
|
initializeCopilotHubLayout({ repoRoot, layout });
|
|
15
16
|
const nodeBin = process.execPath;
|
|
16
17
|
const daemonScriptPath = path.join(repoRoot, "scripts", "dist", "daemon.mjs");
|
|
18
|
+
const windowsLauncherScriptPath = getWindowsHiddenLauncherScriptPath(layout.runtimeDir);
|
|
17
19
|
const WINDOWS_TASK_NAME = "CopilotHub";
|
|
18
20
|
const WINDOWS_RUN_KEY_PATH = "HKCU\\Software\\Microsoft\\Windows\\CurrentVersion\\Run";
|
|
19
21
|
const WINDOWS_RUN_VALUE_NAME = "CopilotHub";
|
|
@@ -117,7 +119,13 @@ async function showStatus() {
|
|
|
117
119
|
}
|
|
118
120
|
async function startService() {
|
|
119
121
|
if (process.platform === "win32") {
|
|
120
|
-
startWindowsAutoStart();
|
|
122
|
+
const mode = startWindowsAutoStart();
|
|
123
|
+
if (mode === "run-key") {
|
|
124
|
+
console.log("Service started in background (Windows startup registry entry).");
|
|
125
|
+
}
|
|
126
|
+
else {
|
|
127
|
+
console.log("Service started in background (Windows Task Scheduler).");
|
|
128
|
+
}
|
|
121
129
|
return;
|
|
122
130
|
}
|
|
123
131
|
if (process.platform === "linux") {
|
|
@@ -160,7 +168,7 @@ function installWindowsAutoStart() {
|
|
|
160
168
|
throw new Error(taskCreate.combinedOutput || "Failed to create Windows auto-start task.");
|
|
161
169
|
}
|
|
162
170
|
installWindowsRunKey(command);
|
|
163
|
-
|
|
171
|
+
runWindowsHiddenLauncher();
|
|
164
172
|
return "run-key";
|
|
165
173
|
}
|
|
166
174
|
function uninstallWindowsAutoStart() {
|
|
@@ -185,6 +193,9 @@ function uninstallWindowsAutoStart() {
|
|
|
185
193
|
else if (!isRegistryValueNotFoundMessage(runKeyDelete.combinedOutput)) {
|
|
186
194
|
throw new Error(runKeyDelete.combinedOutput || "Failed to remove Windows startup registry entry.");
|
|
187
195
|
}
|
|
196
|
+
if (fs.existsSync(windowsLauncherScriptPath)) {
|
|
197
|
+
fs.rmSync(windowsLauncherScriptPath, { force: true });
|
|
198
|
+
}
|
|
188
199
|
return removed;
|
|
189
200
|
}
|
|
190
201
|
function showWindowsAutoStartStatus() {
|
|
@@ -219,12 +230,20 @@ function runWindowsTask() {
|
|
|
219
230
|
}
|
|
220
231
|
}
|
|
221
232
|
function startWindowsAutoStart() {
|
|
233
|
+
const command = buildWindowsLaunchCommand();
|
|
222
234
|
const runKey = queryWindowsRunKey();
|
|
223
235
|
if (runKey.installed) {
|
|
224
|
-
|
|
225
|
-
|
|
236
|
+
installWindowsRunKey(command);
|
|
237
|
+
runWindowsHiddenLauncher();
|
|
238
|
+
return "run-key";
|
|
239
|
+
}
|
|
240
|
+
const task = queryWindowsTask();
|
|
241
|
+
if (!task.installed) {
|
|
242
|
+
throw new Error("Service is not installed. Run 'copilot-hub service install' first.");
|
|
226
243
|
}
|
|
244
|
+
ensureTaskSchedulerAutoStart(command);
|
|
227
245
|
runWindowsTask();
|
|
246
|
+
return "task";
|
|
228
247
|
}
|
|
229
248
|
function queryWindowsRunKey() {
|
|
230
249
|
const result = runChecked("reg", ["query", WINDOWS_RUN_KEY_PATH, "/v", WINDOWS_RUN_VALUE_NAME], {
|
|
@@ -238,6 +257,18 @@ function queryWindowsRunKey() {
|
|
|
238
257
|
}
|
|
239
258
|
throw new Error(result.combinedOutput || "Failed to query Windows startup registry entry.");
|
|
240
259
|
}
|
|
260
|
+
function queryWindowsTask() {
|
|
261
|
+
const result = runChecked("schtasks", ["/Query", "/TN", WINDOWS_TASK_NAME], {
|
|
262
|
+
allowFailure: true,
|
|
263
|
+
});
|
|
264
|
+
if (result.ok) {
|
|
265
|
+
return { installed: true };
|
|
266
|
+
}
|
|
267
|
+
if (isNotFoundMessage(result.combinedOutput)) {
|
|
268
|
+
return { installed: false };
|
|
269
|
+
}
|
|
270
|
+
throw new Error(result.combinedOutput || "Failed to query Windows auto-start task.");
|
|
271
|
+
}
|
|
241
272
|
function installWindowsRunKey(command) {
|
|
242
273
|
runChecked("reg", [
|
|
243
274
|
"add",
|
|
@@ -251,6 +282,16 @@ function installWindowsRunKey(command) {
|
|
|
251
282
|
"/f",
|
|
252
283
|
], { stdio: "pipe" });
|
|
253
284
|
}
|
|
285
|
+
function ensureTaskSchedulerAutoStart(command) {
|
|
286
|
+
const result = runChecked("schtasks", ["/Create", "/TN", WINDOWS_TASK_NAME, "/SC", "ONLOGON", "/RL", "LIMITED", "/F", "/TR", command], { allowFailure: true });
|
|
287
|
+
if (result.ok) {
|
|
288
|
+
return;
|
|
289
|
+
}
|
|
290
|
+
if (isNotFoundMessage(result.combinedOutput)) {
|
|
291
|
+
throw new Error("Service is not installed. Run 'copilot-hub service install' first.");
|
|
292
|
+
}
|
|
293
|
+
throw new Error(result.combinedOutput || "Failed to update Windows auto-start task.");
|
|
294
|
+
}
|
|
254
295
|
function installLinuxService() {
|
|
255
296
|
ensureSystemctl();
|
|
256
297
|
const unitPath = getLinuxUnitPath();
|
|
@@ -413,6 +454,19 @@ function ensureDaemonScript() {
|
|
|
413
454
|
].join("\n"));
|
|
414
455
|
}
|
|
415
456
|
}
|
|
457
|
+
function runWindowsHiddenLauncher() {
|
|
458
|
+
const launcherScriptPath = ensureWindowsLauncherScript();
|
|
459
|
+
const scriptHost = resolveWindowsScriptHost(process.env);
|
|
460
|
+
if (!fs.existsSync(scriptHost)) {
|
|
461
|
+
throw new Error("Windows Script Host is not available.");
|
|
462
|
+
}
|
|
463
|
+
const result = runChecked(scriptHost, ["//B", "//Nologo", launcherScriptPath], {
|
|
464
|
+
allowFailure: true,
|
|
465
|
+
});
|
|
466
|
+
if (!result.ok) {
|
|
467
|
+
throw new Error(result.combinedOutput || "Failed to launch hidden Windows service starter.");
|
|
468
|
+
}
|
|
469
|
+
}
|
|
416
470
|
function ensureSystemctl() {
|
|
417
471
|
ensureCommandAvailable("systemctl", ["--version"], "systemd is not available. This command requires Linux with systemd user services.");
|
|
418
472
|
}
|
|
@@ -517,7 +571,17 @@ function getErrorMessage(error) {
|
|
|
517
571
|
return String(error ?? "Unknown error.");
|
|
518
572
|
}
|
|
519
573
|
function buildWindowsLaunchCommand() {
|
|
520
|
-
|
|
574
|
+
const launcherScriptPath = ensureWindowsLauncherScript();
|
|
575
|
+
return buildWindowsHiddenLauncherCommand(launcherScriptPath, process.env);
|
|
576
|
+
}
|
|
577
|
+
function ensureWindowsLauncherScript() {
|
|
578
|
+
ensureDaemonScript();
|
|
579
|
+
return ensureWindowsHiddenLauncher({
|
|
580
|
+
scriptPath: windowsLauncherScriptPath,
|
|
581
|
+
nodeBin,
|
|
582
|
+
daemonScriptPath,
|
|
583
|
+
runtimeDir: layout.runtimeDir,
|
|
584
|
+
});
|
|
521
585
|
}
|
|
522
586
|
function escapeXml(value) {
|
|
523
587
|
return String(value ?? "")
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
export function resolveWindowsScriptHost(env = process.env) {
|
|
4
|
+
const systemRoot = String(env.SystemRoot ?? env.SYSTEMROOT ?? "C:\\Windows").trim();
|
|
5
|
+
const baseDir = systemRoot || "C:\\Windows";
|
|
6
|
+
return path.win32.join(baseDir, "System32", "wscript.exe");
|
|
7
|
+
}
|
|
8
|
+
export function getWindowsHiddenLauncherScriptPath(runtimeDir) {
|
|
9
|
+
return path.win32.join(runtimeDir, "windows-daemon-launcher.vbs");
|
|
10
|
+
}
|
|
11
|
+
export function ensureWindowsHiddenLauncher({ scriptPath, nodeBin, daemonScriptPath, runtimeDir, }) {
|
|
12
|
+
const content = buildWindowsHiddenLauncherContent({
|
|
13
|
+
nodeBin,
|
|
14
|
+
daemonScriptPath,
|
|
15
|
+
runtimeDir,
|
|
16
|
+
});
|
|
17
|
+
let current = "";
|
|
18
|
+
try {
|
|
19
|
+
current = fs.readFileSync(scriptPath, "utf8");
|
|
20
|
+
}
|
|
21
|
+
catch {
|
|
22
|
+
current = "";
|
|
23
|
+
}
|
|
24
|
+
if (current !== content) {
|
|
25
|
+
fs.mkdirSync(path.dirname(scriptPath), { recursive: true });
|
|
26
|
+
fs.writeFileSync(scriptPath, content, "utf8");
|
|
27
|
+
}
|
|
28
|
+
return scriptPath;
|
|
29
|
+
}
|
|
30
|
+
export function buildWindowsHiddenLauncherCommand(scriptPath, env = process.env) {
|
|
31
|
+
const scriptHost = resolveWindowsScriptHost(env);
|
|
32
|
+
return `"${scriptHost}" //B //Nologo "${scriptPath}"`;
|
|
33
|
+
}
|
|
34
|
+
export function buildWindowsHiddenLauncherContent({ nodeBin, daemonScriptPath, runtimeDir, }) {
|
|
35
|
+
const command = buildWindowsCommandLine([nodeBin, daemonScriptPath, "start"]);
|
|
36
|
+
return [
|
|
37
|
+
"Option Explicit",
|
|
38
|
+
"Dim shell",
|
|
39
|
+
'Set shell = CreateObject("WScript.Shell")',
|
|
40
|
+
`shell.CurrentDirectory = "${escapeVbsString(runtimeDir)}"`,
|
|
41
|
+
`shell.Run "${escapeVbsString(command)}", 0, False`,
|
|
42
|
+
"Set shell = Nothing",
|
|
43
|
+
"",
|
|
44
|
+
].join("\r\n");
|
|
45
|
+
}
|
|
46
|
+
function buildWindowsCommandLine(args) {
|
|
47
|
+
return args.map((value) => `"${String(value ?? "")}"`).join(" ");
|
|
48
|
+
}
|
|
49
|
+
function escapeVbsString(value) {
|
|
50
|
+
return String(value ?? "").replace(/"/g, '""');
|
|
51
|
+
}
|
package/scripts/src/daemon.mts
CHANGED
|
@@ -35,6 +35,7 @@ const nodeBin = process.execPath;
|
|
|
35
35
|
const agentEngineEnvPath = layout.agentEngineEnvPath;
|
|
36
36
|
const controlPlaneEnvPath = layout.controlPlaneEnvPath;
|
|
37
37
|
const codexInstallCommand = `npm install -g ${codexInstallPackageSpec}`;
|
|
38
|
+
const WINDOWS_BACKGROUND_ENV = "COPILOT_HUB_DAEMON_BACKGROUND";
|
|
38
39
|
|
|
39
40
|
const BASE_CHECK_MS = 5000;
|
|
40
41
|
const MAX_BACKOFF_MS = 60000;
|
|
@@ -74,13 +75,23 @@ async function main() {
|
|
|
74
75
|
}
|
|
75
76
|
|
|
76
77
|
async function startDaemonProcess() {
|
|
78
|
+
const existingPid = getRunningDaemonPid();
|
|
79
|
+
if (existingPid > 0) {
|
|
80
|
+
console.log(`[daemon] already running (pid ${existingPid})`);
|
|
81
|
+
return;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
const pid = await spawnDetachedDaemonProcess();
|
|
85
|
+
console.log(`[daemon] started (pid ${pid})`);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
async function spawnDetachedDaemonProcess() {
|
|
77
89
|
ensureScripts();
|
|
78
90
|
ensureRuntimeDirs();
|
|
79
91
|
|
|
80
92
|
const existingPid = getRunningDaemonPid();
|
|
81
93
|
if (existingPid > 0) {
|
|
82
|
-
|
|
83
|
-
return;
|
|
94
|
+
return existingPid;
|
|
84
95
|
}
|
|
85
96
|
|
|
86
97
|
removeDaemonState();
|
|
@@ -94,7 +105,10 @@ async function startDaemonProcess() {
|
|
|
94
105
|
stdio: ["ignore", logFd, logFd],
|
|
95
106
|
windowsHide: true,
|
|
96
107
|
shell: false,
|
|
97
|
-
env:
|
|
108
|
+
env: {
|
|
109
|
+
...process.env,
|
|
110
|
+
[WINDOWS_BACKGROUND_ENV]: "1",
|
|
111
|
+
},
|
|
98
112
|
});
|
|
99
113
|
} finally {
|
|
100
114
|
fs.closeSync(logFd);
|
|
@@ -111,10 +125,15 @@ async function startDaemonProcess() {
|
|
|
111
125
|
throw new Error(`Daemon process exited immediately (pid ${pid}). Check logs: ${daemonLogPath}`);
|
|
112
126
|
}
|
|
113
127
|
|
|
114
|
-
|
|
128
|
+
return pid;
|
|
115
129
|
}
|
|
116
130
|
|
|
117
131
|
async function runDaemonLoop() {
|
|
132
|
+
if (shouldDetachInteractiveWindowsDaemon()) {
|
|
133
|
+
await spawnDetachedDaemonProcess();
|
|
134
|
+
return;
|
|
135
|
+
}
|
|
136
|
+
|
|
118
137
|
ensureScripts();
|
|
119
138
|
ensureRuntimeDirs();
|
|
120
139
|
|
|
@@ -545,6 +564,22 @@ function shouldPauseBeforeExit() {
|
|
|
545
564
|
return true;
|
|
546
565
|
}
|
|
547
566
|
|
|
567
|
+
function shouldDetachInteractiveWindowsDaemon() {
|
|
568
|
+
if (process.platform !== "win32") {
|
|
569
|
+
return false;
|
|
570
|
+
}
|
|
571
|
+
|
|
572
|
+
if (String(process.env[WINDOWS_BACKGROUND_ENV] ?? "").trim() === "1") {
|
|
573
|
+
return false;
|
|
574
|
+
}
|
|
575
|
+
|
|
576
|
+
if (!process.stdin || !process.stdout) {
|
|
577
|
+
return false;
|
|
578
|
+
}
|
|
579
|
+
|
|
580
|
+
return Boolean(process.stdin.isTTY && process.stdout.isTTY);
|
|
581
|
+
}
|
|
582
|
+
|
|
548
583
|
function detectFatalStartupError(ensureResult) {
|
|
549
584
|
const evidenceChunks = [
|
|
550
585
|
String(ensureResult?.combinedOutput ?? ""),
|
package/scripts/src/service.mts
CHANGED
|
@@ -7,6 +7,12 @@ import process from "node:process";
|
|
|
7
7
|
import { spawnSync } from "node:child_process";
|
|
8
8
|
import { fileURLToPath } from "node:url";
|
|
9
9
|
import { initializeCopilotHubLayout, resolveCopilotHubLayout } from "./install-layout.mjs";
|
|
10
|
+
import {
|
|
11
|
+
buildWindowsHiddenLauncherCommand,
|
|
12
|
+
ensureWindowsHiddenLauncher,
|
|
13
|
+
getWindowsHiddenLauncherScriptPath,
|
|
14
|
+
resolveWindowsScriptHost,
|
|
15
|
+
} from "./windows-hidden-launcher.mjs";
|
|
10
16
|
|
|
11
17
|
const __filename = fileURLToPath(import.meta.url);
|
|
12
18
|
const __dirname = path.dirname(__filename);
|
|
@@ -15,6 +21,7 @@ const layout = resolveCopilotHubLayout({ repoRoot });
|
|
|
15
21
|
initializeCopilotHubLayout({ repoRoot, layout });
|
|
16
22
|
const nodeBin = process.execPath;
|
|
17
23
|
const daemonScriptPath = path.join(repoRoot, "scripts", "dist", "daemon.mjs");
|
|
24
|
+
const windowsLauncherScriptPath = getWindowsHiddenLauncherScriptPath(layout.runtimeDir);
|
|
18
25
|
|
|
19
26
|
const WINDOWS_TASK_NAME = "CopilotHub";
|
|
20
27
|
const WINDOWS_RUN_KEY_PATH = "HKCU\\Software\\Microsoft\\Windows\\CurrentVersion\\Run";
|
|
@@ -134,7 +141,12 @@ async function showStatus() {
|
|
|
134
141
|
|
|
135
142
|
async function startService() {
|
|
136
143
|
if (process.platform === "win32") {
|
|
137
|
-
startWindowsAutoStart();
|
|
144
|
+
const mode = startWindowsAutoStart();
|
|
145
|
+
if (mode === "run-key") {
|
|
146
|
+
console.log("Service started in background (Windows startup registry entry).");
|
|
147
|
+
} else {
|
|
148
|
+
console.log("Service started in background (Windows Task Scheduler).");
|
|
149
|
+
}
|
|
138
150
|
return;
|
|
139
151
|
}
|
|
140
152
|
|
|
@@ -196,7 +208,7 @@ function installWindowsAutoStart() {
|
|
|
196
208
|
}
|
|
197
209
|
|
|
198
210
|
installWindowsRunKey(command);
|
|
199
|
-
|
|
211
|
+
runWindowsHiddenLauncher();
|
|
200
212
|
return "run-key";
|
|
201
213
|
}
|
|
202
214
|
|
|
@@ -236,6 +248,10 @@ function uninstallWindowsAutoStart() {
|
|
|
236
248
|
);
|
|
237
249
|
}
|
|
238
250
|
|
|
251
|
+
if (fs.existsSync(windowsLauncherScriptPath)) {
|
|
252
|
+
fs.rmSync(windowsLauncherScriptPath, { force: true });
|
|
253
|
+
}
|
|
254
|
+
|
|
239
255
|
return removed;
|
|
240
256
|
}
|
|
241
257
|
|
|
@@ -281,12 +297,20 @@ function runWindowsTask() {
|
|
|
281
297
|
}
|
|
282
298
|
|
|
283
299
|
function startWindowsAutoStart() {
|
|
300
|
+
const command = buildWindowsLaunchCommand();
|
|
284
301
|
const runKey = queryWindowsRunKey();
|
|
285
302
|
if (runKey.installed) {
|
|
286
|
-
|
|
287
|
-
|
|
303
|
+
installWindowsRunKey(command);
|
|
304
|
+
runWindowsHiddenLauncher();
|
|
305
|
+
return "run-key";
|
|
306
|
+
}
|
|
307
|
+
const task = queryWindowsTask();
|
|
308
|
+
if (!task.installed) {
|
|
309
|
+
throw new Error("Service is not installed. Run 'copilot-hub service install' first.");
|
|
288
310
|
}
|
|
311
|
+
ensureTaskSchedulerAutoStart(command);
|
|
289
312
|
runWindowsTask();
|
|
313
|
+
return "task";
|
|
290
314
|
}
|
|
291
315
|
|
|
292
316
|
function queryWindowsRunKey() {
|
|
@@ -302,6 +326,19 @@ function queryWindowsRunKey() {
|
|
|
302
326
|
throw new Error(result.combinedOutput || "Failed to query Windows startup registry entry.");
|
|
303
327
|
}
|
|
304
328
|
|
|
329
|
+
function queryWindowsTask() {
|
|
330
|
+
const result = runChecked("schtasks", ["/Query", "/TN", WINDOWS_TASK_NAME], {
|
|
331
|
+
allowFailure: true,
|
|
332
|
+
});
|
|
333
|
+
if (result.ok) {
|
|
334
|
+
return { installed: true };
|
|
335
|
+
}
|
|
336
|
+
if (isNotFoundMessage(result.combinedOutput)) {
|
|
337
|
+
return { installed: false };
|
|
338
|
+
}
|
|
339
|
+
throw new Error(result.combinedOutput || "Failed to query Windows auto-start task.");
|
|
340
|
+
}
|
|
341
|
+
|
|
305
342
|
function installWindowsRunKey(command) {
|
|
306
343
|
runChecked(
|
|
307
344
|
"reg",
|
|
@@ -320,6 +357,21 @@ function installWindowsRunKey(command) {
|
|
|
320
357
|
);
|
|
321
358
|
}
|
|
322
359
|
|
|
360
|
+
function ensureTaskSchedulerAutoStart(command) {
|
|
361
|
+
const result = runChecked(
|
|
362
|
+
"schtasks",
|
|
363
|
+
["/Create", "/TN", WINDOWS_TASK_NAME, "/SC", "ONLOGON", "/RL", "LIMITED", "/F", "/TR", command],
|
|
364
|
+
{ allowFailure: true },
|
|
365
|
+
);
|
|
366
|
+
if (result.ok) {
|
|
367
|
+
return;
|
|
368
|
+
}
|
|
369
|
+
if (isNotFoundMessage(result.combinedOutput)) {
|
|
370
|
+
throw new Error("Service is not installed. Run 'copilot-hub service install' first.");
|
|
371
|
+
}
|
|
372
|
+
throw new Error(result.combinedOutput || "Failed to update Windows auto-start task.");
|
|
373
|
+
}
|
|
374
|
+
|
|
323
375
|
function installLinuxService() {
|
|
324
376
|
ensureSystemctl();
|
|
325
377
|
const unitPath = getLinuxUnitPath();
|
|
@@ -504,6 +556,20 @@ function ensureDaemonScript() {
|
|
|
504
556
|
}
|
|
505
557
|
}
|
|
506
558
|
|
|
559
|
+
function runWindowsHiddenLauncher() {
|
|
560
|
+
const launcherScriptPath = ensureWindowsLauncherScript();
|
|
561
|
+
const scriptHost = resolveWindowsScriptHost(process.env);
|
|
562
|
+
if (!fs.existsSync(scriptHost)) {
|
|
563
|
+
throw new Error("Windows Script Host is not available.");
|
|
564
|
+
}
|
|
565
|
+
const result = runChecked(scriptHost, ["//B", "//Nologo", launcherScriptPath], {
|
|
566
|
+
allowFailure: true,
|
|
567
|
+
});
|
|
568
|
+
if (!result.ok) {
|
|
569
|
+
throw new Error(result.combinedOutput || "Failed to launch hidden Windows service starter.");
|
|
570
|
+
}
|
|
571
|
+
}
|
|
572
|
+
|
|
507
573
|
function ensureSystemctl() {
|
|
508
574
|
ensureCommandAvailable(
|
|
509
575
|
"systemctl",
|
|
@@ -629,7 +695,18 @@ function getErrorMessage(error) {
|
|
|
629
695
|
}
|
|
630
696
|
|
|
631
697
|
function buildWindowsLaunchCommand() {
|
|
632
|
-
|
|
698
|
+
const launcherScriptPath = ensureWindowsLauncherScript();
|
|
699
|
+
return buildWindowsHiddenLauncherCommand(launcherScriptPath, process.env);
|
|
700
|
+
}
|
|
701
|
+
|
|
702
|
+
function ensureWindowsLauncherScript() {
|
|
703
|
+
ensureDaemonScript();
|
|
704
|
+
return ensureWindowsHiddenLauncher({
|
|
705
|
+
scriptPath: windowsLauncherScriptPath,
|
|
706
|
+
nodeBin,
|
|
707
|
+
daemonScriptPath,
|
|
708
|
+
runtimeDir: layout.runtimeDir,
|
|
709
|
+
});
|
|
633
710
|
}
|
|
634
711
|
|
|
635
712
|
function escapeXml(value) {
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
|
|
4
|
+
export function resolveWindowsScriptHost(env: NodeJS.ProcessEnv = process.env): string {
|
|
5
|
+
const systemRoot = String(env.SystemRoot ?? env.SYSTEMROOT ?? "C:\\Windows").trim();
|
|
6
|
+
const baseDir = systemRoot || "C:\\Windows";
|
|
7
|
+
return path.win32.join(baseDir, "System32", "wscript.exe");
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export function getWindowsHiddenLauncherScriptPath(runtimeDir: string): string {
|
|
11
|
+
return path.win32.join(runtimeDir, "windows-daemon-launcher.vbs");
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export function ensureWindowsHiddenLauncher({
|
|
15
|
+
scriptPath,
|
|
16
|
+
nodeBin,
|
|
17
|
+
daemonScriptPath,
|
|
18
|
+
runtimeDir,
|
|
19
|
+
}: {
|
|
20
|
+
scriptPath: string;
|
|
21
|
+
nodeBin: string;
|
|
22
|
+
daemonScriptPath: string;
|
|
23
|
+
runtimeDir: string;
|
|
24
|
+
}): string {
|
|
25
|
+
const content = buildWindowsHiddenLauncherContent({
|
|
26
|
+
nodeBin,
|
|
27
|
+
daemonScriptPath,
|
|
28
|
+
runtimeDir,
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
let current = "";
|
|
32
|
+
try {
|
|
33
|
+
current = fs.readFileSync(scriptPath, "utf8");
|
|
34
|
+
} catch {
|
|
35
|
+
current = "";
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
if (current !== content) {
|
|
39
|
+
fs.mkdirSync(path.dirname(scriptPath), { recursive: true });
|
|
40
|
+
fs.writeFileSync(scriptPath, content, "utf8");
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
return scriptPath;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export function buildWindowsHiddenLauncherCommand(
|
|
47
|
+
scriptPath: string,
|
|
48
|
+
env: NodeJS.ProcessEnv = process.env,
|
|
49
|
+
): string {
|
|
50
|
+
const scriptHost = resolveWindowsScriptHost(env);
|
|
51
|
+
return `"${scriptHost}" //B //Nologo "${scriptPath}"`;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export function buildWindowsHiddenLauncherContent({
|
|
55
|
+
nodeBin,
|
|
56
|
+
daemonScriptPath,
|
|
57
|
+
runtimeDir,
|
|
58
|
+
}: {
|
|
59
|
+
nodeBin: string;
|
|
60
|
+
daemonScriptPath: string;
|
|
61
|
+
runtimeDir: string;
|
|
62
|
+
}): string {
|
|
63
|
+
const command = buildWindowsCommandLine([nodeBin, daemonScriptPath, "start"]);
|
|
64
|
+
return [
|
|
65
|
+
"Option Explicit",
|
|
66
|
+
"Dim shell",
|
|
67
|
+
'Set shell = CreateObject("WScript.Shell")',
|
|
68
|
+
`shell.CurrentDirectory = "${escapeVbsString(runtimeDir)}"`,
|
|
69
|
+
`shell.Run "${escapeVbsString(command)}", 0, False`,
|
|
70
|
+
"Set shell = Nothing",
|
|
71
|
+
"",
|
|
72
|
+
].join("\r\n");
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function buildWindowsCommandLine(args: string[]): string {
|
|
76
|
+
return args.map((value) => `"${String(value ?? "")}"`).join(" ");
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function escapeVbsString(value: string): string {
|
|
80
|
+
return String(value ?? "").replace(/"/g, '""');
|
|
81
|
+
}
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import assert from "node:assert/strict";
|
|
2
|
+
import fs from "node:fs";
|
|
3
|
+
import os from "node:os";
|
|
4
|
+
import path from "node:path";
|
|
5
|
+
import test from "node:test";
|
|
6
|
+
import {
|
|
7
|
+
buildWindowsHiddenLauncherCommand,
|
|
8
|
+
buildWindowsHiddenLauncherContent,
|
|
9
|
+
ensureWindowsHiddenLauncher,
|
|
10
|
+
resolveWindowsScriptHost,
|
|
11
|
+
} from "../dist/windows-hidden-launcher.mjs";
|
|
12
|
+
|
|
13
|
+
test("resolveWindowsScriptHost prefers SystemRoot", () => {
|
|
14
|
+
const actual = resolveWindowsScriptHost({ SystemRoot: "D:\\Windows" });
|
|
15
|
+
assert.equal(actual, path.win32.join("D:\\Windows", "System32", "wscript.exe"));
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
test("buildWindowsHiddenLauncherCommand uses wscript in batch mode", () => {
|
|
19
|
+
const actual = buildWindowsHiddenLauncherCommand("C:\\runtime\\launcher.vbs", {
|
|
20
|
+
SystemRoot: "C:\\Windows",
|
|
21
|
+
});
|
|
22
|
+
assert.equal(
|
|
23
|
+
actual,
|
|
24
|
+
'"C:\\Windows\\System32\\wscript.exe" //B //Nologo "C:\\runtime\\launcher.vbs"',
|
|
25
|
+
);
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
test("buildWindowsHiddenLauncherContent starts daemon in hidden mode", () => {
|
|
29
|
+
const content = buildWindowsHiddenLauncherContent({
|
|
30
|
+
nodeBin: "C:\\Program Files\\nodejs\\node.exe",
|
|
31
|
+
daemonScriptPath: "C:\\Program Files\\copilot-hub\\scripts\\dist\\daemon.mjs",
|
|
32
|
+
runtimeDir: "C:\\Users\\amine\\AppData\\Roaming\\copilot-hub\\runtime",
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
assert.match(content, /CreateObject\("WScript\.Shell"\)/);
|
|
36
|
+
assert.match(content, /shell\.Run/);
|
|
37
|
+
assert.match(content, /, 0, False/);
|
|
38
|
+
assert.match(content, /"start"/);
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
test("ensureWindowsHiddenLauncher writes and preserves launcher content", () => {
|
|
42
|
+
const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), "copilot-hub-launcher-"));
|
|
43
|
+
const scriptPath = path.join(tempDir, "windows-daemon-launcher.vbs");
|
|
44
|
+
|
|
45
|
+
try {
|
|
46
|
+
ensureWindowsHiddenLauncher({
|
|
47
|
+
scriptPath,
|
|
48
|
+
nodeBin: "C:\\node.exe",
|
|
49
|
+
daemonScriptPath: "C:\\copilot-hub\\daemon.mjs",
|
|
50
|
+
runtimeDir: "C:\\copilot-hub\\runtime",
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
const first = fs.readFileSync(scriptPath, "utf8");
|
|
54
|
+
ensureWindowsHiddenLauncher({
|
|
55
|
+
scriptPath,
|
|
56
|
+
nodeBin: "C:\\node.exe",
|
|
57
|
+
daemonScriptPath: "C:\\copilot-hub\\daemon.mjs",
|
|
58
|
+
runtimeDir: "C:\\copilot-hub\\runtime",
|
|
59
|
+
});
|
|
60
|
+
const second = fs.readFileSync(scriptPath, "utf8");
|
|
61
|
+
|
|
62
|
+
assert.equal(second, first);
|
|
63
|
+
} finally {
|
|
64
|
+
fs.rmSync(tempDir, { recursive: true, force: true });
|
|
65
|
+
}
|
|
66
|
+
});
|