windows-development-sandbox 1.25.0 → 1.25.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.
- package/config.example.json +5 -2
- package/dist/app/features/data/data.def.d.ts +4 -4
- package/dist/app/features/data/data.run.d.ts +1 -1
- package/dist/app/features/defs.d.ts +161 -6
- package/dist/app/features/defs.js +7 -0
- package/dist/app/features/projects/projects.def.d.ts +1 -1
- package/dist/app/features/projects/projects.run.d.ts +1 -1
- package/dist/app/features/unreal/index.d.ts +2 -0
- package/dist/app/features/unreal/index.js +3 -0
- package/dist/app/features/unreal/unreal.def.d.ts +236 -0
- package/dist/app/features/unreal/unreal.def.js +379 -0
- package/dist/app/features/unreal/unreal.run.d.ts +96 -0
- package/dist/app/features/unreal/unreal.run.js +217 -0
- package/dist/app/registry.js +5 -0
- package/dist/bin/agent-sandbox.js +2 -2
- package/dist/bin/{chunk-SUYYVIDW.js → chunk-3GAXLATK.js} +58 -5
- package/dist/bin/{chunk-IZRGTSQ5.js → chunk-F4PVBN23.js} +2465 -1392
- package/dist/bin/{chunk-KH4MI2U5.js → chunk-G3TG4ZT7.js} +1 -1
- package/dist/bin/claude-host.js +2 -2
- package/dist/bin/claude-sbx.js +2 -2
- package/dist/bin/codex-sbx.js +2 -2
- package/dist/bin/gemini-sbx.js +2 -2
- package/dist/bin/sandbox.js +2 -2
- package/dist/bin/sbx-hide.js +2 -2
- package/dist/bin/sbx-show.js +2 -2
- package/dist/cli/commands/launch/launch.js +5 -2
- package/dist/cli/commands/registry.js +3 -0
- package/dist/cli/manifest.d.ts +18 -0
- package/dist/cli/manifest.js +26 -0
- package/dist/cli/menu/coverage-exempt.js +1 -0
- package/dist/cli/menu/pages/sandbox-page.d.ts +3 -0
- package/dist/cli/menu/pages/sandbox-page.js +34 -4
- package/dist/cli/menu/views.js +2 -0
- package/dist/core/config/defaults.js +18 -4
- package/dist/core/schema/config.d.ts +9 -7
- package/dist/core/schema/pc-installs.d.ts +3 -1
- package/dist/core/schema/pc-installs.js +10 -1
- package/dist/core/schema/toolchains.js +1 -1
- package/dist/guest/accounts/kinds/epic.js +18 -13
- package/dist/guest/apps/after-install.d.ts +1 -1
- package/dist/guest/apps/after-install.js +6 -1
- package/dist/guest/apps/app-installer.d.ts +1 -0
- package/dist/guest/apps/app-installer.js +13 -3
- package/dist/guest/apps/comet-unpack.js +3 -2
- package/dist/guest/apps/resolvers-gamedev.js +55 -1
- package/dist/guest/boot/boot.js +14 -1
- package/dist/guest/boot/guide.js +24 -0
- package/dist/guest/daemon/daemon.js +3 -0
- package/dist/guest/setup/addon-mcp.d.ts +7 -0
- package/dist/guest/setup/addon-mcp.js +19 -4
- package/dist/guest/setup/browser-gpu.js +14 -11
- package/dist/guest/setup/headroom.js +4 -0
- package/dist/guest/toolchains/engines/epic-launcher.d.ts +26 -0
- package/dist/guest/toolchains/engines/epic-launcher.js +127 -0
- package/dist/guest/toolchains/engines/unreal-engines.d.ts +21 -1
- package/dist/guest/toolchains/engines/unreal-engines.js +163 -3
- package/dist/guest/toolchains/engines/unreal-status.d.ts +13 -0
- package/dist/guest/toolchains/engines/unreal-status.js +41 -0
- package/dist/guest/toolchains/native/debuggers.d.ts +6 -0
- package/dist/guest/toolchains/native/debuggers.js +57 -0
- package/dist/guest/toolchains/native/mesa.js +15 -8
- package/dist/guest/toolchains/native/rust-extras.d.ts +7 -0
- package/dist/guest/toolchains/native/rust-extras.js +23 -1
- package/dist/guest/toolchains/native/unreal.d.ts +2 -2
- package/dist/guest/toolchains/native/unreal.js +96 -18
- package/dist/guest/toolchains/plan.js +12 -1
- package/dist/guest/toolchains/tools/vscode-profile.js +2 -0
- package/dist/host/accounts/kinds/epic.js +11 -5
- package/dist/host/accounts/source.d.ts +2 -0
- package/dist/host/accounts/source.js +1 -0
- package/dist/host/pc-installs/discover.d.ts +2 -0
- package/dist/host/pc-installs/discover.js +25 -5
- package/dist/host/pc-installs/epic.d.ts +3 -0
- package/dist/host/pc-installs/epic.js +33 -2
- package/dist/host/pc-installs/source.d.ts +1 -1
- package/dist/host/pc-installs/unreal-vault.d.ts +11 -0
- package/dist/host/pc-installs/unreal-vault.js +78 -0
- package/dist/host/pc-installs/unreal.d.ts +36 -0
- package/dist/host/pc-installs/unreal.js +145 -2
- package/dist/host/profile/claude-profile.d.ts +8 -0
- package/dist/host/profile/claude-profile.js +43 -4
- package/dist/host/profile/index.d.ts +1 -1
- package/dist/host/profile/index.js +1 -1
- package/dist/runtime/agent.js +81 -7
- package/dist/runtime/daemon.js +2645 -1287
- package/dist/runtime/db.js +81 -7
- package/dist/runtime/main.js +2901 -1722
- package/dist/runtime/netproxy.js +81 -7
- package/dist/runtime/tool.js +81 -7
- package/dist/runtime/view.js +81 -7
- package/dist/selftest/agent-paths.test.js +1 -0
- package/dist/selftest/agents-web.test.js +12 -0
- package/dist/selftest/engines.test.js +602 -3
- package/dist/selftest/heavy-load.test.js +3 -1
- package/dist/selftest/hostile-input.test.js +11 -1
- package/dist/selftest/index.js +2 -0
- package/dist/selftest/menu-screens.js +3 -1
- package/dist/selftest/mixamo.test.d.ts +2 -0
- package/dist/selftest/mixamo.test.js +60 -0
- package/dist/selftest/secrets.test.js +36 -1
- package/dist/selftest/support/engine-golden.js +3 -0
- package/dist/selftest/support/fixtures.d.ts +3 -1
- package/dist/selftest/support/unreal-sample.d.ts +2 -0
- package/dist/selftest/support/unreal-sample.js +83 -0
- package/dist/selftest/toolchains.test.js +32 -2
- package/dist/shared/accounts/index.d.ts +1 -1
- package/dist/shared/accounts/index.js +1 -1
- package/dist/shared/accounts/kinds/epic.d.ts +6 -1
- package/dist/shared/accounts/kinds/epic.js +11 -1
- package/dist/shared/apps/associations.d.ts +7 -0
- package/dist/shared/apps/associations.js +26 -0
- package/dist/shared/apps/catalogue-agents-web.d.ts +4 -1
- package/dist/shared/apps/catalogue-agents-web.js +5 -2
- package/dist/shared/apps/catalogue-gamedev.js +71 -1
- package/dist/shared/apps/index.d.ts +2 -0
- package/dist/shared/apps/index.js +2 -0
- package/dist/shared/apps/preview.d.ts +2 -0
- package/dist/shared/apps/preview.js +10 -0
- package/dist/shared/browser-policy.d.ts +14 -0
- package/dist/shared/browser-policy.js +22 -0
- package/dist/shared/io/index.d.ts +1 -0
- package/dist/shared/io/index.js +1 -0
- package/dist/shared/io/rotate-log.d.ts +2 -0
- package/dist/shared/io/rotate-log.js +16 -0
- package/dist/shared/memory/policy.d.ts +5 -0
- package/dist/shared/memory/policy.js +7 -0
- package/dist/shared/paths/pc-installs.d.ts +13 -2
- package/dist/shared/paths/pc-installs.js +1 -0
- package/dist/shared/readiness.d.ts +0 -1
- package/dist/shared/readiness.js +0 -1
- package/dist/shared/signer.d.ts +4 -0
- package/dist/shared/signer.js +2 -0
- package/dist/shared/unreal/engines.d.ts +161 -0
- package/dist/shared/unreal/engines.js +308 -0
- package/dist/shared/unreal/guide.d.ts +19 -0
- package/dist/shared/unreal/guide.js +105 -0
- package/dist/shared/unreal/index.d.ts +7 -0
- package/dist/shared/unreal/index.js +8 -0
- package/dist/shared/unreal/launcher-files.d.ts +88 -0
- package/dist/shared/unreal/launcher-files.js +292 -0
- package/dist/shared/unreal/mixamo.d.ts +5 -0
- package/dist/shared/unreal/mixamo.js +16 -0
- package/dist/shared/unreal/room.d.ts +17 -0
- package/dist/shared/unreal/room.js +34 -0
- package/dist/shared/unreal/status.d.ts +54 -0
- package/dist/shared/unreal/status.js +57 -0
- package/dist/shared/unreal/vault.d.ts +21 -0
- package/dist/shared/unreal/vault.js +67 -0
- package/dist/shared/unreal/vs-check.d.ts +39 -0
- package/dist/shared/unreal/vs-check.js +79 -0
- package/dist/web/index.html +1 -1
- package/dist/web/{main-CCILLX2M.js → main-FNE6XEHA.js} +84 -84
- package/package.json +1 -1
- package/sandbox/csharp/VsSetup.cs +210 -0
- package/sandbox/scripts/get-vs-setup.ps1 +93 -0
- package/sandbox/scripts/set-registry-values.ps1 +35 -0
- package/schemas/config.schema.json +35 -2
- package/schemas/sandbox-config.schema.json +8 -1
|
@@ -1 +1,55 @@
|
|
|
1
|
-
|
|
1
|
+
// Where the latest version of the game-development apps (catalogue-gamedev.ts) is, when it is not a plain GitHub
|
|
2
|
+
// release: Krita (download.kde.org, with the SHA-256 its mirror publishes beside each file), Inkscape (inkscape.org's
|
|
3
|
+
// portable 7z and its .sha256) and RenderDoc (renderdoc.org's portable zip; it publishes no checksum, the download is
|
|
4
|
+
// HTTPS from renderdoc.org).
|
|
5
|
+
import { fetchText } from '../../shared/download/index.js';
|
|
6
|
+
const firstHash = (text) => /\b([0-9a-f]{64})\b/i.exec(text)?.[1].toLowerCase() ?? null;
|
|
7
|
+
const byVersion = (a, b) => {
|
|
8
|
+
const x = a.split('.').map(Number);
|
|
9
|
+
const y = b.split('.').map(Number);
|
|
10
|
+
for (let i = 0; i < Math.max(x.length, y.length); i++)
|
|
11
|
+
if ((x[i] ?? 0) !== (y[i] ?? 0))
|
|
12
|
+
return (x[i] ?? 0) - (y[i] ?? 0);
|
|
13
|
+
return 0;
|
|
14
|
+
};
|
|
15
|
+
/** The newest Krita release folder that has the portable Windows zip (a hotfix folder may have only some files). */
|
|
16
|
+
async function krita() {
|
|
17
|
+
const base = 'https://download.kde.org/stable/krita/';
|
|
18
|
+
const versions = [...(await fetchText(base)).matchAll(/href="(\d+\.\d+\.\d+(?:\.\d+)?)\/"/g)]
|
|
19
|
+
.map((m) => m[1])
|
|
20
|
+
.sort(byVersion)
|
|
21
|
+
.reverse()
|
|
22
|
+
.slice(0, 4);
|
|
23
|
+
for (const v of versions) {
|
|
24
|
+
const file = 'krita-x64-' + v + '.zip';
|
|
25
|
+
if (!(await fetchText(base + v + '/')).includes('href="' + file + '"'))
|
|
26
|
+
continue;
|
|
27
|
+
const url = base + v + '/' + file;
|
|
28
|
+
return { url, fileName: file, sha256: firstHash(await fetchText(url + '.sha256').catch(() => '')) };
|
|
29
|
+
}
|
|
30
|
+
throw new Error('no portable Krita zip on download.kde.org');
|
|
31
|
+
}
|
|
32
|
+
async function inkscape() {
|
|
33
|
+
const release = await fetchText('https://inkscape.org/release/');
|
|
34
|
+
const v = /\/release\/inkscape-(\d+\.\d+(?:\.\d+)?)\//.exec(release)?.[1];
|
|
35
|
+
if (!v)
|
|
36
|
+
throw new Error('could not find the latest Inkscape release');
|
|
37
|
+
const page = await fetchText('https://inkscape.org/release/inkscape-' + v + '/windows/64-bit/compressed-7z/dl/');
|
|
38
|
+
const item = /\/gallery\/item\/\d+\/inkscape-[\w.-]+-x64[\w-]*\.7z/.exec(page);
|
|
39
|
+
const sums = /https:\/\/media\.inkscape\.org\/media\/resources\/sigs\/[\w.-]+\.sha256/.exec(page)?.[0];
|
|
40
|
+
if (!item)
|
|
41
|
+
throw new Error('no portable Inkscape 7z for ' + v);
|
|
42
|
+
return {
|
|
43
|
+
url: 'https://inkscape.org' + item[0],
|
|
44
|
+
fileName: 'inkscape-' + v + '-x64.7z',
|
|
45
|
+
sha256: sums ? firstHash(await fetchText(sums).catch(() => '')) : null,
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
async function renderdoc() {
|
|
49
|
+
const page = await fetchText('https://renderdoc.org/builds');
|
|
50
|
+
const hit = /https:\/\/renderdoc\.org\/stable\/([\d.]+)\/RenderDoc_[\d.]+_64\.zip/.exec(page);
|
|
51
|
+
if (!hit)
|
|
52
|
+
throw new Error('could not find the RenderDoc download on renderdoc.org');
|
|
53
|
+
return { url: hit[0], fileName: 'RenderDoc_' + hit[1] + '_64.zip' };
|
|
54
|
+
}
|
|
55
|
+
export const GAMEDEV_RESOLVERS = { krita, inkscape, renderdoc };
|
package/dist/guest/boot/boot.js
CHANGED
|
@@ -52,7 +52,20 @@ import { setBackgroundWork, setSandboxStatus } from '../runtime/status.js';
|
|
|
52
52
|
import { errMsg } from '../../core/util/errors.js';
|
|
53
53
|
const PRIMARY_POLLS = 600;
|
|
54
54
|
const POLL_MS = 500;
|
|
55
|
-
const VSCODE_LANGUAGES = [
|
|
55
|
+
const VSCODE_LANGUAGES = [
|
|
56
|
+
'python',
|
|
57
|
+
'java',
|
|
58
|
+
'go',
|
|
59
|
+
'rust',
|
|
60
|
+
'cpp',
|
|
61
|
+
'msvc',
|
|
62
|
+
'unreal',
|
|
63
|
+
'dotnet',
|
|
64
|
+
'ruby',
|
|
65
|
+
'php',
|
|
66
|
+
'deno',
|
|
67
|
+
'pwsh',
|
|
68
|
+
];
|
|
56
69
|
function startDaemon(env, secrets) {
|
|
57
70
|
spawn(process.execPath, [path.join(path.dirname(process.argv[1]), 'daemon.js')], {
|
|
58
71
|
detached: true,
|
package/dist/guest/boot/guide.js
CHANGED
|
@@ -7,6 +7,10 @@ import { dataDir, launchDir, logsDir, toolsDir } from '../runtime/paths.js';
|
|
|
7
7
|
import { addonGuideLine, APPS, headroomGuideLine, RTK_GUIDE_LINE } from '../../shared/apps/index.js';
|
|
8
8
|
import { installedAddon, installedRtk } from '../setup/agent-addons.js';
|
|
9
9
|
import { projectFolderLines, toolLines } from './guide-lines.js';
|
|
10
|
+
import { sandboxScriptsDir } from '../../shared/scripts.js';
|
|
11
|
+
import { findUproject, unrealGuideLines } from '../../shared/unreal/index.js';
|
|
12
|
+
import { DISK } from '../toolchains/engines/unreal-engines.js';
|
|
13
|
+
import { readUnrealStatus } from '../toolchains/engines/unreal-status.js';
|
|
10
14
|
const BEGIN = '<!-- agent-sandbox:begin (generated every boot, do not edit) -->';
|
|
11
15
|
const END = '<!-- agent-sandbox:end -->';
|
|
12
16
|
/** Drives other than C: are subst drives onto C:\Mirror\<letter> (mapped folders shown at their PC paths). */
|
|
@@ -25,6 +29,25 @@ const mirrorLine = () => {
|
|
|
25
29
|
};
|
|
26
30
|
/** Folders mapped for other projects at boot; launch folders from before mappedProjects only list the guest side. */
|
|
27
31
|
const mappedFolders = () => session.mappedProjects ?? (session.roots ?? []).map((guest) => ({ host: 'a folder on the PC', guest }));
|
|
32
|
+
/** The Mixamo import template, when this version ships one. */
|
|
33
|
+
function mixamoScript() {
|
|
34
|
+
const file = path.join(sandboxScriptsDir(), 'templates', 'unreal', 'import_mixamo.py');
|
|
35
|
+
return fs.existsSync(file) ? file : null;
|
|
36
|
+
}
|
|
37
|
+
/** The Unreal lines (engine per project, build/test/package/debug commands) when the sandbox is set up for Unreal. */
|
|
38
|
+
function unrealLines(projects) {
|
|
39
|
+
const any = session.toolchains.unreal.enabled === true || projects.some((p) => p.tools?.some((t) => /^unreal/.test(t)));
|
|
40
|
+
if (!any)
|
|
41
|
+
return [];
|
|
42
|
+
const status = readUnrealStatus();
|
|
43
|
+
if (!status.updatedAt)
|
|
44
|
+
return [];
|
|
45
|
+
const folders = [workspace, ...projects.map((p) => p.guest)];
|
|
46
|
+
return unrealGuideLines(status, folders
|
|
47
|
+
.filter((f, i) => folders.findIndex((g) => g.toLowerCase() === f.toLowerCase()) === i)
|
|
48
|
+
.filter((f) => status.projects.some((p) => p.workspace.toLowerCase() === f.toLowerCase()))
|
|
49
|
+
.map((f) => ({ workspace: f, uproject: findUproject(f, DISK) })), { mixamo: { downloads: path.join(dataDir, 'downloads'), script: mixamoScript() } });
|
|
50
|
+
}
|
|
28
51
|
/** Deliberately tiny: it is loaded into every conversation. */
|
|
29
52
|
export function sandboxGuide(toolSummary, lan = 'blocked', projects = [], addons = []) {
|
|
30
53
|
return [
|
|
@@ -45,6 +68,7 @@ export function sandboxGuide(toolSummary, lan = 'blocked', projects = [], addons
|
|
|
45
68
|
'- RO: `' + launchDir + "` (this boot's scripts) · lost on close: everything else, incl. `C:\\nvm` (Node symlink)",
|
|
46
69
|
...mirrorLine(),
|
|
47
70
|
...toolLines(session.project, toolSummary, projects),
|
|
71
|
+
...unrealLines(projects),
|
|
48
72
|
// Agent add-ons that change how the agent works (rtk, Headroom, MCP servers, ccusage).
|
|
49
73
|
...addons,
|
|
50
74
|
'- Persistent installs: `npm i -g` (per nvm version), `pip`, `cargo install`, `dotnet tool -g`, `uv tool`, `bun add -g`. Real installers must re-run each boot via a hook (ask the user).',
|
|
@@ -12,6 +12,7 @@ import { liveFolders } from './live.js';
|
|
|
12
12
|
import { warn } from '../runtime/log.js';
|
|
13
13
|
import { errMsg } from '../../core/util/errors.js';
|
|
14
14
|
import { flushAccounts } from './accounts.js';
|
|
15
|
+
import { captureLauncherEngines } from '../toolchains/engines/unreal-engines.js';
|
|
15
16
|
const POLL_MS = 400;
|
|
16
17
|
const MEM_SAVE_MS = 120000;
|
|
17
18
|
export class Daemon {
|
|
@@ -47,6 +48,8 @@ export class Daemon {
|
|
|
47
48
|
safely('claude-mem save', saveClaudeMem);
|
|
48
49
|
safely('agent databases save', saveLocalState);
|
|
49
50
|
void flushAccounts();
|
|
51
|
+
// Engines the Epic Games Launcher installed on the data disk since: kept for the next sandbox.
|
|
52
|
+
void captureLauncherEngines().catch((e) => warn('Unreal launcher save: ' + errMsg(e)));
|
|
50
53
|
}
|
|
51
54
|
}
|
|
52
55
|
handle(req) {
|
|
@@ -1,2 +1,9 @@
|
|
|
1
|
+
/** The Unreal editor's MCP server (Unreal 5.8's ModelContextProtocol plugin), set by the Unreal engines step. */
|
|
2
|
+
export declare const UNREAL_MCP_NAME = "sbx-unreal-editor";
|
|
3
|
+
/**
|
|
4
|
+
* Adds the Unreal editor's MCP server (its address) to the next writeAddonMcpServers, or takes it away (null). It only
|
|
5
|
+
* answers while the editor runs with the plugin on; until then Claude Code lists it as not connected.
|
|
6
|
+
*/
|
|
7
|
+
export declare function setUnrealMcp(url: string | null): void;
|
|
1
8
|
/** Sets the add-ons' MCP servers in the sandbox's .claude.json (and Codex's config.toml); the names that are on. */
|
|
2
9
|
export declare function writeAddonMcpServers(): string[];
|
|
@@ -13,8 +13,18 @@ import { dryRun } from '../runtime/paths.js';
|
|
|
13
13
|
import { claudeDir, session, stateDir } from '../runtime/session.js';
|
|
14
14
|
import { log, warn } from '../runtime/log.js';
|
|
15
15
|
import { installedMcpAddons } from './agent-addons.js';
|
|
16
|
-
/** The
|
|
17
|
-
const
|
|
16
|
+
/** The Unreal editor's MCP server (Unreal 5.8's ModelContextProtocol plugin), set by the Unreal engines step. */
|
|
17
|
+
export const UNREAL_MCP_NAME = 'sbx-unreal-editor';
|
|
18
|
+
let unrealMcp = null;
|
|
19
|
+
/** The names of every MCP server the sandbox can add (the ones it may remove). */
|
|
20
|
+
const ADDON_MCP_NAMES = [...APPS.flatMap((a) => (a.mcp ? [a.mcp.name] : [])), UNREAL_MCP_NAME];
|
|
21
|
+
/**
|
|
22
|
+
* Adds the Unreal editor's MCP server (its address) to the next writeAddonMcpServers, or takes it away (null). It only
|
|
23
|
+
* answers while the editor runs with the plugin on; until then Claude Code lists it as not connected.
|
|
24
|
+
*/
|
|
25
|
+
export function setUnrealMcp(url) {
|
|
26
|
+
unrealMcp = url ? { type: 'http', url } : null;
|
|
27
|
+
}
|
|
18
28
|
/** Variables only this sandbox can tell an add-on's server (where Comet is). */
|
|
19
29
|
function placeEnv(id) {
|
|
20
30
|
if (id !== 'comet-mcp')
|
|
@@ -34,7 +44,10 @@ function addonServers() {
|
|
|
34
44
|
export function writeAddonMcpServers() {
|
|
35
45
|
const on = addonServers();
|
|
36
46
|
writeCodexAddonMcpServers(on);
|
|
37
|
-
const servers =
|
|
47
|
+
const servers = {
|
|
48
|
+
...Object.fromEntries(on.map(({ mcp, server }) => [mcp.name, server])),
|
|
49
|
+
...(unrealMcp ? { [UNREAL_MCP_NAME]: unrealMcp } : {}),
|
|
50
|
+
};
|
|
38
51
|
const file = path.join(claudeDir, '.claude.json');
|
|
39
52
|
let state = {};
|
|
40
53
|
if (fs.existsSync(file)) {
|
|
@@ -72,10 +85,12 @@ function writeCodexAddonMcpServers(on) {
|
|
|
72
85
|
const servers = Object.fromEntries(on
|
|
73
86
|
.filter(({ mcp }) => mcp.codex)
|
|
74
87
|
.map(({ mcp, server }) => [mcp.name, { ...server, ...(mcp.envVars?.length ? { envVars: mcp.envVars } : {}) }]));
|
|
88
|
+
if (unrealMcp)
|
|
89
|
+
servers[UNREAL_MCP_NAME] = unrealMcp;
|
|
75
90
|
const before = exists(file) ? fs.readFileSync(file, 'utf8') : '';
|
|
76
91
|
if (!before && !Object.keys(servers).length)
|
|
77
92
|
return;
|
|
78
|
-
const known = APPS.flatMap((a) => (a.mcp?.codex ? [a.mcp.name] : []));
|
|
93
|
+
const known = [...APPS.flatMap((a) => (a.mcp?.codex ? [a.mcp.name] : [])), UNREAL_MCP_NAME];
|
|
79
94
|
const result = codexWithAddonMcpServers(before, servers, known);
|
|
80
95
|
for (const name of result.skipped)
|
|
81
96
|
warn('Codex MCP servers from Apps: ' + name + ' is already in config.toml in another form, left as it is');
|
|
@@ -1,18 +1,21 @@
|
|
|
1
|
-
// Browsers in the sandbox
|
|
2
|
-
//
|
|
3
|
-
//
|
|
4
|
-
//
|
|
1
|
+
// Browsers in the sandbox (Edge and Chrome machine policy, written every boot) draw without the GPU, and download to
|
|
2
|
+
// C:\AgentData\downloads, which is kept between sandboxes. With sandbox.vGpu the sandbox shares this PC's graphics card
|
|
3
|
+
// through one virtual GPU: a browser's GPU process starting next to a game or other Direct3D program there has taken
|
|
4
|
+
// the whole sandbox down. Pages still render, WebGL included, in software; programs that need the GPU keep it. Without
|
|
5
|
+
// vGPU there is no GPU to use, so only the download folder matters.
|
|
6
|
+
import path from 'node:path';
|
|
5
7
|
import { SYSTEM } from '../../shared/system.js';
|
|
6
|
-
import {
|
|
8
|
+
import { browserPolicies } from '../../shared/browser-policy.js';
|
|
9
|
+
import { dataDir, dryRun, isWin } from '../runtime/paths.js';
|
|
7
10
|
import { exec } from '../runtime/process.js';
|
|
8
11
|
import { log } from '../runtime/log.js';
|
|
9
|
-
|
|
12
|
+
import { mkdirp } from '../runtime/fs.js';
|
|
10
13
|
export function keepBrowsersOffGpu() {
|
|
11
14
|
if (dryRun || !isWin)
|
|
12
15
|
return;
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
});
|
|
17
|
-
log('Browsers draw without the GPU
|
|
16
|
+
const downloads = path.join(dataDir, 'downloads');
|
|
17
|
+
mkdirp(downloads);
|
|
18
|
+
for (const p of browserPolicies({ downloads }))
|
|
19
|
+
exec(SYSTEM.reg(), ['add', p.key, '/v', p.name, '/t', p.type, '/d', p.data, '/f'], { stdio: 'ignore' });
|
|
20
|
+
log('Browsers draw without the GPU and download to ' + downloads + '.');
|
|
18
21
|
}
|
|
@@ -5,6 +5,7 @@ import fs from 'node:fs';
|
|
|
5
5
|
import http from 'node:http';
|
|
6
6
|
import path from 'node:path';
|
|
7
7
|
import { spawn } from 'node:child_process';
|
|
8
|
+
import { rotateLog } from '../../shared/io/index.js';
|
|
8
9
|
import { HEADROOM_PORT, headroomArgs, headroomEnv, headroomUrl } from '../../shared/apps/index.js';
|
|
9
10
|
import { logsDir } from '../runtime/paths.js';
|
|
10
11
|
import { portOpen, waitForPort } from '../runtime/ports.js';
|
|
@@ -12,6 +13,8 @@ import { log } from '../runtime/log.js';
|
|
|
12
13
|
import { installedAddon } from './agent-addons.js';
|
|
13
14
|
/** The proxy's log (it redacts keys and tokens in what it logs). */
|
|
14
15
|
export const HEADROOM_LOG = path.join(logsDir, 'headroom.log');
|
|
16
|
+
/** The proxy logs every request and the file lives on the data disk: past this it is kept as headroom.log.1. */
|
|
17
|
+
const HEADROOM_LOG_LIMIT = 20 * 1024 * 1024;
|
|
15
18
|
/** Whether Headroom's own health check answers on the port (not some other program that took it). */
|
|
16
19
|
function answersAsHeadroom(port) {
|
|
17
20
|
return new Promise((resolve) => {
|
|
@@ -53,6 +56,7 @@ async function startProxy(env) {
|
|
|
53
56
|
return false;
|
|
54
57
|
if (await portOpen(HEADROOM_PORT))
|
|
55
58
|
return true;
|
|
59
|
+
rotateLog(HEADROOM_LOG, HEADROOM_LOG_LIMIT);
|
|
56
60
|
const out = fs.openSync(HEADROOM_LOG, 'a');
|
|
57
61
|
try {
|
|
58
62
|
spawn(exe, headroomArgs(HEADROOM_PORT), {
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { type LauncherEntry } from '../../../shared/unreal/index.js';
|
|
2
|
+
/** The launcher's program (launcher 17+ is 64-bit; older ones were in Win32). */
|
|
3
|
+
export declare function launcherExe(): string | null;
|
|
4
|
+
/** Downloads the launcher's MSI ahead of the install (once). */
|
|
5
|
+
export declare function fetchLauncher(): Promise<void>;
|
|
6
|
+
/** Installs the launcher when this sandbox does not have it yet (the MSI, quietly). Returns its program. */
|
|
7
|
+
export declare function ensureLauncher(): Promise<string>;
|
|
8
|
+
export interface LauncherPlaceResult {
|
|
9
|
+
readonly installLocation: string | null;
|
|
10
|
+
readonly problem: string | null;
|
|
11
|
+
readonly freeBytes: number | null;
|
|
12
|
+
readonly needBytes: number;
|
|
13
|
+
}
|
|
14
|
+
/** Points the launcher's default install folder at tools\unreal\engines (every settings file it may read). */
|
|
15
|
+
export declare function placeEngines(persist: boolean): LauncherPlaceResult;
|
|
16
|
+
/** The saved LauncherInstalled.dat entries of engines in tools\unreal\engines whose folder is still there. */
|
|
17
|
+
export declare function savedEntries(): LauncherEntry[];
|
|
18
|
+
/** Puts the saved manifests back where the launcher reads them; the engine folders they name. */
|
|
19
|
+
export declare function restoreLauncherItems(): string[];
|
|
20
|
+
/** Merges these entries into the launcher's LauncherInstalled.dat (foreign entries kept). */
|
|
21
|
+
export declare function writeLauncherEntries(entries: readonly LauncherEntry[]): void;
|
|
22
|
+
/**
|
|
23
|
+
* Saves what the launcher knows about engines it installed into tools\unreal\engines (new or changed manifests, its
|
|
24
|
+
* LauncherInstalled.dat entries). Cheap: a few small files. Returns whether something new was saved.
|
|
25
|
+
*/
|
|
26
|
+
export declare function saveLauncherState(): boolean;
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
// The Epic Games Launcher in the sandbox: installed every boot from its MSI (downloaded once into tools\unreal, run
|
|
2
|
+
// only when Epic's signature checks out; a sandbox loses Program Files), pointed at the data disk for engines
|
|
3
|
+
// (GameUserSettings.ini [Launcher] DefaultAppInstallLocation, in the encoding the file has), and given back what it
|
|
4
|
+
// knew about the engines it installed there: their .item manifests and LauncherInstalled.dat entries are saved on the
|
|
5
|
+
// data disk while the sandbox runs (the daemon calls saveLauncherState) and put back at the next boot.
|
|
6
|
+
import fs from 'node:fs';
|
|
7
|
+
import path from 'node:path';
|
|
8
|
+
import { z } from 'zod';
|
|
9
|
+
import { readBoundedFile } from '../../../shared/io/bounded.js';
|
|
10
|
+
import { readValid, writeJsonAtomic } from '../../../shared/io/json.js';
|
|
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';
|
|
13
|
+
import { errMsg } from '../../../core/util/errors.js';
|
|
14
|
+
import { runInstaller } from '../../runtime/pools.js';
|
|
15
|
+
import { exists, mkdirp } from '../../runtime/fs.js';
|
|
16
|
+
import { log } from '../../runtime/log.js';
|
|
17
|
+
import { SIGNERS, signedInstaller } from '../installers.js';
|
|
18
|
+
import { ENGINES_DIR, SAVED_DIR, UNREAL_DIR } from './unreal-status.js';
|
|
19
|
+
const MSI_URL = 'https://launcher-public-service-prod06.ol.epicgames.com/launcher/api/installer/download/EpicGamesLauncherInstaller.msi';
|
|
20
|
+
const MSI_FILE = path.join(UNREAL_DIR, 'EpicGamesLauncherInstaller.msi');
|
|
21
|
+
const SAVED_MANIFESTS = path.join(SAVED_DIR, 'Manifests');
|
|
22
|
+
const SAVED_ENTRIES = path.join(SAVED_DIR, 'LauncherInstalled.json');
|
|
23
|
+
const programData = () => process.env.ProgramData ?? 'C:\\ProgramData';
|
|
24
|
+
const localAppData = () => process.env.LOCALAPPDATA ?? '';
|
|
25
|
+
/** The launcher's program (launcher 17+ is 64-bit; older ones were in Win32). */
|
|
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
|
+
}
|
|
33
|
+
return null;
|
|
34
|
+
}
|
|
35
|
+
/** Downloads the launcher's MSI ahead of the install (once). */
|
|
36
|
+
export async function fetchLauncher() {
|
|
37
|
+
if (!launcherExe())
|
|
38
|
+
await launcherMsi();
|
|
39
|
+
}
|
|
40
|
+
const launcherMsi = () => signedInstaller(MSI_URL, MSI_FILE, 'the Epic Games Launcher (~250 MB once)', SIGNERS.epic);
|
|
41
|
+
/** Installs the launcher when this sandbox does not have it yet (the MSI, quietly). Returns its program. */
|
|
42
|
+
export async function ensureLauncher() {
|
|
43
|
+
const have = launcherExe();
|
|
44
|
+
if (have)
|
|
45
|
+
return have;
|
|
46
|
+
const msi = await launcherMsi();
|
|
47
|
+
log('Installing the Epic Games Launcher ...');
|
|
48
|
+
const code = await runInstaller(SYSTEM.msiexec(), ['/i', msi, '/qn', '/norestart']);
|
|
49
|
+
const exe = launcherExe();
|
|
50
|
+
if ((code !== 0 && code !== 3010) || !exe)
|
|
51
|
+
throw new Error('the Epic Games Launcher installer failed (msiexec exit ' + code + ')');
|
|
52
|
+
return exe;
|
|
53
|
+
}
|
|
54
|
+
/** Points the launcher's default install folder at tools\unreal\engines (every settings file it may read). */
|
|
55
|
+
export function placeEngines(persist) {
|
|
56
|
+
let error;
|
|
57
|
+
if (persist)
|
|
58
|
+
try {
|
|
59
|
+
mkdirp(ENGINES_DIR);
|
|
60
|
+
for (const file of launcherSettingsFiles(localAppData()))
|
|
61
|
+
setLauncherInstallLocation(file, ENGINES_DIR);
|
|
62
|
+
}
|
|
63
|
+
catch (e) {
|
|
64
|
+
error = "could not write the launcher's settings (" + errMsg(e) + ')';
|
|
65
|
+
}
|
|
66
|
+
else
|
|
67
|
+
error = 'toolchains.unreal.persistEngines is off';
|
|
68
|
+
const set = persist && !error;
|
|
69
|
+
const { problem, freeBytes } = enginePlaceProblem(set ? { set } : { set, error: error }, ENGINES_DIR);
|
|
70
|
+
return { installLocation: set ? ENGINES_DIR : null, problem, freeBytes, needBytes: ENGINE_NEED_BYTES };
|
|
71
|
+
}
|
|
72
|
+
const SavedEntriesSchema = z.object({
|
|
73
|
+
entries: z
|
|
74
|
+
.array(z.object({
|
|
75
|
+
InstallLocation: z.string().max(1024),
|
|
76
|
+
NamespaceId: z.string().max(200),
|
|
77
|
+
ItemId: z.string().max(200),
|
|
78
|
+
ArtifactId: z.string().max(200),
|
|
79
|
+
AppVersion: z.string().max(200),
|
|
80
|
+
AppName: z.string().max(200),
|
|
81
|
+
}))
|
|
82
|
+
.max(64),
|
|
83
|
+
});
|
|
84
|
+
/** The saved LauncherInstalled.dat entries of engines in tools\unreal\engines whose folder is still there. */
|
|
85
|
+
export function savedEntries() {
|
|
86
|
+
return (readValid(SavedEntriesSchema, SAVED_ENTRIES)?.entries ?? []).filter((e) => e.InstallLocation.toLowerCase().startsWith(ENGINES_DIR.toLowerCase() + '\\') && exists(e.InstallLocation));
|
|
87
|
+
}
|
|
88
|
+
/** Puts the saved manifests back where the launcher reads them; the engine folders they name. */
|
|
89
|
+
export function restoreLauncherItems() {
|
|
90
|
+
return restoreItems(SAVED_MANIFESTS, launcherManifestsDir(programData()), ENGINES_DIR, exists);
|
|
91
|
+
}
|
|
92
|
+
/** Merges these entries into the launcher's LauncherInstalled.dat (foreign entries kept). */
|
|
93
|
+
export function writeLauncherEntries(entries) {
|
|
94
|
+
if (!entries.length)
|
|
95
|
+
return;
|
|
96
|
+
const file = launcherInstalledFile(programData());
|
|
97
|
+
const bytes = readBoundedFile(file, MAX_LAUNCHER_FILE_BYTES);
|
|
98
|
+
if (!bytes && fs.existsSync(file))
|
|
99
|
+
throw new Error(file + ' is too big to read');
|
|
100
|
+
const next = mergeLauncherInstalled(bytes ? bytes.toString('utf8') : null, entries);
|
|
101
|
+
mkdirp(path.dirname(file));
|
|
102
|
+
fs.writeFileSync(file, next, 'utf8');
|
|
103
|
+
}
|
|
104
|
+
/**
|
|
105
|
+
* Saves what the launcher knows about engines it installed into tools\unreal\engines (new or changed manifests, its
|
|
106
|
+
* LauncherInstalled.dat entries). Cheap: a few small files. Returns whether something new was saved.
|
|
107
|
+
*/
|
|
108
|
+
export function saveLauncherState() {
|
|
109
|
+
if (!exists(ENGINES_DIR))
|
|
110
|
+
return false;
|
|
111
|
+
const items = saveItems(launcherManifestsDir(programData()), SAVED_MANIFESTS, ENGINES_DIR);
|
|
112
|
+
const bytes = readBoundedFile(launcherInstalledFile(programData()), MAX_LAUNCHER_FILE_BYTES);
|
|
113
|
+
const live = bytes ? entriesUnder(bytes.toString('utf8'), ENGINES_DIR) : [];
|
|
114
|
+
const old = readValid(SavedEntriesSchema, SAVED_ENTRIES)?.entries ?? [];
|
|
115
|
+
const merged = [
|
|
116
|
+
...old.filter((o) => !live.some((l) => l.InstallLocation.toLowerCase() === o.InstallLocation.toLowerCase())),
|
|
117
|
+
...live,
|
|
118
|
+
].filter((e) => exists(e.InstallLocation));
|
|
119
|
+
const changed = JSON.stringify(merged) !== JSON.stringify(old);
|
|
120
|
+
if (changed) {
|
|
121
|
+
mkdirp(SAVED_DIR);
|
|
122
|
+
writeJsonAtomic(SAVED_ENTRIES, { entries: merged.slice(0, 64) });
|
|
123
|
+
}
|
|
124
|
+
if (items.length || changed)
|
|
125
|
+
log('Unreal: saved what the Epic Games Launcher knows about its engines on the data disk');
|
|
126
|
+
return items.length > 0 || changed;
|
|
127
|
+
}
|
|
@@ -1,4 +1,24 @@
|
|
|
1
|
+
import { type EngineFs, type LauncherEntry, type MappedEngine, type UnrealEngine } from '../../../shared/unreal/index.js';
|
|
1
2
|
import type { Toolchains } from '../../../core/types.js';
|
|
2
3
|
import type { ToolCtx } from '../step.js';
|
|
3
4
|
import type { ToolEnv } from '../tool-env.js';
|
|
4
|
-
|
|
5
|
+
/** The real disk, read bounded. */
|
|
6
|
+
export declare const DISK: EngineFs;
|
|
7
|
+
/** This PC's engines the host mapped (session.json's pcInstalls). */
|
|
8
|
+
export declare function mappedEngines(ctx: Pick<ToolCtx, 'pcInstalls'>): MappedEngine[];
|
|
9
|
+
/** Every engine this sandbox has (an install the launcher has not finished is not one yet). */
|
|
10
|
+
export declare function sandboxEngines(ctx: Pick<ToolCtx, 'pcInstalls'>, disk?: EngineFs): UnrealEngine[];
|
|
11
|
+
/** The LauncherInstalled.dat entries for the installed builds: the launcher's own saved ones, else made from them. */
|
|
12
|
+
export declare function launcherEntries(engines: readonly UnrealEngine[], ctx: Pick<ToolCtx, 'pcInstalls'>): LauncherEntry[];
|
|
13
|
+
/** Registers the engines machine-wide: the registry, LauncherInstalled.dat, the .uproject file association. */
|
|
14
|
+
export declare function registerEngines(engines: readonly UnrealEngine[], selectorFrom: UnrealEngine | null, ctx: Pick<ToolCtx, 'pcInstalls'>, aliases?: readonly {
|
|
15
|
+
readonly id: string;
|
|
16
|
+
readonly dir: string;
|
|
17
|
+
}[]): Promise<void>;
|
|
18
|
+
/**
|
|
19
|
+
* While the sandbox runs (the daemon, every few minutes): saves what the launcher learned about engines it installed
|
|
20
|
+
* on the data disk and, when something is new, registers them and lists them for the Unreal page.
|
|
21
|
+
*/
|
|
22
|
+
export declare function captureLauncherEngines(): Promise<void>;
|
|
23
|
+
/** The project's engine on PATH, its variables, the guide's facts; the engines registered. */
|
|
24
|
+
export declare function setupUnrealEngines(env: ToolEnv, config: Toolchains['unreal'], ctx: ToolCtx): Promise<void>;
|
|
@@ -1,4 +1,164 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
1
|
+
// Unreal engines, for each project: every engine the sandbox has (this PC's, mapped read-only or as folders you
|
|
2
|
+
// registered; the launcher's and other folders in tools\unreal\engines; source builds in project roots) is registered
|
|
3
|
+
// (the registry, the launcher's LauncherInstalled.dat, the .uproject file association), and the engine the project's
|
|
4
|
+
// .uproject names (else the newest) goes on its PATH with UE_ROOT, the derived data cache on the data disk and, with
|
|
5
|
+
// Unreal 5.8's MCP plugin, the editor's MCP server in Claude Code. Machine-wide writes only add; a project that joins
|
|
6
|
+
// later only adds its own engine choice.
|
|
7
|
+
import fs from 'node:fs';
|
|
8
|
+
import path from 'node:path';
|
|
9
|
+
import { readBoundedFile } from '../../../shared/io/bounded.js';
|
|
10
|
+
import { chooseEngine, ddcVars, engineExtraDirs, engineToolDirs, engineVars, fileAssociationPlan, findEngines, findUproject, hasMcpPlugin, incompleteDirs, itemSizes, launcherManifestsDir, launcherEntriesFor, registryPlan, UNREAL_MCP_URL, unrealSummaryLine, uprojectAssociation, versionSelectorIn, versionText, } from '../../../shared/unreal/index.js';
|
|
11
|
+
import { errMsg } from '../../../core/util/errors.js';
|
|
12
|
+
import { exists, mkdirp } from '../../runtime/fs.js';
|
|
13
|
+
import { log, warn } from '../../runtime/log.js';
|
|
14
|
+
import { dryRun } from '../../runtime/paths.js';
|
|
15
|
+
import { runScriptWithJsonAsync } from '../../runtime/process.js';
|
|
16
|
+
import { projectWorkspace } from '../../runtime/project-context.js';
|
|
17
|
+
import { session } from '../../runtime/session.js';
|
|
18
|
+
import { setUnrealMcp, writeAddonMcpServers } from '../../setup/addon-mcp.js';
|
|
19
|
+
import { saveLauncherState, savedEntries, writeLauncherEntries } from './epic-launcher.js';
|
|
20
|
+
import { DDC_DIR, ENGINES_DIR, SAVED_DIR, updateUnrealStatus } from './unreal-status.js';
|
|
21
|
+
const SAVED_MANIFESTS = path.join(SAVED_DIR, 'Manifests');
|
|
22
|
+
/** The real disk, read bounded. */
|
|
23
|
+
export const DISK = {
|
|
24
|
+
exists: (file) => exists(file),
|
|
25
|
+
read: (file, max) => readBoundedFile(file, max),
|
|
26
|
+
list: (dir) => {
|
|
27
|
+
try {
|
|
28
|
+
return fs.readdirSync(dir).slice(0, 500);
|
|
29
|
+
}
|
|
30
|
+
catch {
|
|
31
|
+
return []; // Not a folder: nothing in it.
|
|
32
|
+
}
|
|
33
|
+
},
|
|
34
|
+
};
|
|
35
|
+
/** This PC's engines the host mapped (session.json's pcInstalls). */
|
|
36
|
+
export function mappedEngines(ctx) {
|
|
37
|
+
return ctx.pcInstalls.items
|
|
38
|
+
.filter((i) => i.kind === 'unreal')
|
|
39
|
+
.map((i) => ({
|
|
40
|
+
guest: i.guest,
|
|
41
|
+
id: i.id,
|
|
42
|
+
...(i.writable ? { writable: true } : {}),
|
|
43
|
+
...(i.buildId ? { buildId: i.buildId } : {}),
|
|
44
|
+
}));
|
|
45
|
+
}
|
|
46
|
+
/** The project roots of this sandbox (their folders may hold source builds). */
|
|
47
|
+
function projectRoots() {
|
|
48
|
+
const roots = (session.mappedProjects ?? []).filter((m) => m.root).map((m) => m.guest);
|
|
49
|
+
return [...new Set([...roots, ...(session.roots ?? [])])].filter((r) => exists(r));
|
|
50
|
+
}
|
|
51
|
+
/** The engines the launcher installed in tools\unreal\engines (its saved manifests and entries name them). */
|
|
52
|
+
function launcherDirs() {
|
|
53
|
+
return [...savedEntries().map((e) => e.InstallLocation), ...itemSizes(SAVED_MANIFESTS).keys()];
|
|
54
|
+
}
|
|
55
|
+
/** Every engine this sandbox has (an install the launcher has not finished is not one yet). */
|
|
56
|
+
export function sandboxEngines(ctx, disk = DISK) {
|
|
57
|
+
const incomplete = incompleteDirs(launcherManifestsDir(process.env.ProgramData ?? 'C:\\ProgramData'), SAVED_MANIFESTS);
|
|
58
|
+
return findEngines({
|
|
59
|
+
mapped: mappedEngines(ctx),
|
|
60
|
+
enginesRoot: ENGINES_DIR,
|
|
61
|
+
launcherDirs: launcherDirs(),
|
|
62
|
+
projectRoots: projectRoots(),
|
|
63
|
+
}, disk).filter((e) => !incomplete.has(path.win32.normalize(e.dir).toLowerCase()));
|
|
64
|
+
}
|
|
65
|
+
/** The LauncherInstalled.dat entries for the installed builds: the launcher's own saved ones, else made from them. */
|
|
66
|
+
export function launcherEntries(engines, ctx) {
|
|
67
|
+
return launcherEntriesFor(engines, savedEntries(), ctx.pcInstalls.items);
|
|
68
|
+
}
|
|
69
|
+
/** Registers the engines machine-wide: the registry, LauncherInstalled.dat, the .uproject file association. */
|
|
70
|
+
export async function registerEngines(engines, selectorFrom, ctx, aliases = []) {
|
|
71
|
+
const selector = selectorFrom ? versionSelectorIn(selectorFrom.dir) : null;
|
|
72
|
+
const values = [
|
|
73
|
+
...registryPlan(engines, aliases),
|
|
74
|
+
...(selector && exists(selector) ? fileAssociationPlan(selector) : []),
|
|
75
|
+
];
|
|
76
|
+
if (values.length)
|
|
77
|
+
await runScriptWithJsonAsync('set-registry-values.ps1', 'PlanFile', { values });
|
|
78
|
+
try {
|
|
79
|
+
writeLauncherEntries(launcherEntries(engines, ctx));
|
|
80
|
+
}
|
|
81
|
+
catch (e) {
|
|
82
|
+
warn("Unreal: the Epic Games Launcher's LauncherInstalled.dat was left as it is (" + errMsg(e) + ')');
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
/**
|
|
86
|
+
* While the sandbox runs (the daemon, every few minutes): saves what the launcher learned about engines it installed
|
|
87
|
+
* on the data disk and, when something is new, registers them and lists them for the Unreal page.
|
|
88
|
+
*/
|
|
89
|
+
export async function captureLauncherEngines() {
|
|
90
|
+
const unreal = session.toolchains.unreal;
|
|
91
|
+
if (dryRun || unreal.enabled !== true || !unreal.persistEngines || !saveLauncherState())
|
|
92
|
+
return;
|
|
93
|
+
const ctx = { pcInstalls: session.pcInstalls ?? { items: [] } };
|
|
94
|
+
const engines = sandboxEngines(ctx);
|
|
95
|
+
await registerEngines(engines, engines[0] ?? null, ctx);
|
|
96
|
+
const sizes = itemSizes(SAVED_MANIFESTS);
|
|
97
|
+
updateUnrealStatus((s) => ({ ...s, engines: engines.map((e) => statusEngine(e, sizes)) }));
|
|
98
|
+
}
|
|
99
|
+
const statusEngine = (e, sizes) => ({
|
|
100
|
+
id: e.id,
|
|
101
|
+
dir: e.dir,
|
|
102
|
+
version: versionText(e.version),
|
|
103
|
+
build: e.build,
|
|
104
|
+
origin: e.origin,
|
|
105
|
+
writable: e.writable,
|
|
106
|
+
sizeBytes: sizes.get(e.dir.toLowerCase()) ?? null,
|
|
107
|
+
});
|
|
108
|
+
/** The project's engine on PATH, its variables, the guide's facts; the engines registered. */
|
|
109
|
+
export async function setupUnrealEngines(env, config, ctx) {
|
|
110
|
+
const workspace = projectWorkspace();
|
|
111
|
+
const engines = sandboxEngines(ctx);
|
|
112
|
+
const uproject = findUproject(workspace, DISK);
|
|
113
|
+
const text = uproject ? readBoundedFile(uproject, 1024 * 1024) : null;
|
|
114
|
+
const association = (text ? uprojectAssociation(text.toString('utf8')) : null) ?? { kind: 'none' };
|
|
115
|
+
const choice = chooseEngine(engines, association);
|
|
116
|
+
const engine = choice.engine;
|
|
117
|
+
if (dryRun) {
|
|
118
|
+
log('(dry run) would register ' + engines.length + ' Unreal engines and use ' + (engine?.dir ?? 'none'));
|
|
119
|
+
return;
|
|
120
|
+
}
|
|
121
|
+
try {
|
|
122
|
+
await registerEngines(engines, engine ?? engines[0] ?? null, ctx, engine && choice.alias ? [{ id: choice.alias, dir: engine.dir }] : []);
|
|
123
|
+
}
|
|
124
|
+
catch (e) {
|
|
125
|
+
warn('Unreal: registering the engines failed (' + errMsg(e) + '); Build.bat and RunUAT.bat still work from PATH');
|
|
126
|
+
}
|
|
127
|
+
const ddc = config.derivedDataCache === 'persisted' ? mkdirp(DDC_DIR) : null;
|
|
128
|
+
Object.assign(env.vars, ddcVars(config.derivedDataCache, DDC_DIR));
|
|
129
|
+
if (engine) {
|
|
130
|
+
env.prepend.push(...engineToolDirs(engine.dir, DISK));
|
|
131
|
+
env.append.push(...engineExtraDirs(engine.dir, DISK));
|
|
132
|
+
Object.assign(env.vars, engineVars(engine));
|
|
133
|
+
}
|
|
134
|
+
const mcp = engine && hasMcpPlugin(engine, DISK) ? UNREAL_MCP_URL : null;
|
|
135
|
+
if (mcp || !ctx.duringSession) {
|
|
136
|
+
setUnrealMcp(mcp);
|
|
137
|
+
try {
|
|
138
|
+
writeAddonMcpServers();
|
|
139
|
+
}
|
|
140
|
+
catch (e) {
|
|
141
|
+
warn("Unreal: the editor's MCP server was not added to Claude Code (" + errMsg(e) + ')');
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
const sizes = itemSizes(SAVED_MANIFESTS);
|
|
145
|
+
const status = updateUnrealStatus((s) => ({
|
|
146
|
+
...s,
|
|
147
|
+
engines: engines.map((e) => statusEngine(e, sizes)),
|
|
148
|
+
projects: [
|
|
149
|
+
...s.projects.filter((p) => p.workspace.toLowerCase() !== workspace.toLowerCase()),
|
|
150
|
+
{ workspace, engine: engine?.dir ?? null, reason: choice.reason },
|
|
151
|
+
].slice(-64),
|
|
152
|
+
ddc: { mode: config.derivedDataCache, dir: ddc },
|
|
153
|
+
vaults: ctx.pcInstalls.items.filter((i) => i.kind === 'unreal-vault').map((i) => i.guest),
|
|
154
|
+
mcpUrl: mcp ?? (ctx.duringSession ? s.mcpUrl : null),
|
|
155
|
+
}));
|
|
156
|
+
env.summary.push(unrealSummaryLine(status, workspace));
|
|
157
|
+
log('Unreal: ' +
|
|
158
|
+
engines.length +
|
|
159
|
+
' engine(s); ' +
|
|
160
|
+
(engine ? 'this project uses ' + engine.id + ' at ' + engine.dir : 'none for this project') +
|
|
161
|
+
' (' +
|
|
162
|
+
choice.reason +
|
|
163
|
+
')');
|
|
4
164
|
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { type UnrealStatus } from '../../../shared/unreal/index.js';
|
|
2
|
+
export declare const UNREAL_DIR: string;
|
|
3
|
+
/** Where the Epic Games Launcher installs engines (persistEngines). */
|
|
4
|
+
export declare const ENGINES_DIR: string;
|
|
5
|
+
/** The engines' local derived data cache (derivedDataCache "persisted"). */
|
|
6
|
+
export declare const DDC_DIR: string;
|
|
7
|
+
/** The launcher's manifests and entries of the engines in ENGINES_DIR, kept for later boots. */
|
|
8
|
+
export declare const SAVED_DIR: string;
|
|
9
|
+
export declare function readUnrealStatus(): UnrealStatus;
|
|
10
|
+
/** Changes the status with `change` and writes it (whole, atomically). */
|
|
11
|
+
export declare function updateUnrealStatus(change: (s: UnrealStatus) => UnrealStatus): UnrealStatus;
|
|
12
|
+
/** A fresh status at boot: the machine parts start over, the engines and projects are found again. */
|
|
13
|
+
export declare function resetUnrealStatus(): void;
|