windows-development-sandbox 1.27.0 → 1.27.3

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 (93) hide show
  1. package/dist/app/context.d.ts +5 -0
  2. package/dist/app/context.js +13 -0
  3. package/dist/app/features/accounts/accounts.def.d.ts +16 -0
  4. package/dist/app/features/accounts/accounts.run.d.ts +4 -0
  5. package/dist/app/features/accounts/services.def.d.ts +15 -1
  6. package/dist/app/features/accounts/services.def.js +67 -31
  7. package/dist/app/features/accounts/services.run.d.ts +7 -2
  8. package/dist/app/features/accounts/services.run.js +25 -6
  9. package/dist/app/features/defs.d.ts +17 -0
  10. package/dist/app/features/unreal/unreal.run.js +13 -3
  11. package/dist/app/registry.d.ts +1 -1
  12. package/dist/app/registry.js +15 -2
  13. package/dist/bin/agent-sandbox.js +2 -2
  14. package/dist/bin/{chunk-JXL6ZOO3.js → chunk-4M5SZZ5P.js} +46 -41
  15. package/dist/bin/{chunk-ZXCAVVS4.js → chunk-FA5VWLP4.js} +1 -1
  16. package/dist/bin/{chunk-6WWMBCT2.js → chunk-RU7XRN62.js} +303 -112
  17. package/dist/bin/claude-host.js +2 -2
  18. package/dist/bin/claude-sbx.js +2 -2
  19. package/dist/bin/codex-sbx.js +2 -2
  20. package/dist/bin/gemini-sbx.js +2 -2
  21. package/dist/bin/sandbox.js +2 -2
  22. package/dist/bin/sbx-hide.js +2 -2
  23. package/dist/bin/sbx-show.js +2 -2
  24. package/dist/cli/commands/launch/launch.js +2 -1
  25. package/dist/cli/menu/menu.js +6 -1
  26. package/dist/cli/menu/pages/accounts-page.js +9 -5
  27. package/dist/guest/apps/app-installer.js +7 -5
  28. package/dist/guest/apps/comet-unpack.js +2 -2
  29. package/dist/guest/runtime/pools.d.ts +4 -4
  30. package/dist/guest/runtime/pools.js +4 -15
  31. package/dist/guest/runtime/process.d.ts +5 -2
  32. package/dist/guest/runtime/process.js +23 -3
  33. package/dist/guest/setup/unity-agent.js +4 -1
  34. package/dist/guest/toolchains/engines/epic-launcher.d.ts +7 -1
  35. package/dist/guest/toolchains/engines/epic-launcher.js +30 -8
  36. package/dist/guest/toolchains/engines/unreal-engines.js +2 -1
  37. package/dist/guest/toolchains/engines/unreal-status.js +1 -1
  38. package/dist/guest/toolchains/installers.d.ts +3 -3
  39. package/dist/guest/toolchains/installers.js +5 -5
  40. package/dist/guest/toolchains/native/mesa.js +35 -11
  41. package/dist/guest/toolchains/native/unreal.js +1 -0
  42. package/dist/guest/toolchains/native/vcruntime.js +2 -2
  43. package/dist/guest/toolchains/native/vs-persist.d.ts +2 -2
  44. package/dist/guest/toolchains/signature.d.ts +2 -0
  45. package/dist/guest/toolchains/signature.js +9 -2
  46. package/dist/guest/toolchains/step-scheduler.js +1 -1
  47. package/dist/host/accounts/deliver.d.ts +3 -0
  48. package/dist/host/accounts/deliver.js +27 -0
  49. package/dist/host/accounts/source.d.ts +2 -0
  50. package/dist/host/accounts/source.js +2 -0
  51. package/dist/host/pc-installs/discover.js +8 -4
  52. package/dist/host/pc-installs/epic.js +3 -0
  53. package/dist/host/pc-installs/unity.d.ts +2 -0
  54. package/dist/host/pc-installs/unity.js +12 -3
  55. package/dist/host/pc-installs/unreal.d.ts +5 -1
  56. package/dist/host/pc-installs/unreal.js +51 -9
  57. package/dist/runtime/agent.js +34 -22
  58. package/dist/runtime/daemon.js +134 -65
  59. package/dist/runtime/db.js +32 -16
  60. package/dist/runtime/main.js +136 -67
  61. package/dist/runtime/netproxy.js +26 -12
  62. package/dist/runtime/tool.js +32 -15
  63. package/dist/runtime/view.js +26 -12
  64. package/dist/selftest/app.test.js +20 -2
  65. package/dist/selftest/boot-runner.test.js +15 -1
  66. package/dist/selftest/engines.test.js +52 -2
  67. package/dist/selftest/menu-screens.js +52 -1
  68. package/dist/selftest/menu.test.js +29 -3
  69. package/dist/selftest/mixamo.test.js +1 -1
  70. package/dist/selftest/service-accounts.test.js +88 -2
  71. package/dist/selftest/ui-text.test.js +4 -0
  72. package/dist/shared/accounts/kinds.js +9 -9
  73. package/dist/shared/apps/catalogue-gamedev.js +1 -0
  74. package/dist/shared/apps/catalogue.d.ts +2 -0
  75. package/dist/shared/concurrency/pools.js +1 -1
  76. package/dist/shared/download/archive.js +4 -2
  77. package/dist/shared/download/index.d.ts +1 -1
  78. package/dist/shared/download/index.js +1 -1
  79. package/dist/shared/download/spawn.d.ts +7 -1
  80. package/dist/shared/download/spawn.js +16 -1
  81. package/dist/shared/paths/pc-installs.d.ts +8 -0
  82. package/dist/shared/unreal/launcher-files.d.ts +7 -0
  83. package/dist/shared/unreal/launcher-files.js +32 -1
  84. package/dist/shared/unreal/status.d.ts +1 -0
  85. package/dist/shared/unreal/status.js +2 -0
  86. package/dist/web/index.html +1 -1
  87. package/dist/web/{main-VPMYTSUL.js → main-NKM7ECSB.js} +59 -59
  88. package/native/csharp/HcsHost/AssemblyInfo.cs +3 -3
  89. package/native/csharp/HostTools/AssemblyInfo.cs +3 -3
  90. package/native/csharp/StorageHelper/AssemblyInfo.cs +3 -3
  91. package/package.json +1 -1
  92. package/schemas/config.schema.json +9 -9
  93. package/schemas/sandbox-config.schema.json +9 -9
@@ -2,8 +2,8 @@
2
2
  import { createRequire as __createRequire } from 'node:module'; const require = __createRequire(import.meta.url);
3
3
  import {
4
4
  run
5
- } from "./chunk-JXL6ZOO3.js";
6
- import "./chunk-6WWMBCT2.js";
5
+ } from "./chunk-4M5SZZ5P.js";
6
+ import "./chunk-RU7XRN62.js";
7
7
 
8
8
  // src/bin/claude-host.ts
9
9
  var args = process.argv.slice(2);
@@ -2,8 +2,8 @@
2
2
  import { createRequire as __createRequire } from 'node:module'; const require = __createRequire(import.meta.url);
3
3
  import {
4
4
  run
5
- } from "./chunk-JXL6ZOO3.js";
6
- import "./chunk-6WWMBCT2.js";
5
+ } from "./chunk-4M5SZZ5P.js";
6
+ import "./chunk-RU7XRN62.js";
7
7
 
8
8
  // src/bin/claude-sbx.ts
9
9
  run(process.argv.slice(2), { harness: "claude" });
@@ -2,8 +2,8 @@
2
2
  import { createRequire as __createRequire } from 'node:module'; const require = __createRequire(import.meta.url);
3
3
  import {
4
4
  run
5
- } from "./chunk-JXL6ZOO3.js";
6
- import "./chunk-6WWMBCT2.js";
5
+ } from "./chunk-4M5SZZ5P.js";
6
+ import "./chunk-RU7XRN62.js";
7
7
 
8
8
  // src/bin/codex-sbx.ts
9
9
  run(process.argv.slice(2), { harness: "codex" });
@@ -2,8 +2,8 @@
2
2
  import { createRequire as __createRequire } from 'node:module'; const require = __createRequire(import.meta.url);
3
3
  import {
4
4
  run
5
- } from "./chunk-JXL6ZOO3.js";
6
- import "./chunk-6WWMBCT2.js";
5
+ } from "./chunk-4M5SZZ5P.js";
6
+ import "./chunk-RU7XRN62.js";
7
7
 
8
8
  // src/bin/gemini-sbx.ts
9
9
  run(process.argv.slice(2), { harness: "gemini" });
@@ -2,8 +2,8 @@
2
2
  import { createRequire as __createRequire } from 'node:module'; const require = __createRequire(import.meta.url);
3
3
  import {
4
4
  run
5
- } from "./chunk-JXL6ZOO3.js";
6
- import "./chunk-6WWMBCT2.js";
5
+ } from "./chunk-4M5SZZ5P.js";
6
+ import "./chunk-RU7XRN62.js";
7
7
 
8
8
  // src/bin/sandbox.ts
9
9
  run(process.argv.slice(2));
@@ -2,8 +2,8 @@
2
2
  import { createRequire as __createRequire } from 'node:module'; const require = __createRequire(import.meta.url);
3
3
  import {
4
4
  run
5
- } from "./chunk-JXL6ZOO3.js";
6
- import "./chunk-6WWMBCT2.js";
5
+ } from "./chunk-4M5SZZ5P.js";
6
+ import "./chunk-RU7XRN62.js";
7
7
 
8
8
  // src/bin/sbx-hide.ts
9
9
  run(process.argv.slice(2), { command: "hide" });
@@ -2,8 +2,8 @@
2
2
  import { createRequire as __createRequire } from 'node:module'; const require = __createRequire(import.meta.url);
3
3
  import {
4
4
  run
5
- } from "./chunk-JXL6ZOO3.js";
6
- import "./chunk-6WWMBCT2.js";
5
+ } from "./chunk-4M5SZZ5P.js";
6
+ import "./chunk-RU7XRN62.js";
7
7
 
8
8
  // src/bin/sbx-show.ts
9
9
  run(process.argv.slice(2), { command: "show" });
@@ -10,7 +10,7 @@ import { findSecretFiles } from '../../../host/profile/index.js';
10
10
  import { RepoGuardStore } from '../../../host/guard/index.js';
11
11
  import { hostPublicIp, prepareVpn, readVpnLine, vpnNotes } from '../../../host/vpn/index.js';
12
12
  import { prepareGithubToken } from '../../../host/github/index.js';
13
- import { prepareServiceAccounts } from '../../../host/accounts/index.js';
13
+ import { pcEnv, prepareServiceAccounts } from '../../../host/accounts/index.js';
14
14
  import { prepareStorePackages } from '../../../host/apps/index.js';
15
15
  import { storeApps } from '../../../shared/apps/index.js';
16
16
  import { discoverPcInstalls } from '../../../host/pc-installs/index.js';
@@ -194,6 +194,7 @@ class Launcher {
194
194
  launchId,
195
195
  // A new launch needs no sandbox running, so a lease held elsewhere is from one that stopped.
196
196
  isRunning: () => isSandboxRunning(),
197
+ pc: pcEnv(),
197
198
  });
198
199
  // Store apps (apps.install): their packages are fetched with this PC's own Store tooling.
199
200
  const store = generateOnly
@@ -8,6 +8,7 @@ import { errMsg } from '../../core/util/index.js';
8
8
  import { SandboxChannel } from '../../host/channel/index.js';
9
9
  import { RunDir } from '../../shared/channel/index.js';
10
10
  import { isSandboxRunning } from '../../host/sandbox/index.js';
11
+ import { reloadSettings } from '../../app/context.js';
11
12
  import { lookAtSandbox } from '../commands/shared-use.js';
12
13
  import { commandLine } from '../history.js';
13
14
  import { commandSpec } from '../manifest.js';
@@ -98,7 +99,11 @@ export class Menu {
98
99
  new SettingsPage(app, files, draft, ctx),
99
100
  new AccountsPage(app, ctx),
100
101
  new MaintenancePage(app, ctx),
101
- ], draft, (saved) => this.applyAgentCommands(saved));
102
+ ], draft, (saved) => {
103
+ // Ctrl+S wrote the file: every page and operation reads the new settings from here on.
104
+ reloadSettings(this.ctx);
105
+ return this.applyAgentCommands(saved);
106
+ });
102
107
  }
103
108
  /** Runs the menu until you leave it, or until a command that takes over the terminal (attach) has run. */
104
109
  async start() {
@@ -7,12 +7,16 @@ import { readQuery } from '../../../app/registry.js';
7
7
  import { MenuOperations } from '../io-menu.js';
8
8
  import { menuView } from '../render-view.js';
9
9
  import { ListPage } from './list-page.js';
10
+ /** The services the menu lists: those whose sign-in is written (the others would offer actions that cannot work). */
11
+ function shown(m) {
12
+ return m.services.filter((s) => s.ready);
13
+ }
10
14
  /** The menu's rows, keys and Enter labels of one agent's category. */
11
15
  function viewOf(m, category) {
12
16
  const agent = m.agents.find((a) => a.name === category);
13
17
  if (agent)
14
18
  return menuView(agentPage(agent), { category: agent.name, refresh: false });
15
- const service = m.services.find((s) => serviceCategory(s.kind) === category);
19
+ const service = shown(m).find((s) => serviceCategory(s.kind) === category);
16
20
  if (service)
17
21
  return menuView(servicePage(service), { category: serviceCategory(service.kind), refresh: false });
18
22
  return menuView({ title: 'Accounts', sections: [] }, { refresh: false });
@@ -21,7 +25,7 @@ function viewOf(m, category) {
21
25
  export function accountsOptions(m) {
22
26
  return {
23
27
  path: [],
24
- intro: 'Logins saved on this PC, per agent (Use puts one into the sandbox; new launches start with the default), and the service sign-ins sandboxes may get.',
28
+ intro: 'Logins saved on this PC, per agent, and the service sign-ins sandboxes may get.',
25
29
  categories: [
26
30
  ...m.agents.map((a) => ({
27
31
  id: a.name,
@@ -30,15 +34,15 @@ export function accountsOptions(m) {
30
34
  ? 'Saved ' + a.name + ' logins.'
31
35
  : "This agent's login is not kept between sandboxes: it cannot be pointed at a folder we control.",
32
36
  })),
33
- ...m.services.map((s) => ({
37
+ ...shown(m).map((s) => ({
34
38
  id: serviceCategory(s.kind),
35
39
  label: s.label,
36
- description: 'Your ' + s.label + ' sign-in for sandboxes that use ' + s.usedFor + '.',
40
+ description: 'For ' + s.usedFor + '.',
37
41
  })),
38
42
  ],
39
43
  items: [
40
44
  ...m.agents.flatMap((a) => menuView(agentPage(a), { category: a.name, refresh: false }).items),
41
- ...m.services.flatMap((s) => menuView(servicePage(s), { category: serviceCategory(s.kind), refresh: false }).items),
45
+ ...shown(m).flatMap((s) => menuView(servicePage(s), { category: serviceCategory(s.kind), refresh: false }).items),
42
46
  ],
43
47
  empty: 'No agents.',
44
48
  actions: (category) => viewOf(m, category).actions,
@@ -3,6 +3,7 @@
3
3
  import fs from 'node:fs';
4
4
  import path from 'node:path';
5
5
  import { appsToInstall, previewGuideLine } from '../../shared/apps/index.js';
6
+ import { asBackground } from '../../shared/concurrency/index.js';
6
7
  import { downloader, moveIntoPlace, stagingFolder } from '../../shared/download/index.js';
7
8
  import { safeRemove } from '../../shared/io/safe-remove.js';
8
9
  import { dryRun, launchDir } from '../runtime/paths.js';
@@ -28,8 +29,8 @@ const MARKER = '.agent-sandbox-app';
28
29
  function prefetchedFile(entry, fileName) {
29
30
  return path.join(tool('apps', ...entry.id.split('/')) + '.download', fileName.replace(/[\\/:*?"<>|]/g, '_'));
30
31
  }
31
- /** How many apps download and unpack at once (they are big: a few at a time keeps the disk and network busy). */
32
- const PARALLEL = 3;
32
+ /** How many apps unpack at once (as many as the heavy pool holds: more would only queue up behind it). */
33
+ const PARALLEL = 2;
33
34
  export class AppInstaller {
34
35
  env;
35
36
  options;
@@ -53,7 +54,8 @@ export class AppInstaller {
53
54
  !('npm' in e.source) &&
54
55
  e.id !== '7zip' &&
55
56
  (this.options.refresh || !exists(path.join(tool('apps', ...e.id.split('/')), MARKER))));
56
- await Promise.all(todo.map(async (entry) => {
57
+ // Behind the downloads other steps need now (the runtimes, Visual Studio): these only save time for the install.
58
+ await asBackground(() => Promise.all(todo.map(async (entry) => {
57
59
  try {
58
60
  const d = await resolve(entry);
59
61
  const file = prefetchedFile(entry, d.fileName);
@@ -67,7 +69,7 @@ export class AppInstaller {
67
69
  catch {
68
70
  /* the install resolves and downloads again, and reports the reason if it fails too */
69
71
  }
70
- }));
72
+ })));
71
73
  }
72
74
  /** Installs the entries (7-Zip first: the NSIS apps need it to unpack), then their shortcuts. */
73
75
  async installAll(entries) {
@@ -124,7 +126,7 @@ export class AppInstaller {
124
126
  throw new Error(entry.exe + ' not found in the download');
125
127
  await afterInstall(entry, exe);
126
128
  // A proxy or MCP server (agent add-ons) is started by the sandbox or the agent: no PATH entry, no shortcut.
127
- if (!entry.service && (entry.cli || entry.id === 'sysinternals')) {
129
+ if (!entry.service && (entry.cli || entry.onPath || entry.id === 'sysinternals')) {
128
130
  // rustup's rust-analyzer proxy (in cargo's bin folder) only errors until its component is added.
129
131
  if (entry.id === 'rust-analyzer')
130
132
  this.env.prepend.unshift(path.dirname(exe));
@@ -9,7 +9,7 @@ import { installedCometExe } from '../browser/comet.js';
9
9
  import { execAsync } from '../runtime/process.js';
10
10
  import { heavy } from '../../shared/concurrency/index.js';
11
11
  import { mkdirp } from '../runtime/fs.js';
12
- import { signedBy } from '../toolchains/signature.js';
12
+ import { signedByAsync } from '../toolchains/signature.js';
13
13
  /** The archives inside, last stage first: the first one found is opened next. */
14
14
  const INNER = [
15
15
  /^chrome\.7z$/i,
@@ -118,7 +118,7 @@ export async function installFromKept(dest, signer, what) {
118
118
  const installed = installedCometExe();
119
119
  if (fs.existsSync(installed))
120
120
  return installed;
121
- if (!signedBy(installer, signer)) {
121
+ if (!(await signedByAsync(installer, signer))) {
122
122
  fs.rmSync(path.join(dest, '.agent-sandbox-app'), { force: true });
123
123
  fs.rmSync(installer, { force: true });
124
124
  throw new Error(what + "'s installer is not signed by its vendor; deleted it (it is downloaded again next boot)");
@@ -1,10 +1,10 @@
1
- import { type SpawnOptions } from 'node:child_process';
2
1
  import { heavy, installer, net } from '../../shared/concurrency/index.js';
2
+ import { lowerPriority, type SpawnAsyncOptions } from '../../shared/download/index.js';
3
3
  export { net, heavy, installer };
4
4
  /** Called at the start of the boot and of the daemon: the downloader and the extractor use the pools from then on. */
5
5
  export declare function installPools(): void;
6
6
  /** Lowers a process (and the children it starts from now on) to below-normal priority; best effort. */
7
- export declare function belowNormal(pid: number | undefined): void;
7
+ export declare const belowNormal: typeof lowerPriority;
8
8
  export interface InstallerOptions {
9
9
  /** Below-normal priority for the installer and what it starts (the background Visual Studio). */
10
10
  readonly lowPriority?: boolean;
@@ -20,5 +20,5 @@ export interface InstallerOptions {
20
20
  export declare function runInstaller(file: string, args: string[], o?: InstallerOptions): Promise<number>;
21
21
  /** runInstaller that throws like execAsync when the installer fails. */
22
22
  export declare function runInstallerChecked(file: string, args: string[], o?: InstallerOptions): Promise<void>;
23
- /** execAsync for heavy local work (npm, uv, rustup ...): one of the two heavy slots. */
24
- export declare function execHeavy(file: string, args: string[], options?: SpawnOptions): Promise<void>;
23
+ /** execAsync for heavy local work (npm, uv, rustup ...): one of the two heavy slots, at below-normal priority. */
24
+ export declare function execHeavy(file: string, args: string[], options?: SpawnAsyncOptions): Promise<void>;
@@ -1,10 +1,9 @@
1
1
  // The guest's work pools: network bodies, heavy local work and installers. A task takes one slot at the leaf (the
2
2
  // call that downloads, unpacks or installs), never while it holds another: the limiter refuses nested pools.
3
- import os from 'node:os';
4
3
  import { spawn } from 'node:child_process';
5
4
  import path from 'node:path';
6
5
  import { heavy, installer, net, INSTALLER_WAIT_MS, installerRetry } from '../../shared/concurrency/index.js';
7
- import { downloader, limitExtraction } from '../../shared/download/index.js';
6
+ import { downloader, limitExtraction, lowerPriority } from '../../shared/download/index.js';
8
7
  import { log } from './log.js';
9
8
  import { execAsync } from './process.js';
10
9
  export { net, heavy, installer };
@@ -13,18 +12,8 @@ export function installPools() {
13
12
  downloader.limit = net;
14
13
  limitExtraction(heavy);
15
14
  }
16
- const BELOW_NORMAL = os.constants.priority.PRIORITY_BELOW_NORMAL;
17
15
  /** Lowers a process (and the children it starts from now on) to below-normal priority; best effort. */
18
- export function belowNormal(pid) {
19
- if (pid === undefined)
20
- return;
21
- try {
22
- os.setPriority(pid, BELOW_NORMAL);
23
- }
24
- catch {
25
- /* the process may have ended already; a normal priority only makes it a little slower to share the machine */
26
- }
27
- }
16
+ export const belowNormal = lowerPriority;
28
17
  /**
29
18
  * Runs an installer in the one installer slot and starts it again while another process holds the Windows Installer
30
19
  * lock (exit 1618). Resolves with the exit code of the last try.
@@ -54,7 +43,7 @@ export async function runInstallerChecked(file, args, o = {}) {
54
43
  if (code !== 0)
55
44
  throw new Error(path.basename(file) + ' ' + args.slice(0, 3).join(' ') + ' failed with exit code ' + code);
56
45
  }
57
- /** execAsync for heavy local work (npm, uv, rustup ...): one of the two heavy slots. */
46
+ /** execAsync for heavy local work (npm, uv, rustup ...): one of the two heavy slots, at below-normal priority. */
58
47
  export function execHeavy(file, args, options = {}) {
59
- return heavy.run(() => execAsync(file, args, options));
48
+ return heavy.run(() => execAsync(file, args, { ...options, lowPriority: true }));
60
49
  }
@@ -1,4 +1,5 @@
1
- import { type SpawnOptions, type SpawnSyncOptions } from 'node:child_process';
1
+ import { type SpawnSyncOptions } from 'node:child_process';
2
+ import { type SpawnAsyncOptions } from '../../shared/download/index.js';
2
3
  /** Takes the output of the programs started while the boot draws its progress (boot/boot-console.ts). */
3
4
  export interface OutputSink {
4
5
  /** Whether programs started now hand their output over instead of writing to the console. */
@@ -11,7 +12,7 @@ export declare function routeOutput(sink: OutputSink | null): void;
11
12
  /** Runs a program, streaming output, and throws with a readable message when it fails. */
12
13
  export declare function exec(file: string, args: string[], options?: SpawnSyncOptions): void;
13
14
  /** exec without blocking: other toolchains keep downloading while this one runs an installer. */
14
- export declare function execAsync(file: string, args: string[], options?: SpawnOptions): Promise<void>;
15
+ export declare function execAsync(file: string, args: string[], options?: SpawnAsyncOptions): Promise<void>;
15
16
  export declare function execOutput(file: string, args: string[], options?: SpawnSyncOptions): string;
16
17
  /** Runs sandbox\scripts\<name> (a .ps1 in the launch folder) with `args`; throws when it fails. */
17
18
  export declare function runScript(name: string, args?: readonly string[]): void;
@@ -19,6 +20,8 @@ export declare function runScript(name: string, args?: readonly string[]): void;
19
20
  export declare function runScriptAsync(name: string, args?: readonly string[]): Promise<void>;
20
21
  /** runScript for what it prints ('' when it fails). */
21
22
  export declare function scriptOutput(name: string, args?: readonly string[], options?: SpawnSyncOptions): string;
23
+ /** scriptOutput without blocking the event loop ('' when the script fails). */
24
+ export declare function scriptOutputAsync(name: string, args?: readonly string[]): Promise<string>;
22
25
  /** Runs sandbox\scripts\<name> with a JSON file (-<param> path) holding `data`, removed afterwards. */
23
26
  export declare function runScriptWithJson(name: string, param: string, data: unknown, args?: readonly string[]): void;
24
27
  /** runScriptWithJson without blocking the event loop. */
@@ -2,7 +2,7 @@ import fs from 'node:fs';
2
2
  import os from 'node:os';
3
3
  import path from 'node:path';
4
4
  import { spawn, spawnSync } from 'node:child_process';
5
- import { moveIntoPlace, spawnAsync, stagingFolder } from '../../shared/download/index.js';
5
+ import { lowerPriority, moveIntoPlace, spawnAsync, stagingFolder, } from '../../shared/download/index.js';
6
6
  import { safeRemove } from '../../shared/io/safe-remove.js';
7
7
  import { robocopyErrors, robocopyFailed, robocopyMeaning } from '../../shared/io/robocopy-result.js';
8
8
  import { SYSTEM } from '../../shared/system.js';
@@ -57,15 +57,18 @@ export function exec(file, args, options = {}) {
57
57
  }
58
58
  /** exec without blocking: other toolchains keep downloading while this one runs an installer. */
59
59
  export async function execAsync(file, args, options = {}) {
60
+ const { lowPriority, ...spawnOptions } = options;
60
61
  const code = captureOutput()
61
62
  ? await new Promise((resolve, reject) => {
62
- const child = spawn(file, args, { stdio: ['inherit', 'pipe', 'pipe'], windowsHide: true, ...options });
63
+ const child = spawn(file, args, { stdio: ['inherit', 'pipe', 'pipe'], windowsHide: true, ...spawnOptions });
64
+ if (lowPriority)
65
+ lowerPriority(child.pid);
63
66
  logLines(child.stdout);
64
67
  logLines(child.stderr);
65
68
  child.once('error', reject);
66
69
  child.once('close', (c) => resolve(c ?? -1));
67
70
  })
68
- : await spawnAsync(file, args, { stdio: 'inherit', ...options });
71
+ : await spawnAsync(file, args, { stdio: 'inherit', ...spawnOptions, ...(lowPriority ? { lowPriority } : {}) });
69
72
  if (code !== 0)
70
73
  throw new Error(path.basename(file) + ' ' + args.slice(0, 3).join(' ') + ' failed with exit code ' + code);
71
74
  }
@@ -131,6 +134,23 @@ export function runScriptAsync(name, args = []) {
131
134
  export function scriptOutput(name, args = [], options = {}) {
132
135
  return execOutput(SYSTEM.powershell(), scriptArgs(name, args), options);
133
136
  }
137
+ /** scriptOutput without blocking the event loop ('' when the script fails). */
138
+ export function scriptOutputAsync(name, args = []) {
139
+ return new Promise((resolve) => {
140
+ const child = spawn(SYSTEM.powershell(), scriptArgs(name, args), {
141
+ stdio: ['ignore', 'pipe', 'ignore'],
142
+ windowsHide: true,
143
+ });
144
+ let out = '';
145
+ child.stdout?.setEncoding('utf8');
146
+ child.stdout?.on('data', (chunk) => {
147
+ if (out.length < CAPTURE_BYTES)
148
+ out += chunk;
149
+ });
150
+ child.once('error', () => resolve(''));
151
+ child.once('close', (code) => resolve(code === 0 ? out.trim() : ''));
152
+ });
153
+ }
134
154
  /** Runs sandbox\scripts\<name> with a JSON file (-<param> path) holding `data`, removed afterwards. */
135
155
  export function runScriptWithJson(name, param, data, args = []) {
136
156
  const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'sbx-'));
@@ -10,7 +10,10 @@ import { setEngineMcp, writeAddonMcpServers } from './addon-mcp.js';
10
10
  async function syncSkills(unityExe, agent) {
11
11
  const home = agent === 'claude' ? { CLAUDE_CONFIG_DIR: claudeDir } : { CODEX_HOME: path.join(stateDir, 'codex') };
12
12
  try {
13
- await execAsync(unityExe, ['skill', 'sync', agent], { env: { ...process.env, ...UNITY_CLI_ENV, ...home } });
13
+ // The Unity CLI's client names: claude-code, codex.
14
+ await execAsync(unityExe, ['skill', 'sync', agent === 'claude' ? 'claude-code' : 'codex'], {
15
+ env: { ...process.env, ...UNITY_CLI_ENV, ...home },
16
+ });
14
17
  return true;
15
18
  }
16
19
  catch (e) {
@@ -1,5 +1,6 @@
1
+ import type { PcInstall } from '../../../shared/paths/pc-installs.js';
1
2
  import { type LauncherEntry } from '../../../shared/unreal/index.js';
2
- /** The launcher's program (launcher 17+ is 64-bit; older ones were in Win32). */
3
+ /** The launcher's program: newer MSIs install into Program Files, older ones into Program Files (x86). */
3
4
  export declare function launcherExe(): string | null;
4
5
  /** Downloads the launcher's MSI ahead of the install (once). */
5
6
  export declare function fetchLauncher(): Promise<void>;
@@ -17,6 +18,11 @@ export declare function placeEngines(persist: boolean): LauncherPlaceResult;
17
18
  export declare function savedEntries(): LauncherEntry[];
18
19
  /** Puts the saved manifests back where the launcher reads them; the engine folders they name. */
19
20
  export declare function restoreLauncherItems(): string[];
21
+ /**
22
+ * The manifests of this PC's engines (session.json) where the launcher reads them: without one it drops the engine
23
+ * from LauncherInstalled.dat when it starts. Only a manifest of the engine's own folder; one of ours is replaced.
24
+ */
25
+ export declare function writePcLauncherItems(items: readonly Pick<PcInstall, 'kind' | 'guest' | 'launcherItem'>[]): number;
20
26
  /** Merges these entries into the launcher's LauncherInstalled.dat (foreign entries kept). */
21
27
  export declare function writeLauncherEntries(entries: readonly LauncherEntry[]): void;
22
28
  /**
@@ -9,7 +9,7 @@ import { z } from 'zod';
9
9
  import { readBoundedFile } from '../../../shared/io/bounded.js';
10
10
  import { readValid, writeJsonAtomic } from '../../../shared/io/json.js';
11
11
  import { SYSTEM } from '../../../shared/system.js';
12
- import { enginePlaceProblem, ENGINE_NEED_BYTES, entriesUnder, launcherInstalledFile, launcherManifestsDir, launcherSettingsFiles, MAX_LAUNCHER_FILE_BYTES, mergeLauncherInstalled, restoreItems, saveItems, setLauncherInstallLocation, } from '../../../shared/unreal/index.js';
12
+ import { enginePlaceProblem, ENGINE_NEED_BYTES, entriesUnder, itemInstallLocation, ITEM_NAME, launcherInstalledFile, launcherManifestsDir, launcherSettingsFiles, MAX_LAUNCHER_FILE_BYTES, mergeLauncherInstalled, restoreItems, saveItems, setLauncherInstallLocation, } from '../../../shared/unreal/index.js';
13
13
  import { errMsg } from '../../../core/util/errors.js';
14
14
  import { runInstaller } from '../../runtime/pools.js';
15
15
  import { exists, mkdirp } from '../../runtime/fs.js';
@@ -22,14 +22,18 @@ const SAVED_MANIFESTS = path.join(SAVED_DIR, 'Manifests');
22
22
  const SAVED_ENTRIES = path.join(SAVED_DIR, 'LauncherInstalled.json');
23
23
  const programData = () => process.env.ProgramData ?? 'C:\\ProgramData';
24
24
  const localAppData = () => process.env.LOCALAPPDATA ?? '';
25
- /** The launcher's program (launcher 17+ is 64-bit; older ones were in Win32). */
25
+ /** The launcher's program: newer MSIs install into Program Files, older ones into Program Files (x86). */
26
26
  export function launcherExe() {
27
- const root = path.join(process.env['ProgramFiles(x86)'] ?? 'C:\\Program Files (x86)', 'Epic Games', 'Launcher');
28
- for (const arch of ['Win64', 'Win32']) {
29
- const exe = path.join(root, 'Portal', 'Binaries', arch, 'EpicGamesLauncher.exe');
30
- if (exists(exe))
31
- return exe;
32
- }
27
+ const roots = [
28
+ process.env.ProgramFiles ?? 'C:\\Program Files',
29
+ process.env['ProgramFiles(x86)'] ?? 'C:\\Program Files (x86)',
30
+ ];
31
+ for (const root of roots)
32
+ for (const arch of ['Win64', 'Win32']) {
33
+ const exe = path.join(root, 'Epic Games', 'Launcher', 'Portal', 'Binaries', arch, 'EpicGamesLauncher.exe');
34
+ if (exists(exe))
35
+ return exe;
36
+ }
33
37
  return null;
34
38
  }
35
39
  /** Downloads the launcher's MSI ahead of the install (once). */
@@ -89,6 +93,24 @@ export function savedEntries() {
89
93
  export function restoreLauncherItems() {
90
94
  return restoreItems(SAVED_MANIFESTS, launcherManifestsDir(programData()), ENGINES_DIR, exists);
91
95
  }
96
+ /**
97
+ * The manifests of this PC's engines (session.json) where the launcher reads them: without one it drops the engine
98
+ * from LauncherInstalled.dat when it starts. Only a manifest of the engine's own folder; one of ours is replaced.
99
+ */
100
+ export function writePcLauncherItems(items) {
101
+ const dir = launcherManifestsDir(programData());
102
+ let written = 0;
103
+ for (const i of items) {
104
+ if (i.kind !== 'unreal' || !i.launcherItem || !ITEM_NAME.test(i.launcherItem.name))
105
+ continue;
106
+ const at = itemInstallLocation(i.launcherItem.text);
107
+ if (!at || path.win32.resolve(at).toLowerCase() !== path.win32.resolve(i.guest).toLowerCase())
108
+ continue;
109
+ fs.writeFileSync(path.join(mkdirp(dir), i.launcherItem.name), i.launcherItem.text, 'utf8');
110
+ written++;
111
+ }
112
+ return written;
113
+ }
92
114
  /** Merges these entries into the launcher's LauncherInstalled.dat (foreign entries kept). */
93
115
  export function writeLauncherEntries(entries) {
94
116
  if (!entries.length)
@@ -16,7 +16,7 @@ import { runScriptWithJsonAsync } from '../../runtime/process.js';
16
16
  import { projectWorkspace } from '../../runtime/project-context.js';
17
17
  import { session } from '../../runtime/session.js';
18
18
  import { setUnrealMcp, writeAddonMcpServers } from '../../setup/addon-mcp.js';
19
- import { saveLauncherState, savedEntries, writeLauncherEntries } from './epic-launcher.js';
19
+ import { saveLauncherState, savedEntries, writeLauncherEntries, writePcLauncherItems } from './epic-launcher.js';
20
20
  import { DDC_DIR, ENGINES_DIR, SAVED_DIR, updateUnrealStatus } from './unreal-status.js';
21
21
  const SAVED_MANIFESTS = path.join(SAVED_DIR, 'Manifests');
22
22
  /** The real disk, read bounded. */
@@ -76,6 +76,7 @@ export async function registerEngines(engines, selectorFrom, ctx, aliases = [])
76
76
  if (values.length)
77
77
  await runScriptWithJsonAsync('set-registry-values.ps1', 'PlanFile', { values });
78
78
  try {
79
+ writePcLauncherItems(ctx.pcInstalls.items);
79
80
  writeLauncherEntries(launcherEntries(engines, ctx));
80
81
  }
81
82
  catch (e) {
@@ -18,7 +18,7 @@ const EMPTY = {
18
18
  updatedAt: '',
19
19
  engines: [],
20
20
  projects: [],
21
- launcher: { installed: false, installLocation: null, problem: null, freeBytes: null, needBytes: 0 },
21
+ launcher: { installed: false, exe: null, installLocation: null, problem: null, freeBytes: null, needBytes: 0 },
22
22
  visualStudio: { version: '', path: '', how: '', check: '', ok: false },
23
23
  debugger: null,
24
24
  ddc: { mode: 'local', dir: null },
@@ -1,8 +1,8 @@
1
1
  import { SIGNERS, type AppSigner } from '../../shared/signer.js';
2
- import { signedBy } from './signature.js';
2
+ import { signedBy, signedByAsync } from './signature.js';
3
3
  export { SIGNERS };
4
- export { signedBy };
4
+ export { signedBy, signedByAsync };
5
5
  /** A downloader `verify` hook: refuses a file not signed by `signer` (it counts as integrity for requireChecksum). */
6
- export declare function signatureCheck(signer: AppSigner, what: string): (file: string) => void;
6
+ export declare function signatureCheck(signer: AppSigner, what: string): (file: string) => Promise<void>;
7
7
  /** The installer at `file` (downloaded once from `url`), checked against its signer and, when given, its SHA-256. */
8
8
  export declare function signedInstaller(url: string, file: string, what: string, signer: AppSigner, sha256?: string | null): Promise<string>;
@@ -6,13 +6,13 @@ import { downloader } from '../../shared/download/index.js';
6
6
  import { exists, mkdirp } from '../runtime/fs.js';
7
7
  import { log } from '../runtime/log.js';
8
8
  import { SIGNERS } from '../../shared/signer.js';
9
- import { signedBy } from './signature.js';
9
+ import { signedBy, signedByAsync } from './signature.js';
10
10
  export { SIGNERS };
11
- export { signedBy };
11
+ export { signedBy, signedByAsync };
12
12
  /** A downloader `verify` hook: refuses a file not signed by `signer` (it counts as integrity for requireChecksum). */
13
13
  export function signatureCheck(signer, what) {
14
- return (file) => {
15
- if (!signedBy(file, signer))
14
+ return async (file) => {
15
+ if (!(await signedByAsync(file, signer)))
16
16
  throw new Error(what + ' is not signed by its vendor');
17
17
  };
18
18
  }
@@ -23,7 +23,7 @@ export async function signedInstaller(url, file, what, signer, sha256 = null) {
23
23
  mkdirp(path.dirname(file));
24
24
  await downloader.file(url, file, { sha256, verify: signatureCheck(signer, path.basename(file)) });
25
25
  }
26
- if (!signedBy(file, signer)) {
26
+ if (!(await signedByAsync(file, signer))) {
27
27
  fs.rmSync(file, { force: true });
28
28
  throw new Error(path.basename(file) + ' is not signed by its vendor; deleted it (it is downloaded again next boot)');
29
29
  }
@@ -1,11 +1,11 @@
1
1
  // Mesa for running OpenGL and Vulkan programs without a GPU (llvmpipe, lavapipe).
2
2
  import path from 'node:path';
3
- import { downloader } from '../../../shared/download/index.js';
3
+ import { downloader, fetchJson } from '../../../shared/download/index.js';
4
4
  import { SYSTEM } from '../../../shared/system.js';
5
5
  import { dryRun } from '../../runtime/paths.js';
6
6
  import { execAsync, scriptOutput } from '../../runtime/process.js';
7
7
  import { exists } from '../../runtime/fs.js';
8
- import { log, warn } from '../../runtime/log.js';
8
+ import { log } from '../../runtime/log.js';
9
9
  import { errMsg } from '../../../core/util/errors.js';
10
10
  import { arch, tool } from '../tool-env.js';
11
11
  import { installFromGithub } from '../install.js';
@@ -33,9 +33,28 @@ export async function setupMesa(env, config) {
33
33
  if (config.vulkan)
34
34
  await setupLavapipe(env, dir);
35
35
  }
36
- const VULKAN_RUNTIME = 'https://sdk.lunarg.com/sdk/download/latest/windows/vulkan-runtime-components.zip';
37
- /** Used when "latest" is not published yet (LunarG announces a version before its runtime zip is up: a 404). */
38
- const VULKAN_RUNTIME_PINNED = 'https://sdk.lunarg.com/sdk/download/1.4.350.0/windows/vulkan-runtime-components.zip';
36
+ /** LunarG's newest SDK version per platform ({"windows": "1.4.363.1", ...}). */
37
+ const VULKAN_LATEST = 'https://vulkan.lunarg.com/sdk/latest.json';
38
+ const runtimeUrl = (version) => 'https://sdk.lunarg.com/sdk/download/' + version + '/windows/vulkan-runtime-components.zip';
39
+ /** Known to have the runtime zip: used when LunarG cannot be asked, or none of its versions has one. */
40
+ const VULKAN_PINNED = '1.4.363.0';
41
+ /**
42
+ * The versions to try for the runtime zip, newest first: LunarG's latest, its x.y.z.0 (a point release such as
43
+ * 1.4.363.1 reissues the SDK without the runtime zip), then the pinned one.
44
+ */
45
+ async function runtimeVersions() {
46
+ let latest = null;
47
+ try {
48
+ const value = (await fetchJson(VULKAN_LATEST)).windows;
49
+ if (typeof value === 'string' && /^\d{1,2}\.\d{1,2}\.\d{1,4}\.\d{1,3}$/.test(value))
50
+ latest = value;
51
+ }
52
+ catch {
53
+ // LunarG not reachable: the pinned version.
54
+ }
55
+ const found = latest ? [latest, latest.replace(/\.\d+$/, '.0')] : [];
56
+ return [...new Set([...found, VULKAN_PINNED])];
57
+ }
39
58
  /**
40
59
  * Software Vulkan: Mesa's lavapipe plus the Khronos loader (vulkan-1.dll, from LunarG's runtime components; Windows
41
60
  * Sandbox has no Vulkan loader). The loader ignores VK_DRIVER_FILES in elevated processes, and everything in the
@@ -52,12 +71,17 @@ const setupLavapipe = async (env, mesaDir) => {
52
71
  // LunarG publishes no checksum for this zip; it comes over HTTPS from LunarG, the Khronos SDK's maintainer.
53
72
  // The zip has x86 and x64 folders: only the 64-bit loader (and vulkaninfo) is extracted.
54
73
  const fetchRuntime = (url) => downloader.archive(url, dir, { locate: 'vulkan-1.dll', fileName: 'vulkan-runtime.zip', members: ['*/x64/*'] });
55
- try {
56
- await fetchRuntime(VULKAN_RUNTIME);
57
- }
58
- catch (e) {
59
- warn('The latest Vulkan runtime could not be downloaded (' + errMsg(e) + '); using version 1.4.350.0.');
60
- await fetchRuntime(VULKAN_RUNTIME_PINNED);
74
+ const versions = await runtimeVersions();
75
+ for (const [n, version] of versions.entries()) {
76
+ try {
77
+ await fetchRuntime(runtimeUrl(version));
78
+ log('Vulkan loader ' + version + ' installed');
79
+ break;
80
+ }
81
+ catch (e) {
82
+ if (n === versions.length - 1)
83
+ throw new Error('the Vulkan loader could not be downloaded (' + errMsg(e) + ')');
84
+ }
61
85
  }
62
86
  }
63
87
  const register = (file) => execAsync(SYSTEM.reg(), ['add', 'HKLM\\SOFTWARE\\Khronos\\Vulkan\\Drivers', '/v', file, '/t', 'REG_DWORD', '/d', '0', '/f'], { stdio: 'ignore' });
@@ -110,6 +110,7 @@ export async function setupUnreal(env, config, _ctx) {
110
110
  ...s,
111
111
  launcher: {
112
112
  installed: launcher !== null,
113
+ exe: launcher,
113
114
  installLocation: place.installLocation,
114
115
  problem: place.problem,
115
116
  freeBytes: place.freeBytes,