@vellumai/cli 0.11.7-staging.2 → 0.11.8-staging.1

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.
Files changed (61) hide show
  1. package/AGENTS.md +1 -1
  2. package/node_modules/@vellumai/environments/package.json +2 -1
  3. package/node_modules/@vellumai/environments/src/__tests__/test-path-guard.test.ts +89 -0
  4. package/node_modules/@vellumai/environments/src/test-path-guard.ts +113 -0
  5. package/node_modules/@vellumai/local-mode/node_modules/@vellumai/environments/package.json +2 -1
  6. package/node_modules/@vellumai/local-mode/node_modules/@vellumai/environments/src/__tests__/test-path-guard.test.ts +89 -0
  7. package/node_modules/@vellumai/local-mode/node_modules/@vellumai/environments/src/test-path-guard.ts +113 -0
  8. package/node_modules/@vellumai/local-mode/node_modules/@vellumai/service-contracts/package.json +1 -0
  9. package/node_modules/@vellumai/local-mode/node_modules/@vellumai/service-contracts/src/channels.ts +7 -0
  10. package/node_modules/@vellumai/local-mode/node_modules/@vellumai/service-contracts/src/index.ts +1 -0
  11. package/node_modules/@vellumai/local-mode/node_modules/@vellumai/service-contracts/src/no-response.ts +84 -0
  12. package/node_modules/@vellumai/local-mode/src/hatch.ts +17 -2
  13. package/node_modules/@vellumai/local-mode/src/index.ts +5 -0
  14. package/node_modules/@vellumai/local-mode/src/lifecycle-budgets.ts +47 -0
  15. package/node_modules/@vellumai/local-mode/src/retire.ts +9 -3
  16. package/node_modules/@vellumai/local-mode/src/sleep.ts +5 -5
  17. package/node_modules/@vellumai/service-contracts/package.json +1 -0
  18. package/node_modules/@vellumai/service-contracts/src/channels.ts +7 -0
  19. package/node_modules/@vellumai/service-contracts/src/index.ts +1 -0
  20. package/node_modules/@vellumai/service-contracts/src/no-response.ts +84 -0
  21. package/package.json +4 -3
  22. package/src/__tests__/assistant-client-platform-auth.test.ts +219 -0
  23. package/src/__tests__/clean.test.ts +1 -0
  24. package/src/__tests__/nginx-ingress.test.ts +123 -8
  25. package/src/__tests__/retire-local.test.ts +32 -1
  26. package/src/__tests__/sleep.test.ts +5 -3
  27. package/src/__tests__/teleport.test.ts +1 -0
  28. package/src/__tests__/upgrade-local.test.ts +116 -0
  29. package/src/__tests__/windows-lifecycle.test.ts +77 -5
  30. package/src/adapters/openclaw-http-server.ts +5 -0
  31. package/src/commands/client.ts +35 -7
  32. package/src/commands/exec.ts +8 -2
  33. package/src/commands/logs.ts +11 -2
  34. package/src/commands/sleep.ts +6 -9
  35. package/src/commands/ssh.ts +6 -3
  36. package/src/commands/teleport.ts +6 -1
  37. package/src/commands/upgrade.ts +22 -12
  38. package/src/commands/voice.test.ts +81 -0
  39. package/src/commands/voice.ts +83 -6
  40. package/src/commands/wake.ts +7 -1
  41. package/src/lib/assistant-client.ts +145 -31
  42. package/src/lib/cloudflare-tunnel.ts +2 -1
  43. package/src/lib/detached-process.ts +1 -0
  44. package/src/lib/guardian-token.ts +4 -2
  45. package/src/lib/hatch-local.ts +5 -1
  46. package/src/lib/live-voice/client.ts +23 -2
  47. package/src/lib/live-voice/connection.ts +87 -10
  48. package/src/lib/live-voice/pcm-player.ts +16 -3
  49. package/src/lib/live-voice/velay.test.ts +99 -0
  50. package/src/lib/live-voice/velay.ts +199 -0
  51. package/src/lib/local-hatch-lock.ts +16 -3
  52. package/src/lib/local.ts +48 -13
  53. package/src/lib/nginx-ingress.ts +70 -10
  54. package/src/lib/ngrok.ts +2 -0
  55. package/src/lib/open-browser.ts +5 -1
  56. package/src/lib/process.ts +72 -10
  57. package/src/lib/provider-secrets.ts +5 -0
  58. package/src/lib/retire-local.ts +12 -5
  59. package/src/lib/step-runner.ts +3 -0
  60. package/src/lib/tailscale-tunnel.ts +6 -1
  61. package/src/lib/upgrade-lifecycle.ts +1 -0
@@ -180,7 +180,7 @@ function remoteWebIngressConfig(
180
180
  * fingerprint matches, so this must change whenever the generated index or
181
181
  * nginx template does.
182
182
  */
183
- const EDGE_TEMPLATE_VERSION = 5;
183
+ export const EDGE_TEMPLATE_VERSION = 7;
184
184
 
185
185
  /**
186
186
  * Stable fingerprint of the SPA config injected into the served index and
@@ -201,19 +201,56 @@ function safeScriptJson(value: unknown): string {
201
201
  }
202
202
 
203
203
  /**
204
- * Preloading the whole chunk graph opens ~290 tunnel connections on a cold
205
- * load, and one dropped request blanks the app before React can report it.
206
- * These are hints only; the entry module still pulls what it needs.
204
+ * How many modulepreload hints the served index may carry before they are
205
+ * stripped wholesale.
206
+ *
207
+ * The consolidated boot graph emits 4 hints (the entry chunk's static
208
+ * imports). Serving them lets a phone fetch the whole boot set in parallel;
209
+ * without them the browser cannot discover the other chunks until the entry
210
+ * has downloaded AND parsed, so ~515 kB gzip serializes behind ~623 kB plus
211
+ * a phone-CPU parse of ~2.2 MB of JavaScript.
212
+ *
213
+ * The strip exists for the pathological shape: a whole-chunk-graph index
214
+ * (~290 hints) floods the tunnel with requests, and one dropped *import*
215
+ * blanks the app before React can report it. A build that regresses toward
216
+ * that shape trips this threshold and gets the protective strip.
217
+ * All-or-nothing, because preserving an arbitrary prefix of a broken graph
218
+ * would be luck, not policy. 12 sits comfortably above the consolidated
219
+ * graph and within two rounds of the browser's six-per-origin HTTP/1.1
220
+ * connection pool, so kept hints never widen concurrency much beyond what
221
+ * the entry's own imports would open.
222
+ */
223
+ export const MAX_PRESERVED_MODULE_PRELOADS = 12;
224
+
225
+ const MODULE_PRELOAD_RE = /<link[^>]+rel="modulepreload"[^>]*>\s*/g;
226
+
227
+ /**
228
+ * Failure semantics are engine-dependent while whatwg/html#10327 rolls out:
229
+ * older engines cache a failed module fetch in the module map (a later
230
+ * import returns the failure without refetching), newer ones evict it so a
231
+ * later import refetches. The invariant that matters holds in both models:
232
+ * the preserved hints name exactly the URLs the entry's static imports
233
+ * fetch anyway, through the same module map, so a dropped request for a
234
+ * BOOT chunk has the same outcome whether or not the chunk was hinted.
235
+ * Hints for the boot set add no failure surface; they only start the same
236
+ * fetches earlier. What the strip guards against is the whole-graph shape,
237
+ * where hundreds of speculative NON-boot fetches amplify the odds of any
238
+ * failure and (on old-model engines) a dropped lazy chunk poisons a later
239
+ * dynamic import.
207
240
  */
208
- function stripModulePreloads(html: string): string {
209
- return html.replace(/<link[^>]+rel="modulepreload"[^>]*>\s*/g, "");
241
+ function stripExcessModulePreloads(html: string): string {
242
+ const count = html.match(MODULE_PRELOAD_RE)?.length ?? 0;
243
+ if (count <= MAX_PRESERVED_MODULE_PRELOADS) {
244
+ return html;
245
+ }
246
+ return html.replace(MODULE_PRELOAD_RE, "");
210
247
  }
211
248
 
212
249
  export function buildRemoteWebIndexHtml(
213
250
  rawHtml: string,
214
251
  config: Record<string, unknown>,
215
252
  ): string {
216
- const html = stripModulePreloads(rawHtml);
253
+ const html = stripExcessModulePreloads(rawHtml);
217
254
  const script = `<script>window.__VELLUM_CONFIG__=${safeScriptJson(config)}</script>`;
218
255
  if (html.includes("</head>")) {
219
256
  return html.replace("</head>", `${script}</head>`);
@@ -274,6 +311,19 @@ http {
274
311
  access_log off;
275
312
  default_type application/octet-stream;
276
313
 
314
+ # Compression tuning only. It stays inert here because nginx ships with
315
+ # gzip off, and only the static SPA locations below turn it on: this server
316
+ # also proxies authenticated /v1 and /webhooks traffic, and compressing a
317
+ # response that carries both a secret and attacker-influenced content over
318
+ # TLS is the BREACH side channel. The SPA bytes are the whole win, so the
319
+ # boundary is drawn by opting locations in rather than by excluding proxies.
320
+ # text/html is always compressed once gzip is on, so it must not be
321
+ # repeated in gzip_types.
322
+ gzip_vary on;
323
+ gzip_comp_level 5;
324
+ gzip_min_length 1024;
325
+ gzip_types application/javascript application/json application/wasm image/svg+xml text/css text/plain;
326
+
277
327
  types {
278
328
  application/javascript js mjs;
279
329
  application/json json map;
@@ -369,10 +419,16 @@ ${proxyBlock}
369
419
  rewrite ^ /assistant/__remote-index.html last;
370
420
  }
371
421
 
422
+ # The shell and the unhashed files beside it revalidate rather than
423
+ # refusing storage: they carry no credential, and nginx serves them from
424
+ # disk with a validator, so a repeat load costs a 304 instead of the whole
425
+ # document. __config is the exception below: it is returned inline, so
426
+ # nginx attaches no validator for a revalidation to match against.
372
427
  location = /assistant/__remote-index.html {
373
428
  internal;
429
+ gzip on;
374
430
  alias ${nginxQuoted(indexHtmlPath, "remote web ingress index path")};
375
- add_header Cache-Control "no-store";
431
+ add_header Cache-Control "no-cache";
376
432
  }
377
433
 
378
434
  location = /assistant/__config {
@@ -382,15 +438,17 @@ ${proxyBlock}
382
438
  }
383
439
 
384
440
  location ^~ /assistant/assets/ {
441
+ gzip on;
385
442
  alias ${nginxQuoted(nginxDirPath(webAssetsDir), "web assets path")};
386
443
  try_files $uri =404;
387
444
  add_header Cache-Control "public, max-age=31536000, immutable";
388
445
  }
389
446
 
390
447
  location ^~ /assistant/ {
448
+ gzip on;
391
449
  alias ${nginxQuoted(webDistDir, "web dist path")};
392
450
  try_files $uri $uri/ /assistant/__remote-index.html;
393
- add_header Cache-Control "no-store";
451
+ add_header Cache-Control "no-cache";
394
452
  }
395
453
 
396
454
  location = / {
@@ -415,6 +473,7 @@ export function getNginxVersion(): string | null {
415
473
  const result = spawnSync(nginxBin(), ["-v"], {
416
474
  encoding: "utf-8",
417
475
  timeout: 5_000,
476
+ windowsHide: true,
418
477
  });
419
478
  if (result.error || result.status !== 0) return null;
420
479
  const output = `${result.stderr || ""}${result.stdout || ""}`.trim();
@@ -464,6 +523,7 @@ function isIngressNginxProcess(pid: number, paths: IngressPaths): boolean {
464
523
  encoding: "utf-8",
465
524
  timeout: 3000,
466
525
  stdio: ["ignore", "pipe", "ignore"],
526
+ windowsHide: true,
467
527
  },
468
528
  ).trim();
469
529
  return (
@@ -604,7 +664,7 @@ export function startIngressNginx(opts: {
604
664
  const child = spawn(
605
665
  nginxBin(),
606
666
  ["-p", paths.dir, "-c", paths.confPath, "-g", "daemon off;"],
607
- { detached: true, stdio: ["ignore", fd, fd] },
667
+ { detached: true, stdio: ["ignore", fd, fd], windowsHide: true },
608
668
  );
609
669
  closeSync(fd);
610
670
 
package/src/lib/ngrok.ts CHANGED
@@ -36,6 +36,7 @@ export function getNgrokVersion(): string | null {
36
36
  encoding: "utf-8",
37
37
  timeout: 5_000,
38
38
  stdio: ["ignore", "pipe", "ignore"],
39
+ windowsHide: true,
39
40
  });
40
41
  return output.trim();
41
42
  } catch {
@@ -152,6 +153,7 @@ export function startNgrokProcess(
152
153
  const child = spawn("ngrok", args, {
153
154
  detached: true,
154
155
  stdio,
156
+ windowsHide: true,
155
157
  });
156
158
 
157
159
  // The child process inherits a duplicate of the fd via dup2, so the
@@ -12,7 +12,11 @@ export function openBrowser(url: string): void {
12
12
  platform === "win32"
13
13
  ? ["/c", "start", '""', url.replace(/&/g, "^&")]
14
14
  : [url];
15
- const child = spawn(cmd, args, { stdio: "ignore", detached: true });
15
+ const child = spawn(cmd, args, {
16
+ stdio: "ignore",
17
+ detached: true,
18
+ windowsHide: true,
19
+ });
16
20
  child.on("error", () => {
17
21
  // Silently ignore — the user can still copy the URL from the console.
18
22
  });
@@ -76,13 +76,22 @@ export function windowsCommandLineLookupArgs(pid: number): string[] {
76
76
  ];
77
77
  }
78
78
 
79
- function readWindowsCommandLine(pid: number): string {
80
- return execFileSync("powershell.exe", windowsCommandLineLookupArgs(pid), {
79
+ /** Read a process command line using the platform's process inspection tool. */
80
+ export function readProcessCommandLine(
81
+ pid: number,
82
+ hostPlatform: NodeJS.Platform = platform(),
83
+ ): string {
84
+ const command = hostPlatform === "win32" ? "powershell.exe" : "ps";
85
+ const args =
86
+ hostPlatform === "win32"
87
+ ? windowsCommandLineLookupArgs(pid)
88
+ : ["-p", String(pid), "-o", "command="];
89
+ return execFileSync(command, args, {
81
90
  windowsHide: true,
82
91
  encoding: "utf8",
83
92
  timeout: 3000,
84
93
  stdio: ["ignore", "pipe", "ignore"],
85
- });
94
+ }).trim();
86
95
  }
87
96
 
88
97
  export function isVellumWindowsProcess(
@@ -102,6 +111,21 @@ export function isVellumWindowsProcess(
102
111
  return /^bun\.exe$/i.test(imageName) && isVellumCommandLine(commandLine);
103
112
  }
104
113
 
114
+ /**
115
+ * The POSIX command line for `pid`, or null when it cannot be read.
116
+ *
117
+ * Used to prove a PID still names the same process across an awaited gap. The
118
+ * Windows path never needs it: `taskkill /T` already terminates the tree, so
119
+ * no group escalation happens there.
120
+ */
121
+ export function readPosixCommandLine(pid: number): string | null {
122
+ try {
123
+ return readProcessCommandLine(pid, platform());
124
+ } catch {
125
+ return null;
126
+ }
127
+ }
128
+
105
129
  export function isVellumProcess(
106
130
  pid: number,
107
131
  hostPlatform: NodeJS.Platform = platform(),
@@ -110,15 +134,11 @@ export function isVellumProcess(
110
134
  if (hostPlatform === "win32") {
111
135
  const imageName = readWindowsProcesses(pid)[0]?.imageName ?? "";
112
136
  const commandLine = /^(?:bun|qdrant)\.exe$/i.test(imageName)
113
- ? readWindowsCommandLine(pid)
137
+ ? readProcessCommandLine(pid, hostPlatform)
114
138
  : "";
115
139
  return isVellumWindowsProcess(imageName, commandLine);
116
140
  }
117
- const output = execFileSync("ps", ["-p", String(pid), "-o", "command="], {
118
- encoding: "utf-8",
119
- timeout: 3000,
120
- stdio: ["ignore", "pipe", "ignore"],
121
- }).trim();
141
+ const output = readProcessCommandLine(pid, hostPlatform);
122
142
  return isVellumCommandLine(output);
123
143
  } catch {
124
144
  return false;
@@ -277,6 +297,15 @@ export async function resolveProcessState(
277
297
  return { status: "healthy", pid: result.pid };
278
298
  }
279
299
 
300
+ /**
301
+ * SIGKILL ceiling for stopping the assistant daemon.
302
+ *
303
+ * Defined in `@vellumai/local-mode` because the host wrappers that spawn these
304
+ * commands derive their own timeouts from it: a wrapper that expires first
305
+ * kills the CLI before this ceiling is ever reached.
306
+ */
307
+ export { DAEMON_STOP_TIMEOUT_MS } from "@vellumai/local-mode";
308
+
280
309
  /**
281
310
  * Stop a process by PID: SIGTERM, wait up to `timeoutMs`, then SIGKILL if still alive.
282
311
  * Returns true if the process was stopped, false if it wasn't alive or
@@ -294,6 +323,7 @@ export async function stopProcess(
294
323
  stdio: "ignore",
295
324
  });
296
325
  },
326
+ readIdentity: (pid: number) => string | null = readPosixCommandLine,
297
327
  ): Promise<boolean> {
298
328
  try {
299
329
  process.kill(pid, 0);
@@ -301,6 +331,11 @@ export async function stopProcess(
301
331
  return false;
302
332
  }
303
333
 
334
+ // Snapshot who this PID is before any waiting, so the group escalation below
335
+ // can prove it is still the same process rather than a recycled PID.
336
+ const identityBeforeWait =
337
+ hostPlatform === "win32" ? null : readIdentity(pid);
338
+
304
339
  console.log(`Stopping ${label} (pid ${pid})...`);
305
340
  let waitForGracefulExit = true;
306
341
  if (hostPlatform === "win32") {
@@ -343,6 +378,32 @@ export async function stopProcess(
343
378
  }
344
379
  }
345
380
  console.log(`${label} did not exit after SIGTERM, sending SIGKILL...`);
381
+ // Kill the process group, not just the process. Daemons are spawned
382
+ // `detached: true` (a session and group leader), and their worker
383
+ // subprocesses inherit that group, so a single-PID SIGKILL here is what
384
+ // orphans workers onto init running a stale runtime. Group-kill takes the
385
+ // whole tree at once, which is what the Windows branch above already does
386
+ // via `taskkill /T`. Tool subprocesses run in their own groups (see
387
+ // assistant orphan-reaper), so they are unaffected. SIGTERM above stays
388
+ // single-PID on purpose: graceful shutdown is the daemon's to orchestrate,
389
+ // and a group-wide SIGTERM would kill the route host mid-HTTP-drain.
390
+ //
391
+ // Widening a signal to a whole group demands more than liveness: the target
392
+ // may have exited during the wait and the OS may have handed its PID to an
393
+ // unrelated group leader. Escalate only while the command line still matches
394
+ // the one captured before the wait; otherwise fall through to the
395
+ // single-PID kill this function has always sent.
396
+ const identityHolds =
397
+ identityBeforeWait != null && readIdentity(pid) === identityBeforeWait;
398
+ if (identityHolds) {
399
+ try {
400
+ process.kill(-pid, "SIGKILL");
401
+ return true;
402
+ } catch {
403
+ // `pid` does not lead a group (or the group is already gone). Fall back
404
+ // to the single-PID kill.
405
+ }
406
+ }
346
407
  try {
347
408
  process.kill(pid, "SIGKILL");
348
409
  return true;
@@ -428,7 +489,7 @@ export async function stopOrphanedDaemonProcesses(
428
489
  (/^vellum-daemon\.exe$/i.test(imageName) ||
429
490
  (/^bun\.exe$/i.test(imageName) &&
430
491
  /vellum-daemon|[\\/]assistant[\\/]src[\\/](?:index|daemon[\\/]main)\.ts/i.test(
431
- readWindowsCommandLine(pid),
492
+ readProcessCommandLine(pid, hostPlatform),
432
493
  ))),
433
494
  )
434
495
  .map(({ pid }) =>
@@ -446,6 +507,7 @@ export async function stopOrphanedDaemonProcesses(
446
507
  encoding: "utf-8",
447
508
  timeout: 5000,
448
509
  stdio: ["ignore", "pipe", "ignore"],
510
+ windowsHide: true,
449
511
  });
450
512
  } catch {
451
513
  return false;
@@ -243,6 +243,11 @@ export function inferProviderFromModel(model: string): string | undefined {
243
243
  if (model.startsWith("poolside/")) {
244
244
  return "poolside";
245
245
  }
246
+ if (model === "qwen/qwen3-8b") {
247
+ // The only qwen/* ID the Vellum hosted provider carries; the rest are
248
+ // OpenRouter's.
249
+ return "vellum";
250
+ }
246
251
  if (model.includes("/")) {
247
252
  return "openrouter";
248
253
  }
@@ -12,6 +12,7 @@ import type { AssistantEntry } from "./assistant-config.js";
12
12
  import { stopIngressNginx } from "./nginx-ingress.js";
13
13
  import { getKnownPidsFromAssistants } from "./orphan-detection.js";
14
14
  import {
15
+ DAEMON_STOP_TIMEOUT_MS,
15
16
  stopOrphanedDaemonProcesses,
16
17
  stopProcessByPidFile,
17
18
  } from "./process.js";
@@ -119,7 +120,12 @@ export async function retireLocal(
119
120
  }
120
121
 
121
122
  const daemonPidFile = getDaemonPidPath(resources);
122
- const daemonStopped = await stopProcessByPidFile(daemonPidFile, "daemon");
123
+ const daemonStopped = await stopProcessByPidFile(
124
+ daemonPidFile,
125
+ "daemon",
126
+ undefined,
127
+ DAEMON_STOP_TIMEOUT_MS,
128
+ );
123
129
 
124
130
  // Stop gateway via PID file — use a longer timeout because the gateway has a
125
131
  // drain window (5s) before it exits.
@@ -137,10 +143,10 @@ export async function retireLocal(
137
143
  reporter.log("credential-executor stopped.");
138
144
  }
139
145
 
140
- // Stop Qdrant — the daemon's graceful shutdown tries to stop it via
141
- // qdrantManager.stop(), but if the daemon was SIGKILL'd (after 2s timeout)
142
- // Qdrant may still be running as an orphan. Check both the current PID file
143
- // location and the legacy location.
146
+ // Stop Qdrant. The daemon's graceful shutdown tries to stop it via
147
+ // qdrantManager.stop(), but a daemon that never completes that shutdown
148
+ // (crash, OOM kill, or the SIGKILL ceiling above) leaves Qdrant running as
149
+ // an orphan. Check both the current PID file location and the legacy one.
144
150
  const qdrantPidFile = join(
145
151
  vellumDir,
146
152
  "workspace",
@@ -211,6 +217,7 @@ export async function retireLocal(
211
217
  const child = spawn(archiveCommand.command, archiveCommand.args, {
212
218
  stdio: "ignore",
213
219
  detached: true,
220
+ windowsHide: true,
214
221
  ...(archiveCommand.env
215
222
  ? { env: { ...process.env, ...archiveCommand.env } }
216
223
  : {}),
@@ -42,6 +42,7 @@ export function exec(
42
42
  const child = spawn(command, args, {
43
43
  cwd: options.cwd,
44
44
  stdio: ["pipe", "pipe", "pipe"],
45
+ windowsHide: true,
45
46
  });
46
47
 
47
48
  let stdout = "";
@@ -82,6 +83,7 @@ export function execWithStdin(
82
83
  const child = spawn(command, args, {
83
84
  cwd: options.cwd,
84
85
  stdio: ["pipe", "pipe", "pipe"],
86
+ windowsHide: true,
85
87
  });
86
88
 
87
89
  let stdout = "";
@@ -116,6 +118,7 @@ export function execOutput(
116
118
  const child = spawn(command, args, {
117
119
  cwd: options.cwd,
118
120
  stdio: ["pipe", "pipe", "pipe"],
121
+ windowsHide: true,
119
122
  });
120
123
 
121
124
  let settled = false;
@@ -88,6 +88,7 @@ function realFindBinary(): string | null {
88
88
  encoding: "utf-8",
89
89
  timeout: 5_000,
90
90
  stdio: ["ignore", "pipe", "ignore"],
91
+ windowsHide: true,
91
92
  });
92
93
  if (!res.error && res.status === 0) {
93
94
  return candidate;
@@ -97,7 +98,11 @@ function realFindBinary(): string | null {
97
98
  }
98
99
 
99
100
  function realRun(bin: string, args: string[]): TailscaleCommandResult {
100
- const res = spawnSync(bin, args, { encoding: "utf-8", timeout: 15_000 });
101
+ const res = spawnSync(bin, args, {
102
+ encoding: "utf-8",
103
+ timeout: 15_000,
104
+ windowsHide: true,
105
+ });
101
106
  if (res.error) {
102
107
  throw res.error;
103
108
  }
@@ -74,6 +74,7 @@ export async function captureUpgradeFailureLogs(
74
74
  {
75
75
  encoding: "utf8",
76
76
  maxBuffer: 10 * 1024 * 1024, // 10 MB
77
+ windowsHide: true,
77
78
  },
78
79
  );
79
80
  const output = [result.stdout, result.stderr].filter(Boolean).join("");