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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "copilot-hub",
3
- "version": "0.1.23",
3
+ "version": "0.1.24",
4
4
  "description": "Copilot Hub CLI and runtime bundle",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -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: process.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
- console.log(`[daemon] started (pid ${pid})`);
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 ?? ""),
@@ -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
- runDaemon("start");
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
- runDaemon("start");
225
- return;
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
- return `"${nodeBin}" "${daemonScriptPath}" run`;
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
+ }
@@ -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
- console.log(`[daemon] already running (pid ${existingPid})`);
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: process.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
- console.log(`[daemon] started (pid ${pid})`);
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 ?? ""),
@@ -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
- runDaemon("start");
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
- runDaemon("start");
287
- return;
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
- return `"${nodeBin}" "${daemonScriptPath}" run`;
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
+ });