windows-development-sandbox 1.25.1 → 1.27.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 +12 -3
- package/dist/app/features/accounts/services.def.d.ts +1 -1
- package/dist/app/features/accounts/services.def.js +1 -1
- package/dist/app/features/defs.d.ts +198 -3
- package/dist/app/features/defs.js +8 -0
- package/dist/app/features/settings/categories.def.js +1 -0
- package/dist/app/features/unity/index.d.ts +2 -0
- package/dist/app/features/unity/index.js +3 -0
- package/dist/app/features/unity/unity.def.d.ts +294 -0
- package/dist/app/features/unity/unity.def.js +471 -0
- package/dist/app/features/unity/unity.run.d.ts +142 -0
- package/dist/app/features/unity/unity.run.js +363 -0
- package/dist/app/features/unreal/unreal.run.js +13 -3
- package/dist/app/registry.js +6 -0
- package/dist/bin/agent-sandbox.js +2 -2
- package/dist/bin/{chunk-G3TG4ZT7.js → chunk-4TLSWPG7.js} +1 -1
- package/dist/bin/{chunk-3GAXLATK.js → chunk-QUHNXHVG.js} +72 -5
- package/dist/bin/{chunk-F4PVBN23.js → chunk-RSIX7C6D.js} +3633 -1972
- 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 +14 -1
- package/dist/cli/commands/registry.js +3 -0
- package/dist/cli/manifest.d.ts +33 -1
- package/dist/cli/manifest.js +32 -1
- 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 +40 -3
- package/dist/cli/menu/views.js +2 -0
- package/dist/core/config/defaults.js +16 -3
- package/dist/core/schema/accounts.d.ts +3 -0
- package/dist/core/schema/accounts.js +1 -0
- package/dist/core/schema/config.d.ts +29 -13
- package/dist/core/schema/github.d.ts +1 -0
- package/dist/core/schema/github.js +3 -0
- package/dist/core/schema/pc-installs.d.ts +2 -1
- package/dist/core/schema/pc-installs.js +6 -1
- package/dist/core/schema/toolchains.d.ts +8 -0
- package/dist/core/schema/toolchains.js +19 -4
- package/dist/guest/accounts/adapters.js +2 -0
- package/dist/guest/accounts/apply.d.ts +4 -2
- package/dist/guest/accounts/kinds/perplexityapp.d.ts +7 -0
- package/dist/guest/accounts/kinds/perplexityapp.js +53 -0
- package/dist/guest/accounts/kinds/unity.d.ts +27 -0
- package/dist/guest/accounts/kinds/unity.js +164 -7
- package/dist/guest/apps/app-installer.js +18 -0
- package/dist/guest/apps/store-app.d.ts +4 -0
- package/dist/guest/apps/store-app.js +19 -0
- package/dist/guest/apps/store-check.d.ts +8 -0
- package/dist/guest/apps/store-check.js +33 -0
- package/dist/guest/boot/boot.js +1 -1
- package/dist/guest/boot/guide.js +18 -0
- package/dist/guest/browser/comet.d.ts +3 -1
- package/dist/guest/browser/comet.js +1 -1
- package/dist/guest/browser/perplexity-app.d.ts +12 -0
- package/dist/guest/browser/perplexity-app.js +60 -0
- package/dist/guest/daemon/daemon.js +2 -0
- package/dist/guest/devtools/devtool.d.ts +1 -1
- package/dist/guest/devtools/registry.js +1 -2
- package/dist/guest/entries/tool.js +1 -1
- package/dist/guest/setup/addon-mcp.d.ts +7 -2
- package/dist/guest/setup/addon-mcp.js +19 -9
- package/dist/guest/setup/github-token.js +4 -4
- package/dist/guest/setup/unity-agent.d.ts +11 -0
- package/dist/guest/setup/unity-agent.js +39 -0
- package/dist/guest/toolchains/engines/epic-launcher.d.ts +1 -1
- package/dist/guest/toolchains/engines/epic-launcher.js +11 -7
- package/dist/guest/toolchains/engines/unity-cli.d.ts +24 -0
- package/dist/guest/toolchains/engines/unity-cli.js +74 -0
- package/dist/guest/toolchains/engines/unity-hub.d.ts +2 -0
- package/dist/guest/toolchains/engines/unity-hub.js +27 -0
- package/dist/guest/toolchains/engines/unity-release.d.ts +4 -15
- package/dist/guest/toolchains/engines/unity-release.js +17 -5
- package/dist/guest/toolchains/engines/unity-status.d.ts +11 -0
- package/dist/guest/toolchains/engines/unity-status.js +39 -0
- package/dist/guest/toolchains/engines/unity.d.ts +20 -1
- package/dist/guest/toolchains/engines/unity.js +250 -3
- package/dist/guest/toolchains/engines/unreal-status.js +1 -1
- package/dist/guest/toolchains/native/mesa.js +35 -11
- package/dist/guest/toolchains/native/unreal.js +1 -0
- package/dist/guest/toolchains/plan.js +5 -1
- package/dist/guest/toolchains/tools/cli.js +2 -2
- package/dist/host/accounts/deliver.d.ts +3 -0
- package/dist/host/accounts/deliver.js +23 -0
- package/dist/host/accounts/kinds/perplexityapp.d.ts +2 -0
- package/dist/host/accounts/kinds/perplexityapp.js +8 -0
- package/dist/host/accounts/kinds/unity.d.ts +1 -1
- package/dist/host/accounts/kinds/unity.js +60 -8
- package/dist/host/accounts/leases.d.ts +2 -1
- package/dist/host/accounts/source.d.ts +2 -0
- package/dist/host/accounts/source.js +2 -0
- package/dist/host/accounts/sources.js +2 -0
- package/dist/host/apps/index.d.ts +1 -0
- package/dist/host/apps/index.js +2 -0
- package/dist/host/apps/store-package.d.ts +51 -0
- package/dist/host/apps/store-package.js +209 -0
- package/dist/host/github/deliver.d.ts +8 -3
- package/dist/host/github/deliver.js +42 -29
- package/dist/host/github/index.d.ts +1 -0
- package/dist/host/github/index.js +1 -0
- package/dist/host/github/pc-login.d.ts +27 -0
- package/dist/host/github/pc-login.js +129 -0
- package/dist/host/pc-installs/discover.d.ts +4 -0
- package/dist/host/pc-installs/discover.js +19 -6
- package/dist/host/pc-installs/epic.js +3 -0
- package/dist/host/pc-installs/source.d.ts +1 -1
- package/dist/host/pc-installs/unity-assets.d.ts +5 -0
- package/dist/host/pc-installs/unity-assets.js +18 -0
- package/dist/host/pc-installs/unity.d.ts +22 -0
- package/dist/host/pc-installs/unity.js +144 -2
- package/dist/host/pc-installs/unreal.d.ts +2 -0
- package/dist/host/pc-installs/unreal.js +24 -8
- package/dist/runtime/agent.js +116 -11
- package/dist/runtime/daemon.js +2205 -987
- package/dist/runtime/db.js +116 -11
- package/dist/runtime/main.js +2578 -1375
- package/dist/runtime/netproxy.js +116 -11
- package/dist/runtime/tool.js +133 -31
- package/dist/runtime/view.js +116 -11
- package/dist/selftest/agents-web.test.js +2 -1
- package/dist/selftest/engines.test.js +32 -3
- package/dist/selftest/github.test.js +64 -1
- package/dist/selftest/index.js +10 -0
- package/dist/selftest/menu-screens.js +3 -1
- package/dist/selftest/mixamo.test.js +1 -1
- package/dist/selftest/persisted-installs.test.js +1 -1
- package/dist/selftest/service-accounts.test.js +13 -2
- package/dist/selftest/store-app.test.d.ts +2 -0
- package/dist/selftest/store-app.test.js +162 -0
- package/dist/selftest/support/engine-golden.js +7 -0
- package/dist/selftest/support/fixtures.d.ts +10 -1
- package/dist/selftest/support/unity-sample.d.ts +2 -0
- package/dist/selftest/support/unity-sample.js +80 -0
- package/dist/selftest/unity-accounts.test.d.ts +2 -0
- package/dist/selftest/unity-accounts.test.js +106 -0
- package/dist/selftest/unity-agent.test.d.ts +2 -0
- package/dist/selftest/unity-agent.test.js +47 -0
- package/dist/selftest/unity-page.test.d.ts +2 -0
- package/dist/selftest/unity-page.test.js +177 -0
- package/dist/selftest/unity-setup.test.d.ts +2 -0
- package/dist/selftest/unity-setup.test.js +49 -0
- package/dist/selftest/unity.test.js +238 -3
- package/dist/shared/accounts/index.d.ts +1 -0
- package/dist/shared/accounts/index.js +1 -0
- package/dist/shared/accounts/kinds/unity.d.ts +28 -0
- package/dist/shared/accounts/kinds/unity.js +88 -0
- package/dist/shared/accounts/kinds.d.ts +2 -2
- package/dist/shared/accounts/kinds.js +23 -1
- package/dist/shared/accounts/payloads.d.ts +52 -0
- package/dist/shared/accounts/payloads.js +14 -2
- package/dist/shared/apps/catalogue-agents-web.d.ts +7 -0
- package/dist/shared/apps/catalogue-agents-web.js +23 -0
- package/dist/shared/apps/catalogue-gamedev.d.ts +3 -0
- package/dist/shared/apps/catalogue-gamedev.js +31 -0
- package/dist/shared/apps/catalogue.d.ts +8 -0
- package/dist/shared/apps/index.d.ts +3 -2
- package/dist/shared/apps/index.js +3 -2
- package/dist/shared/apps/selection.d.ts +6 -0
- package/dist/shared/apps/selection.js +9 -0
- package/dist/shared/apps/store-package.d.ts +31 -0
- package/dist/shared/apps/store-package.js +34 -0
- package/dist/shared/channel/schema.d.ts +24 -0
- package/dist/shared/channel/secrets.d.ts +17 -0
- package/dist/shared/paths/pc-installs.d.ts +1 -1
- package/dist/shared/paths/pc-installs.js +1 -0
- package/dist/shared/readiness.d.ts +2 -2
- package/dist/shared/readiness.js +2 -2
- package/dist/shared/unity/asset-cache.d.ts +20 -0
- package/dist/shared/unity/asset-cache.js +44 -0
- package/dist/shared/unity/cli.d.ts +40 -0
- package/dist/shared/unity/cli.js +77 -0
- package/dist/shared/unity/editors.d.ts +46 -0
- package/dist/shared/unity/editors.js +89 -0
- package/dist/shared/unity/guide.d.ts +9 -0
- package/dist/shared/unity/guide.js +84 -0
- package/dist/shared/unity/index.d.ts +9 -0
- package/dist/shared/unity/index.js +11 -0
- package/dist/shared/unity/manifest.d.ts +10 -0
- package/dist/shared/unity/manifest.js +53 -0
- package/dist/shared/unity/mcp.d.ts +8 -0
- package/dist/shared/unity/mcp.js +12 -0
- package/dist/shared/unity/release.d.ts +28 -0
- package/dist/shared/unity/release.js +83 -0
- package/dist/shared/unity/status.d.ts +59 -0
- package/dist/shared/unity/status.js +45 -0
- package/dist/shared/unity/version.d.ts +27 -0
- package/dist/shared/unity/version.js +75 -0
- package/dist/shared/unreal/status.d.ts +1 -0
- package/dist/shared/unreal/status.js +2 -0
- package/dist/web/index.html +1 -1
- package/dist/web/{main-FNE6XEHA.js → main-VPMYTSUL.js} +67 -67
- package/native/csharp/HcsHost/AssemblyInfo.cs +3 -3
- package/native/csharp/HostTools/AssemblyInfo.cs +3 -3
- package/native/csharp/StorageHelper/AssemblyInfo.cs +3 -3
- package/native/scripts/get-store-package.ps1 +58 -0
- package/package.json +1 -1
- package/sandbox/addons/openupm-cli/package-lock.json +2921 -0
- package/sandbox/addons/openupm-cli/package.json +7 -0
- package/sandbox/addons/unity-mcp/requirements.in +1 -0
- package/sandbox/addons/unity-mcp/requirements.txt +1165 -0
- package/sandbox/csharp/AppActivation.cs +38 -0
- package/sandbox/scripts/install-store-package.ps1 +28 -0
- package/sandbox/scripts/start-store-app.ps1 +29 -0
- package/schemas/config.schema.json +61 -6
- package/schemas/sandbox-config.schema.json +38 -5
- package/dist/guest/devtools/unity.d.ts +0 -2
- package/dist/guest/devtools/unity.js +0 -7
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
// The Unity page and its host side: this PC's editors and Asset Store downloads (a temp PC tree), "auto" following the
|
|
2
|
+
// launch, the status file never read through a link, the page itself (no command text, every operation on it) and the
|
|
3
|
+
// editor bridge edited in a temp project. No network: the Pipeline version lookup is given a fake.
|
|
4
|
+
import fs from 'node:fs';
|
|
5
|
+
import path from 'node:path';
|
|
6
|
+
import { DEFAULTS } from '../core/config/index.js';
|
|
7
|
+
import { pageActions } from '../app/kernel/index.js';
|
|
8
|
+
import { unityPage, unityView } from '../app/features/unity/index.js';
|
|
9
|
+
import { addBridgeToProject, latestPipelineVersion, readStatus, removeBridgeFromProject, } from '../app/features/unity/unity.run.js';
|
|
10
|
+
import { discoverPcInstalls } from '../host/pc-installs/index.js';
|
|
11
|
+
import { findUnityEditorsOnPc, unityAssetCacheDirs, unityPcFiles } from '../host/pc-installs/unity.js';
|
|
12
|
+
import { unityAssetInstalls } from '../host/pc-installs/unity-assets.js';
|
|
13
|
+
import { pageLines } from '../cli/output/render-view.js';
|
|
14
|
+
import { UNITY_STATUS_FILE, bridgeOf } from '../shared/unity/index.js';
|
|
15
|
+
import { UNITY_SAMPLE } from './support/unity-sample.js';
|
|
16
|
+
const STATUS = {
|
|
17
|
+
updatedAt: '2026-11-05T12:00:00.000Z',
|
|
18
|
+
cli: null,
|
|
19
|
+
editors: [],
|
|
20
|
+
projects: [],
|
|
21
|
+
licence: { state: 'unknown', kind: '', detail: '', user: null },
|
|
22
|
+
assetCache: { mode: 'persisted', dir: null },
|
|
23
|
+
pcAssets: [],
|
|
24
|
+
hub: { installed: false, path: null },
|
|
25
|
+
mcp: null,
|
|
26
|
+
skills: false,
|
|
27
|
+
problem: null,
|
|
28
|
+
};
|
|
29
|
+
const MANIFEST = JSON.stringify({ dependencies: { 'com.unity.ugui': '2.0.0', 'com.unity.modules.ai': '1.0.0' } }, null, 2) + '\n';
|
|
30
|
+
export const unityPageSuite = {
|
|
31
|
+
name: 'unity-page',
|
|
32
|
+
title: "The Unity page: this PC's editors and downloads, auto, the status file, the bridge",
|
|
33
|
+
run: async (t) => {
|
|
34
|
+
t.section("this PC's editors");
|
|
35
|
+
const pcDir = t.dir('pc');
|
|
36
|
+
const appData = path.join(pcDir, 'AppData');
|
|
37
|
+
const programFiles = path.join(pcDir, 'Program Files');
|
|
38
|
+
const editor = (root) => t.write(path.join(root, 'Editor', 'Unity.exe'), 'x');
|
|
39
|
+
editor(path.join(programFiles, 'Unity', 'Hub', 'Editor', '6000.0.58f2'));
|
|
40
|
+
const second = path.join(pcDir, 'Editors');
|
|
41
|
+
editor(path.join(second, '2022.3.20f1'));
|
|
42
|
+
t.write(path.join(appData, 'UnityHub', 'secondaryInstallPath.json'), JSON.stringify(second));
|
|
43
|
+
const listedRoot = path.join(pcDir, 'Elsewhere', '2021.3.1f1');
|
|
44
|
+
editor(listedRoot);
|
|
45
|
+
editor(path.join(pcDir, 'Elsewhere', 'evil'));
|
|
46
|
+
t.write(path.join(appData, 'UnityHub', 'editors-v2.json'), JSON.stringify({
|
|
47
|
+
'schema-version': 'v2',
|
|
48
|
+
data: [
|
|
49
|
+
{ version: '2021.3.1f1', location: [path.join(listedRoot, 'Editor', 'Unity.exe')], other: 1 },
|
|
50
|
+
{ version: '../../evil', location: path.join(pcDir, 'Elsewhere', 'evil', 'Editor', 'Unity.exe') },
|
|
51
|
+
{ version: '2020.3.1f1', location: path.join(pcDir, 'Elsewhere', 'x', 'Editor', 'notunity.exe') },
|
|
52
|
+
{ version: 5, location: 3 },
|
|
53
|
+
],
|
|
54
|
+
}));
|
|
55
|
+
const files = unityPcFiles({ appData, localAppData: '', programData: '', programFiles });
|
|
56
|
+
const found = findUnityEditorsOnPc(files, true);
|
|
57
|
+
t.equal(found.map((e) => e.id), ['6000.0.58f2', '2022.3.20f1', '2021.3.1f1'], 'the default folder, the secondary folder and editors-v2.json are read; a bad version or exe name is skipped');
|
|
58
|
+
t.check(found.every((e) => e.kind === 'unity' && e.name === 'Unity ' + e.id && e.version === e.id), 'each editor is named by its version');
|
|
59
|
+
t.equal(findUnityEditorsOnPc(files, ['2022.3.20f1']).map((e) => e.id), ['2022.3.20f1'], 'a list asks for exact versions only');
|
|
60
|
+
t.write(path.join(appData, 'UnityHub', 'secondaryInstallPath.json'), JSON.stringify('relative\\dir'));
|
|
61
|
+
t.equal(findUnityEditorsOnPc(files, true).map((e) => e.id), ['6000.0.58f2', '2021.3.1f1'], 'a relative secondary path is ignored');
|
|
62
|
+
t.section("this PC's Asset Store downloads");
|
|
63
|
+
const store = path.join(appData, 'Unity', 'Asset Store-5.x');
|
|
64
|
+
t.write(path.join(store, 'Kenney', '2D', 'Pack.unitypackage'), 'x');
|
|
65
|
+
const envRoot = t.dir('pc', 'Cache');
|
|
66
|
+
t.dir('pc', 'Cache', 'Asset Store-5.x');
|
|
67
|
+
t.equal(unityAssetCacheDirs(files, undefined), [store], 'the default folder under %APPDATA%\\Unity');
|
|
68
|
+
t.equal(unityAssetCacheDirs(files, envRoot), [path.join(envRoot, 'Asset Store-5.x'), store], 'the environment variable first, then the default');
|
|
69
|
+
t.equal(unityAssetInstalls([store, store + '2']).map((i) => i.id), ['AssetStore', 'AssetStore2'], 'mapped at AssetStore, AssetStore2');
|
|
70
|
+
t.section('auto follows the launch');
|
|
71
|
+
const host = t.dir('auto-host', '6000.0.58f2');
|
|
72
|
+
const unitySource = {
|
|
73
|
+
kind: 'unity',
|
|
74
|
+
setting: 'unityEditors',
|
|
75
|
+
find: () => [{ kind: 'unity', id: '6000.0.58f2', name: 'Unity 6000.0.58f2', host, guest: '' }],
|
|
76
|
+
};
|
|
77
|
+
const unrealSource = {
|
|
78
|
+
kind: 'unreal',
|
|
79
|
+
setting: 'unrealEngines',
|
|
80
|
+
find: () => [{ kind: 'unreal', id: 'UE_5.4', name: 'Unreal Engine 5.4', host, guest: '' }],
|
|
81
|
+
};
|
|
82
|
+
const run = (o) => discoverPcInstalls(DEFAULTS.pcInstalls, {
|
|
83
|
+
packageRoot: t.packageRoot,
|
|
84
|
+
storageRoot: t.dir('auto-storage'),
|
|
85
|
+
sources: [unitySource, unrealSource],
|
|
86
|
+
...o,
|
|
87
|
+
}).manifest.items.map((i) => i.guest);
|
|
88
|
+
t.equal(run({ unityOn: true }), ['C:\\PcInstalls\\unity\\6000.0.58f2'], 'Unity on: the editors, not the engines');
|
|
89
|
+
t.equal(run({ unrealOn: true }), ['C:\\PcInstalls\\unreal\\UE_5.4'], 'Unreal on: the engines, not the editors');
|
|
90
|
+
t.equal(run({}), [], 'neither on: nothing');
|
|
91
|
+
t.section('the status file');
|
|
92
|
+
const data = t.dir('status-data');
|
|
93
|
+
const target = t.dir('status-elsewhere');
|
|
94
|
+
t.write(path.join(target, UNITY_STATUS_FILE), JSON.stringify(STATUS));
|
|
95
|
+
t.write(path.join(data, 'plain', 'tools', 'unity', UNITY_STATUS_FILE), JSON.stringify(STATUS));
|
|
96
|
+
t.check(readStatus(path.join(data, 'plain')) !== null, 'a status file in the data folder is read');
|
|
97
|
+
fs.mkdirSync(path.join(data, 'linked', 'tools'), { recursive: true });
|
|
98
|
+
fs.symlinkSync(target, path.join(data, 'linked', 'tools', 'unity'), 'junction');
|
|
99
|
+
t.check(readStatus(path.join(data, 'linked')) === null, 'a junction at tools\\unity is never followed');
|
|
100
|
+
t.write(path.join(data, 'bad', 'tools', 'unity', UNITY_STATUS_FILE), '{"editors": "x"}');
|
|
101
|
+
t.check(readStatus(path.join(data, 'bad')) === null, 'a status file that does not fit the schema is not used');
|
|
102
|
+
t.section('the page');
|
|
103
|
+
const page = unityPage(UNITY_SAMPLE);
|
|
104
|
+
t.equal(page.sections.map((s) => s.id), ['editors', 'pc', 'assets', 'licence', 'projects', 'tools', 'settings'], 'editors, this PC, assets, licence, projects, agent tools, settings');
|
|
105
|
+
const actions = pageActions(page);
|
|
106
|
+
const ops = actions.flatMap((a) => (a.kind === 'operation' ? [a.op] : []));
|
|
107
|
+
t.check(unityView.operations.every((op) => ops.includes(op.id) || op.id === 'unity.openHub'), 'every operation of the view is on the page (Open Unity Hub once the Hub is in the sandbox)');
|
|
108
|
+
const withHub = pageActions(unityPage({ ...UNITY_SAMPLE, hub: { installed: true, setting: true } }));
|
|
109
|
+
t.check(withHub.some((a) => a.kind === 'operation' && a.op === 'unity.openHub'), 'Open Unity Hub is offered when the Hub is installed');
|
|
110
|
+
t.check(actions.some((a) => a.kind === 'link' && a.target.kind === 'view' && a.target.view === 'accounts'), 'a link to Unity sign-in in Accounts');
|
|
111
|
+
const settingKeys = actions.flatMap((a) => a.kind === 'link' && a.target.kind === 'setting' ? [a.target.key] : []);
|
|
112
|
+
t.check(settingKeys.includes('pcInstalls.unityEditors'), 'the settings are links');
|
|
113
|
+
const text = pageLines(page).join('\n');
|
|
114
|
+
for (const needle of [
|
|
115
|
+
'6000.0.58f2',
|
|
116
|
+
'this PC, read-only',
|
|
117
|
+
'Platformer Pack',
|
|
118
|
+
'dev@example.com',
|
|
119
|
+
'toolchains.unity.assetCache',
|
|
120
|
+
'pcInstalls.unityAssets',
|
|
121
|
+
])
|
|
122
|
+
t.includes(text, needle, 'the page shows ' + needle);
|
|
123
|
+
t.check(!/sandbox (unity|launch|config|account|projects)/.test(text), 'no page text tells you to run a command');
|
|
124
|
+
t.excludes(text, 'unity editors', 'no Unity CLI command on the page');
|
|
125
|
+
t.excludes(text, ' --', 'no flag on the page');
|
|
126
|
+
const empty = pageLines(unityPage({ ...UNITY_SAMPLE, setUp: false, editors: [], projects: [] })).join('\n');
|
|
127
|
+
t.includes(empty, 'No sandbox has set Unity up yet', 'nothing set up: the page says so');
|
|
128
|
+
t.section('the editor bridge');
|
|
129
|
+
const project = t.dir('bridge', 'Game');
|
|
130
|
+
t.write(path.join(project, 'ProjectSettings', 'ProjectVersion.txt'), 'm_EditorVersion: 6000.0.58f2\n');
|
|
131
|
+
const manifest = t.write(path.join(project, 'Packages', 'manifest.json'), MANIFEST);
|
|
132
|
+
const hooks = {
|
|
133
|
+
pipelineVersion: () => Promise.resolve('0.10.1'),
|
|
134
|
+
confirm: () => Promise.resolve(true),
|
|
135
|
+
};
|
|
136
|
+
const declined = await addBridgeToProject(project, undefined, { ...hooks, confirm: () => Promise.resolve(false) });
|
|
137
|
+
t.check(fs.readFileSync(manifest, 'utf8') === MANIFEST && declined.message === 'Nothing was changed.', 'no: nothing changes');
|
|
138
|
+
const added = await addBridgeToProject(project, undefined, hooks);
|
|
139
|
+
const after = JSON.parse(fs.readFileSync(manifest, 'utf8'));
|
|
140
|
+
t.equal(Object.keys(after.dependencies), ['com.unity.ugui', 'com.unity.modules.ai', 'com.unity.pipeline'], 'exactly one dependency is added, the others keep their order');
|
|
141
|
+
t.check(after.dependencies['com.unity.pipeline'] === '0.10.1', 'the Pipeline version is the one looked up');
|
|
142
|
+
t.check(bridgeOf(fs.readFileSync(manifest, 'utf8')) === 'pipeline' && added.file === manifest, 'it is the Pipeline bridge');
|
|
143
|
+
const again = await addBridgeToProject(project, undefined, hooks);
|
|
144
|
+
t.includes(again.message, 'already', 'adding twice changes nothing');
|
|
145
|
+
await removeBridgeFromProject(project, hooks);
|
|
146
|
+
t.check(fs.readFileSync(manifest, 'utf8') === MANIFEST, 'removing restores the bytes');
|
|
147
|
+
const crlf = MANIFEST.replace(/\n/g, '\r\n');
|
|
148
|
+
fs.writeFileSync(manifest, crlf);
|
|
149
|
+
await addBridgeToProject(project, undefined, hooks);
|
|
150
|
+
await removeBridgeFromProject(project, hooks);
|
|
151
|
+
t.check(fs.readFileSync(manifest, 'utf8') === crlf, 'a CRLF manifest comes back as it was');
|
|
152
|
+
const old = t.dir('bridge', 'Old');
|
|
153
|
+
t.write(path.join(old, 'ProjectSettings', 'ProjectVersion.txt'), 'm_EditorVersion: 2022.3.20f1\n');
|
|
154
|
+
const oldManifest = t.write(path.join(old, 'Packages', 'manifest.json'), MANIFEST);
|
|
155
|
+
await addBridgeToProject(old, undefined, hooks);
|
|
156
|
+
t.check(bridgeOf(fs.readFileSync(oldManifest, 'utf8')) === 'coplay', 'Unity before 6 gets the Coplay bridge');
|
|
157
|
+
await t.throws(() => addBridgeToProject(t.dir('bridge', 'Old2'), 'pipeline', hooks), /not a Unity project/, 'a folder without ProjectVersion.txt is refused');
|
|
158
|
+
const old2 = t.dir('bridge', 'Old3');
|
|
159
|
+
t.write(path.join(old2, 'ProjectSettings', 'ProjectVersion.txt'), 'm_EditorVersion: 2022.3.20f1\n');
|
|
160
|
+
t.write(path.join(old2, 'Packages', 'manifest.json'), MANIFEST);
|
|
161
|
+
await t.throws(() => addBridgeToProject(old2, 'pipeline', hooks), /Unity 6/, 'Pipeline is refused before Unity 6');
|
|
162
|
+
const offline = t.dir('bridge', 'Offline');
|
|
163
|
+
t.write(path.join(offline, 'ProjectSettings', 'ProjectVersion.txt'), 'm_EditorVersion: 6000.0.58f2\n');
|
|
164
|
+
const offlineManifest = t.write(path.join(offline, 'Packages', 'manifest.json'), MANIFEST);
|
|
165
|
+
const fallback = await addBridgeToProject(offline, undefined, {
|
|
166
|
+
...hooks,
|
|
167
|
+
pipelineVersion: () => Promise.reject(new Error('offline')),
|
|
168
|
+
});
|
|
169
|
+
t.includes(fallback.message, 'offline', 'no answer from Unity: the bundled version is used and it says so');
|
|
170
|
+
t.check(bridgeOf(fs.readFileSync(offlineManifest, 'utf8')) === 'pipeline', 'the bridge is still added');
|
|
171
|
+
t.section('the Pipeline version lookup (a fake registry)');
|
|
172
|
+
const reply = (body) => () => Promise.resolve(new Response(body));
|
|
173
|
+
t.check((await latestPipelineVersion(reply(JSON.stringify({ 'dist-tags': { latest: '0.11.0' } })))) === '0.11.0', 'dist-tags.latest is used');
|
|
174
|
+
await t.throws(() => latestPipelineVersion(reply(JSON.stringify({ 'dist-tags': { latest: '1; rm -rf' } }))), /no usable version/, 'a version that is not semver is refused');
|
|
175
|
+
await t.throws(() => latestPipelineVersion(reply('x'.repeat(3 * 1024 * 1024))), /over 2 MB/, 'a huge answer is refused');
|
|
176
|
+
},
|
|
177
|
+
};
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
// Unity set-up on the data disk: an editor is installed into tools\unity\editors\<version> through ensurePersisted (a
|
|
2
|
+
// fake installer in a temp folder stands in for the Unity CLI), kept, pruned; sbx-unity is gone. Nothing is downloaded.
|
|
3
|
+
import fs from 'node:fs';
|
|
4
|
+
import path from 'node:path';
|
|
5
|
+
import { ensurePersisted } from '../guest/toolchains/persisted.js';
|
|
6
|
+
const GB = 1024 ** 3;
|
|
7
|
+
export const unitySetupSuite = {
|
|
8
|
+
name: 'unity-setup',
|
|
9
|
+
title: 'Unity set-up on the data disk: editors kept between sandboxes, no sbx-unity helper',
|
|
10
|
+
run: async (t) => {
|
|
11
|
+
t.section('an editor on the data disk');
|
|
12
|
+
const tools = t.dir('unity-tools');
|
|
13
|
+
const deps = { toolsDir: tools, statfs: () => ({ bavail: 100 * GB, bsize: 1 }) };
|
|
14
|
+
const spec = (version) => ({
|
|
15
|
+
name: 'unity/editors',
|
|
16
|
+
version,
|
|
17
|
+
needBytes: 10 * GB,
|
|
18
|
+
marker: 'Editor\\Unity.exe',
|
|
19
|
+
what: 'Unity ' + version,
|
|
20
|
+
keepVersions: 2,
|
|
21
|
+
});
|
|
22
|
+
const fake = (version) => async (staging, temp) => {
|
|
23
|
+
fs.writeFileSync(path.join(temp, 'download.part'), 'x');
|
|
24
|
+
fs.mkdirSync(path.join(staging, 'Editor'), { recursive: true });
|
|
25
|
+
fs.writeFileSync(path.join(staging, 'Editor', 'Unity.exe'), 'unity ' + version);
|
|
26
|
+
};
|
|
27
|
+
const first = await ensurePersisted(spec('6000.0.58f2'), fake('6000.0.58f2'), deps);
|
|
28
|
+
t.check(first.fresh && first.dir === path.join(tools, 'unity', 'editors', '6000.0.58f2'), 'the editor goes to tools\\unity\\editors\\<version>');
|
|
29
|
+
let ran = false;
|
|
30
|
+
const second = await ensurePersisted(spec('6000.0.58f2'), async () => {
|
|
31
|
+
ran = true;
|
|
32
|
+
}, deps);
|
|
33
|
+
t.check(!second.fresh && !ran, 'a second call finds it and installs nothing');
|
|
34
|
+
await t.throws(() => ensurePersisted(spec('6000.0.60f1'), async () => {
|
|
35
|
+
throw new Error('the CLI failed');
|
|
36
|
+
}, deps), 'the CLI failed', 'a failed install is reported');
|
|
37
|
+
const editors = path.join(tools, 'unity', 'editors');
|
|
38
|
+
t.equal(fs.readdirSync(editors).filter((n) => n.startsWith('.staging-')).length, 0, 'a failed install leaves no .staging-* folder');
|
|
39
|
+
await ensurePersisted(spec('6000.0.59f1'), fake('6000.0.59f1'), deps);
|
|
40
|
+
await ensurePersisted(spec('6000.1.0f1'), fake('6000.1.0f1'), deps);
|
|
41
|
+
const kept = fs.readdirSync(editors).sort();
|
|
42
|
+
t.check(kept.length === 2 && kept.includes('6000.1.0f1') && !kept.includes('6000.0.58f2'), 'keepVersions 2 removes the least recently used editor');
|
|
43
|
+
t.section('no sbx-unity helper');
|
|
44
|
+
t.check(!fs.existsSync(path.join(t.packageRoot, 'src', 'guest', 'devtools', 'unity.ts')), 'devtools/unity.ts is gone');
|
|
45
|
+
const toolJs = path.join(t.packageRoot, 'dist', 'runtime', 'tool.js');
|
|
46
|
+
t.check(!fs.existsSync(toolJs) ||
|
|
47
|
+
!fs.readFileSync(toolJs, 'utf8').includes('build with -executeMethod, the licence status'), 'runtime/tool.js has no sbx-unity helper (the setting texts may still name the MCP server)');
|
|
48
|
+
},
|
|
49
|
+
};
|
|
@@ -1,7 +1,242 @@
|
|
|
1
|
+
// Unity: project versions, the editor choice, the release API, the Unity CLI, the editor bridge, the Asset Store cache,
|
|
2
|
+
// the status, the guide lines and the licence file helpers (pure; nothing is downloaded or run).
|
|
3
|
+
import fs from 'node:fs';
|
|
4
|
+
import { parseLicenceStatus, ulfSummary, UnityPayloadSchema, unityEntitlementFile, unitySummary, unityUlfFile, } from '../shared/accounts/index.js';
|
|
5
|
+
import { isKindReady, isToolchainReady } from '../shared/readiness.js';
|
|
6
|
+
import { assetPackages, bridgeOf, chooseEditor, cliManifestUrl, CliManifestSchema, COPLAY_PACKAGE, COPLAY_PACKAGE_URL, compareUnityVersions, editorModules, editorsAddArgs, installArgs, installModulesArgs, installNeedBytes, installPathArgs, licenceStatusArgs, ORIGIN_LABELS, parseProjectVersion, pcAssetCacheDirs, pickCliBinary, pickRelease, PIPELINE_PACKAGE, releaseUrl, supportsPipeline, UNITY_CLI_ENV, unityCliMcpServer, unityGuideLines, UnityStatusSchema, wantedVersion, withBridge, } from '../shared/unity/index.js';
|
|
7
|
+
const editor = (version, origin, modules = []) => {
|
|
8
|
+
const root = origin === 'pc' ? 'C:\\PcInstalls\\unity\\' + version : 'C:\\AgentData\\tools\\unity\\editors\\' + version;
|
|
9
|
+
return { version, root, exe: root + '\\Editor\\Unity.exe', origin, writable: origin === 'data', modules };
|
|
10
|
+
};
|
|
11
|
+
const RELEASE = {
|
|
12
|
+
results: [
|
|
13
|
+
{
|
|
14
|
+
version: '6000.0.58f2',
|
|
15
|
+
shortRevision: '92dee566b325',
|
|
16
|
+
downloads: [
|
|
17
|
+
{
|
|
18
|
+
url: 'https://download.unity3d.com/download_unity/92dee566b325/Windows64EditorInstaller/UnitySetup64-6000.0.58f2.exe',
|
|
19
|
+
type: 'EXE',
|
|
20
|
+
integrity: 'md5-AAAAAAAAAAAAAAAAAAAAAA==',
|
|
21
|
+
downloadSize: { value: 4_000_000_000 },
|
|
22
|
+
installedSize: { value: 8_000_000_000 },
|
|
23
|
+
modules: [
|
|
24
|
+
{
|
|
25
|
+
id: 'android',
|
|
26
|
+
downloadSize: { value: 100 },
|
|
27
|
+
installedSize: { value: 200 },
|
|
28
|
+
integrity: 'md5-BBBBBBBBBBBBBBBBBBBBBB==',
|
|
29
|
+
subModules: [
|
|
30
|
+
{ id: 'android-sdk-ndk-tools', downloadSize: { value: 10 }, installedSize: { value: 20 } },
|
|
31
|
+
{ id: 'android-open-jdk', downloadSize: { value: 1 }, installedSize: { value: 2 } },
|
|
32
|
+
],
|
|
33
|
+
},
|
|
34
|
+
{ id: 'webgl', downloadSize: { value: 50 }, installedSize: { value: 60 } },
|
|
35
|
+
],
|
|
36
|
+
},
|
|
37
|
+
],
|
|
38
|
+
},
|
|
39
|
+
],
|
|
40
|
+
};
|
|
41
|
+
const SHA = 'a'.repeat(64);
|
|
42
|
+
function status(over = {}) {
|
|
43
|
+
return {
|
|
44
|
+
updatedAt: 'now',
|
|
45
|
+
cli: { version: '1.0.0-beta.16', path: 'C:\\AgentData\\tools\\unity\\cli\\1.0.0-beta.16\\unity.exe' },
|
|
46
|
+
editors: [
|
|
47
|
+
{ ...editor('6000.0.58f2', 'pc'), modules: [], sizeBytes: null },
|
|
48
|
+
{ ...editor('2022.3.40f1', 'data'), modules: [], sizeBytes: null },
|
|
49
|
+
],
|
|
50
|
+
projects: [
|
|
51
|
+
{
|
|
52
|
+
workspace: 'C:\\Workspace\\G',
|
|
53
|
+
version: '6000.0.58f2',
|
|
54
|
+
editor: 'C:\\PcInstalls\\unity\\6000.0.58f2',
|
|
55
|
+
reason: 'the project asks for 6000.0.58f2',
|
|
56
|
+
bridge: null,
|
|
57
|
+
},
|
|
58
|
+
],
|
|
59
|
+
licence: { state: 'active', kind: 'personal', detail: 'Personal', user: null },
|
|
60
|
+
assetCache: { mode: 'persisted', dir: 'C:\\AgentData\\tools\\unity\\asset-store' },
|
|
61
|
+
pcAssets: [],
|
|
62
|
+
hub: { installed: false, path: null },
|
|
63
|
+
mcp: null,
|
|
64
|
+
skills: false,
|
|
65
|
+
problem: null,
|
|
66
|
+
...over,
|
|
67
|
+
};
|
|
68
|
+
}
|
|
1
69
|
export const unitySuite = {
|
|
2
70
|
name: 'unity',
|
|
3
|
-
title: 'Unity: editors,
|
|
4
|
-
run: (t) => {
|
|
5
|
-
t.
|
|
71
|
+
title: 'Unity: editors, the release API, the CLI, the editor bridge, the guide and the licence file',
|
|
72
|
+
run: async (t) => {
|
|
73
|
+
t.section('ProjectVersion.txt');
|
|
74
|
+
t.equal(parseProjectVersion('m_EditorVersion: 6000.0.58f2\nm_EditorVersionWithRevision: 6000.0.58f2 (92dee566b325)\n'), { version: '6000.0.58f2', changeset: '92dee566b325' }, 'version and changeset');
|
|
75
|
+
t.equal(parseProjectVersion('m_EditorVersion: 2022.3.40f1\n'), { version: '2022.3.40f1', changeset: null }, 'without a revision');
|
|
76
|
+
t.equal(wantedVersion('m_EditorVersion: 2022.3.40f1\n', '6000.0.58f2'), { version: '2022.3.40f1', changeset: null }, "a project's own version wins over the setting");
|
|
77
|
+
t.equal(wantedVersion(null, '6000.0.58f2'), { version: '6000.0.58f2', changeset: null }, 'the setting is for folders without one');
|
|
78
|
+
t.equal(wantedVersion(null, 'project'), null, '"project" and no file: nothing');
|
|
79
|
+
t.equal(parseProjectVersion('m_EditorVersion: 2022.3.40f1\r\nm_EditorVersionWithRevision: 2022.3.40f1 (0123456789ab)\r\n'), { version: '2022.3.40f1', changeset: '0123456789ab' }, 'CRLF');
|
|
80
|
+
t.equal(parseProjectVersion('hello\nm_EditorVersion: banana\n'), null, 'garbage');
|
|
81
|
+
t.equal(parseProjectVersion('x'.repeat(70 * 1024) + '\nm_EditorVersion: 6000.0.58f2\n'), null, 'past 64 KB');
|
|
82
|
+
t.equal(parseProjectVersion('\n'.repeat(60) + 'm_EditorVersion: 6000.0.58f2\n'), null, 'past 50 lines');
|
|
83
|
+
t.section('versions');
|
|
84
|
+
t.check(compareUnityVersions('2022.3.9f1', '2022.3.10f1') < 0, 'patch numbers compare as numbers');
|
|
85
|
+
t.check(compareUnityVersions('6000.0.1f1', '2022.3.40f1') > 0, 'newer major is newer');
|
|
86
|
+
t.check(compareUnityVersions('6000.0.1b1', '6000.0.1f1') < 0, 'beta before final');
|
|
87
|
+
t.check(compareUnityVersions('6000.0.1f1', '6000.0.1f1') === 0, 'equal');
|
|
88
|
+
t.check(supportsPipeline('6000.0.58f2') && !supportsPipeline('2022.3.40f1'), 'the pipeline needs Unity 6');
|
|
89
|
+
t.section('chooseEditor');
|
|
90
|
+
const pc = editor('6000.0.58f2', 'pc', ['android']);
|
|
91
|
+
const data = editor('6000.0.58f2', 'data', ['android', 'webgl']);
|
|
92
|
+
const other = editor('2022.3.40f1', 'data');
|
|
93
|
+
const want = { version: '6000.0.58f2', changeset: '92dee566b325' };
|
|
94
|
+
t.equal(chooseEditor([pc, data], null, [], true).reason, 'no ProjectSettings/ProjectVersion.txt', 'no version file');
|
|
95
|
+
const a = chooseEditor([pc, data], want, ['android'], true);
|
|
96
|
+
t.equal([a.editor?.origin, a.reason], ['pc', 'the project asks for 6000.0.58f2'], 'the PC copy first');
|
|
97
|
+
const b = chooseEditor([pc, data], want, ['android', 'webgl'], true);
|
|
98
|
+
t.equal([b.editor?.origin, b.reason], ['data', '6000.0.58f2 from this PC lacks webgl; using the data disk copy'], 'the PC copy lacks a module');
|
|
99
|
+
const c = chooseEditor([other], want, [], true);
|
|
100
|
+
t.equal([c.editor, c.install, c.reason], [null, want, '6000.0.58f2 is not here yet; installing it on the data disk'], 'install it, never another version');
|
|
101
|
+
const d = chooseEditor([other], want, [], false);
|
|
102
|
+
t.equal([d.editor, d.install, d.reason], [
|
|
103
|
+
null,
|
|
104
|
+
null,
|
|
105
|
+
'6000.0.58f2 is not on this PC or the data disk; accept the Unity licence (Unity set-up settings) to install it',
|
|
106
|
+
], 'no licence accepted');
|
|
107
|
+
t.equal(chooseEditor([pc], want, ['webgl'], false).editor?.origin, 'pc', 'a PC copy missing modules beats none');
|
|
108
|
+
t.section('editorModules');
|
|
109
|
+
const root = t.dir('editor');
|
|
110
|
+
const engines = ['Editor', 'Data', 'PlaybackEngines'];
|
|
111
|
+
t.dir('editor', ...engines, 'AndroidPlayer');
|
|
112
|
+
t.dir('editor', ...engines, 'windowsstandalonesupport', 'Variations', 'win64_development_il2cpp');
|
|
113
|
+
t.equal(editorModules(root, { exists: (f) => fs.existsSync(f), list: (d) => fs.readdirSync(d) }), ['android', 'windows-il2cpp'], 'modules found by their folders');
|
|
114
|
+
t.section('release API');
|
|
115
|
+
t.includes(releaseUrl('6000.0.58f2'), 'version=6000.0.58f2&platform=WINDOWS', 'release url');
|
|
116
|
+
t.includes(releaseUrl('a&b'), 'version=a%26b', 'release url encodes');
|
|
117
|
+
const rel = pickRelease(RELEASE);
|
|
118
|
+
t.equal(rel?.changeset, '92dee566b325', 'changeset');
|
|
119
|
+
t.equal(rel?.editor.integrity, 'md5-AAAAAAAAAAAAAAAAAAAAAA==', 'md5 integrity kept');
|
|
120
|
+
t.equal(rel?.modules[0]?.children, ['android-sdk-ndk-tools', 'android-open-jdk'], 'android children');
|
|
121
|
+
t.equal(pickRelease({ results: [] }), null, 'no result');
|
|
122
|
+
t.equal(pickRelease('nope'), null, 'not an object');
|
|
123
|
+
if (rel) {
|
|
124
|
+
t.equal(installNeedBytes(rel, []), Math.ceil(12_000_000_000 * 1.1), 'editor only');
|
|
125
|
+
t.equal(installNeedBytes(rel, ['android']), Math.ceil((12_000_000_000 + 100 + 200 + 11 + 22) * 1.1), 'module and children');
|
|
126
|
+
}
|
|
127
|
+
t.section('Unity CLI');
|
|
128
|
+
t.equal(cliManifestUrl('latest'), 'https://public-cdn.cloud.unity3d.com/hub/prod/cli/latest-beta.json', 'latest');
|
|
129
|
+
t.equal(cliManifestUrl('1.0.0-beta.16'), 'https://public-cdn.cloud.unity3d.com/hub/prod/cli/1.0.0-beta.16/latest.json', 'pinned');
|
|
130
|
+
await t.throws(() => cliManifestUrl('../x'), 'not a Unity CLI version', 'a bad version');
|
|
131
|
+
const manifest = (filename, sha256) => CliManifestSchema.parse({ version: '1.0.0-beta.16', binaries: { 'win32-x64': { filename, sha256, size: 44 } } });
|
|
132
|
+
t.equal(pickCliBinary(manifest('unity-windows-x64.exe', SHA), 'x64'), {
|
|
133
|
+
version: '1.0.0-beta.16',
|
|
134
|
+
url: 'https://public-cdn.cloud.unity3d.com/hub/prod/cli/1.0.0-beta.16/unity-windows-x64.exe',
|
|
135
|
+
sha256: SHA,
|
|
136
|
+
size: 44,
|
|
137
|
+
}, 'the x64 build');
|
|
138
|
+
t.equal(pickCliBinary(manifest('..\\evil.exe', SHA), 'x64'), null, 'a bad file name');
|
|
139
|
+
t.equal(pickCliBinary(manifest('unity-windows-x64.exe', 'abc'), 'x64'), null, 'a bad sha');
|
|
140
|
+
t.equal(pickCliBinary(manifest('unity-windows-x64.exe', SHA), 'arm64'), null, 'no build for this CPU');
|
|
141
|
+
t.equal(installArgs('6000.0.58f2', '92dee566b325', ['android'], 'C:\\S'), ['install', '6000.0.58f2', '-c', '92dee566b325', '-m', 'android', '--cm', '--download-directory', 'C:\\S'], 'install args');
|
|
142
|
+
t.equal(installArgs('6000.0.58f2', null, [], 'C:\\S'), ['install', '6000.0.58f2', '--download-directory', 'C:\\S'], 'install args, bare');
|
|
143
|
+
t.equal(installModulesArgs('6000.0.58f2', ['webgl', 'ios'], 'C:\\S'), ['install-modules', '-e', '6000.0.58f2', '-m', 'webgl', 'ios', '--cm', '--download-directory', 'C:\\S'], 'install-modules args');
|
|
144
|
+
t.equal(installPathArgs('C:\\E'), ['install-path', '--set', 'C:\\E'], 'install-path args');
|
|
145
|
+
t.equal(editorsAddArgs(['C:\\A', 'C:\\B']), ['editors', 'add', 'C:\\A', 'C:\\B'], 'editors add args');
|
|
146
|
+
t.equal(licenceStatusArgs(), ['license', 'status', '--format', 'json'], 'licence status args');
|
|
147
|
+
t.section('MCP');
|
|
148
|
+
t.equal(unityCliMcpServer('C:\\u\\unity.exe', 'C:\\Workspace\\G'), {
|
|
149
|
+
type: 'stdio',
|
|
150
|
+
command: 'C:\\u\\unity.exe',
|
|
151
|
+
args: ['mcp', '--cli-tools'],
|
|
152
|
+
env: { ...UNITY_CLI_ENV, UNITY_PROJECT_PATH: 'C:\\Workspace\\G' },
|
|
153
|
+
}, 'with a project');
|
|
154
|
+
t.equal(unityCliMcpServer('u', null).env, { ...UNITY_CLI_ENV }, 'without a project');
|
|
155
|
+
t.section('Packages/manifest.json');
|
|
156
|
+
const manifestText = JSON.stringify({ dependencies: { 'com.a': '1.0.0', 'com.b': '2.0.0' }, scopedRegistries: [] }, null, 2) + '\n';
|
|
157
|
+
for (const bridge of ['pipeline', 'coplay']) {
|
|
158
|
+
const on = withBridge(manifestText, bridge, true);
|
|
159
|
+
t.equal(bridgeOf(on), bridge, bridge + ' added');
|
|
160
|
+
t.equal(Object.keys(JSON.parse(on).dependencies).slice(0, 2), ['com.a', 'com.b'], bridge + ': order kept');
|
|
161
|
+
t.equal(withBridge(on, bridge, false), manifestText, bridge + ': removed again, byte for byte');
|
|
162
|
+
}
|
|
163
|
+
t.equal(JSON.parse(withBridge(manifestText, 'pipeline', true, '0.11.0')).dependencies[PIPELINE_PACKAGE], '0.11.0', 'pipeline version');
|
|
164
|
+
t.equal(JSON.parse(withBridge(manifestText, 'coplay', true)).dependencies[COPLAY_PACKAGE], COPLAY_PACKAGE_URL, 'coplay url');
|
|
165
|
+
const crlf = withBridge(manifestText.replace(/\n/g, '\r\n'), 'pipeline', true);
|
|
166
|
+
t.check(crlf.includes('\r\n') && !/[^\r]\n/.test(crlf), 'CRLF kept');
|
|
167
|
+
t.equal(bridgeOf('not json'), null, 'garbage has no bridge');
|
|
168
|
+
t.equal(bridgeOf(manifestText), null, 'no bridge');
|
|
169
|
+
await t.throws(() => withBridge('[]', 'pipeline', true), 'not an object', 'refuses an array');
|
|
170
|
+
await t.throws(() => withBridge('{"a":"' + 'x'.repeat(1024 * 1024) + '"}', 'pipeline', true), 'over 1 MB', 'refuses a big file');
|
|
171
|
+
await t.throws(() => withBridge(manifestText, 'pipeline', true, '1.0; rm'), 'not a package version', 'bad version');
|
|
172
|
+
t.section('Asset Store cache');
|
|
173
|
+
const dirs = new Set(['C:\\Env\\Asset Store-5.x', 'C:\\Users\\u\\AppData\\Roaming\\Unity\\Asset Store-5.x']);
|
|
174
|
+
const isDir = (d) => dirs.has(d);
|
|
175
|
+
t.equal(pcAssetCacheDirs({ appData: 'C:\\Users\\u\\AppData\\Roaming', envValue: 'C:\\Env' }, isDir), ['C:\\Env', 'C:\\Users\\u\\AppData\\Roaming\\Unity'], 'the environment first');
|
|
176
|
+
t.equal(pcAssetCacheDirs({ appData: 'C:\\Users\\u\\AppData\\Roaming' }, isDir), ['C:\\Users\\u\\AppData\\Roaming\\Unity'], 'the default');
|
|
177
|
+
t.equal(pcAssetCacheDirs({ appData: 'C:\\X' }, isDir), [], 'none');
|
|
178
|
+
const tree = {
|
|
179
|
+
R: ['Pub', 'file.txt'],
|
|
180
|
+
'R\\Pub': ['Cat'],
|
|
181
|
+
'R\\Pub\\Cat': ['a.unitypackage', 'b.txt'],
|
|
182
|
+
};
|
|
183
|
+
const assetFs = {
|
|
184
|
+
list: (d) => tree[d] ?? [],
|
|
185
|
+
isDir: (d) => d in tree,
|
|
186
|
+
size: (f) => (f.endsWith('.unitypackage') ? 7 : null),
|
|
187
|
+
};
|
|
188
|
+
t.equal(assetPackages('R', assetFs), [{ publisher: 'Pub', category: 'Cat', name: 'a', sizeBytes: 7 }], 'packages');
|
|
189
|
+
const many = {
|
|
190
|
+
R: ['P'],
|
|
191
|
+
'R\\P': ['C'],
|
|
192
|
+
'R\\P\\C': Array.from({ length: 2500 }, (_, i) => 'p' + i + '.unitypackage'),
|
|
193
|
+
};
|
|
194
|
+
t.equal(assetPackages('R', { ...assetFs, list: (d) => many[d] ?? [], isDir: (d) => d in many }).length, 2000, 'at most 2000');
|
|
195
|
+
t.section('status');
|
|
196
|
+
const good = status();
|
|
197
|
+
t.check(UnityStatusSchema.safeParse(good).success, 'a status parses');
|
|
198
|
+
t.check(!UnityStatusSchema.safeParse({ ...good, editors: Array.from({ length: 33 }, () => good.editors[0]) }).success, '33 editors are refused');
|
|
199
|
+
t.section('guide');
|
|
200
|
+
const lines = (s) => unityGuideLines(s, [{ workspace: 'C:\\Workspace\\G' }]).join('\n');
|
|
201
|
+
const exe = '"C:\\PcInstalls\\unity\\6000.0.58f2\\Editor\\Unity.exe"';
|
|
202
|
+
t.equal(unityGuideLines(good, [{ workspace: 'C:\\Workspace\\G' }])[0], '- Unity: editor 6000.0.58f2 at `C:\\PcInstalls\\unity\\6000.0.58f2\\Editor\\Unity.exe` (' +
|
|
203
|
+
ORIGIN_LABELS.pc +
|
|
204
|
+
'; the project asks for 6000.0.58f2). Build or run code without opening it: `' +
|
|
205
|
+
exe +
|
|
206
|
+
' -batchmode -nographics -quit -projectPath "C:\\Workspace\\G" -executeMethod <Class.Method> -logFile -`; tests: `-runTests -testPlatform EditMode -testResults <file>` (without -quit). Opening the project in another Unity version upgrades its files: never do that without asking the user. It is the user\'s PC copy, read-only: never add modules to it.', 'PC read-only line');
|
|
207
|
+
const onData = lines(status({
|
|
208
|
+
projects: [{ ...good.projects[0], editor: 'C:\\AgentData\\tools\\unity\\editors\\2022.3.40f1' }],
|
|
209
|
+
}));
|
|
210
|
+
t.includes(onData, '(' + ORIGIN_LABELS.data + ';', 'data origin label');
|
|
211
|
+
t.excludes(onData, 'read-only: never add modules', 'a data editor has no read-only note');
|
|
212
|
+
t.includes(lines(good), '- `unity` (Unity CLI 1.0.0-beta.16) is on PATH: `unity editors -i`, `unity build`, `unity test`, `unity open <project>` (opens it signed in).', 'CLI line');
|
|
213
|
+
t.includes(lines(good), "- To drive a running editor (scenes, GameObjects, play mode) the project needs an editor bridge package: ask the user to add it on the sandbox tool's Unity page. Never change Packages/manifest.json yourself unless asked.", 'no bridge');
|
|
214
|
+
const bridged = (bridge) => lines(status({ projects: [{ ...good.projects[0], bridge }] }));
|
|
215
|
+
t.includes(bridged('pipeline'), '- The open editor can be driven with `unity command` and `unity eval` (Unity Pipeline package); MCP clients use sbx-unity.', 'pipeline');
|
|
216
|
+
t.includes(bridged('coplay'), '- The open editor can be driven with MCP for Unity (sbx-unity-mcp in Apps).', 'coplay');
|
|
217
|
+
t.excludes(bridged('pipeline'), 'needs an editor bridge', 'no bridge line when there is one');
|
|
218
|
+
t.includes(lines(status({ pcAssets: ['C:\\PcInstalls\\unity\\AssetStore'] })), "- The user's Unity Asset Store downloads (read-only): `C:\\PcInstalls\\unity\\AssetStore`; import one with the editor closed:", 'assets');
|
|
219
|
+
t.includes(lines(status({ licence: { state: 'none', kind: '', detail: 'no licence', user: null } })), "- Unity licence: no licence. Batch mode fails without one: ask the user to sign in to Unity on the sandbox tool's Unity page.", 'no licence');
|
|
220
|
+
t.equal(unityGuideLines(good, [{ workspace: 'C:\\Elsewhere' }]), [], 'nothing for a project Unity has no word on');
|
|
221
|
+
t.section('licence files');
|
|
222
|
+
t.equal(unityUlfFile('C:\\ProgramData'), 'C:\\ProgramData\\Unity\\Unity_lic.ulf', 'ulf path');
|
|
223
|
+
t.equal(unityEntitlementFile('C:\\Users\\u\\AppData\\Local'), 'C:\\Users\\u\\AppData\\Local\\Unity\\licenses\\UnityEntitlementLicense.xml', 'entitlement path');
|
|
224
|
+
const ulf = '<?xml version="1.0" encoding="UTF-8"?>\r\n<root>\r\n <License id="Terms">\r\n <SerialMasked Value="U3-XXXX-XXXX-XXXX-XXXX-XXXX" />\r\n <UpdateDate Value="2026-10-01T10:00:00" />\r\n </License>\r\n</root>';
|
|
225
|
+
t.equal(ulfSummary(ulf), { serialMasked: 'U3-XXXX-XXXX-XXXX-XXXX-XXXX', updated: '2026-10-01T10:00:00', kind: 'personal' }, 'personal ulf');
|
|
226
|
+
t.equal(ulfSummary(ulf.replace('U3-', 'F4-'))?.kind, 'paid', 'paid ulf');
|
|
227
|
+
t.equal(ulfSummary('hello'), null, 'not a ulf');
|
|
228
|
+
t.equal(unitySummary({ email: 'me@x.com', password: 'hunter2hunter2' }), 'me@x.com', 'the email');
|
|
229
|
+
t.excludes(unitySummary({ email: 'me@x.com', password: 'hunter2hunter2', serial: 'SECRET' }), 'hunter2', 'no password in the summary');
|
|
230
|
+
t.equal(unitySummary({ ulf }), 'licence file (Personal)', 'a personal file');
|
|
231
|
+
t.equal(unitySummary({ ulf: ulf.replace('U3-', 'F4-') }), 'licence file (Plus or Pro)', 'a paid file');
|
|
232
|
+
t.check(!UnityPayloadSchema.safeParse({}).success, 'an empty sign-in is refused');
|
|
233
|
+
t.check(UnityPayloadSchema.safeParse({ entitlement: '<x/>' }).success, 'an entitlement alone is a sign-in');
|
|
234
|
+
t.equal(parseLicenceStatus({ weird: true }).state, 'unknown', 'an unknown shape is unknown');
|
|
235
|
+
t.equal(parseLicenceStatus('text').state, 'unknown', 'text is unknown');
|
|
236
|
+
t.equal(parseLicenceStatus(null).state, 'unknown', 'null is unknown');
|
|
237
|
+
t.equal(parseLicenceStatus({ licenses: [] }).state, 'none', 'no licences');
|
|
238
|
+
t.equal(parseLicenceStatus({ status: 'active', type: 'Personal' }).state, 'active', 'active');
|
|
239
|
+
t.section('readiness');
|
|
240
|
+
t.check(isToolchainReady('unity') && isKindReady('unity'), 'Unity is offered');
|
|
6
241
|
},
|
|
7
242
|
};
|
|
@@ -3,6 +3,7 @@ export * from './payloads.js';
|
|
|
3
3
|
export { fingerprint } from './fingerprint.js';
|
|
4
4
|
export { epicSettingsFile, epicSettingsFiles, MAX_EPIC_INI_BYTES, readEpicLogin, withEpicLogin, type EpicPayload, } from './kinds/epic.js';
|
|
5
5
|
export * from './kinds/steam.js';
|
|
6
|
+
export * from './kinds/unity.js';
|
|
6
7
|
export { PERPLEXITY_ENV, perplexityKeyFromText, perplexitySummary, type PerplexityPayload, } from './kinds/perplexity.js';
|
|
7
8
|
export { cometPayloadFromCookies, cometSummary, cookiesToRestore, type CometPayload } from './kinds/comet.js';
|
|
8
9
|
export * from './anthropic.js';
|
|
@@ -5,6 +5,7 @@ export * from './payloads.js';
|
|
|
5
5
|
export { fingerprint } from './fingerprint.js';
|
|
6
6
|
export { epicSettingsFile, epicSettingsFiles, MAX_EPIC_INI_BYTES, readEpicLogin, withEpicLogin, } from './kinds/epic.js';
|
|
7
7
|
export * from './kinds/steam.js';
|
|
8
|
+
export * from './kinds/unity.js';
|
|
8
9
|
export { PERPLEXITY_ENV, perplexityKeyFromText, perplexitySummary, } from './kinds/perplexity.js';
|
|
9
10
|
export { cometPayloadFromCookies, cometSummary, cookiesToRestore } from './kinds/comet.js';
|
|
10
11
|
export * from './anthropic.js';
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import type { Payload } from '../payloads.js';
|
|
2
|
+
export type UnityPayload = Payload<'unity'>;
|
|
3
|
+
/** The licence files' size limit (the payload's). */
|
|
4
|
+
export declare const MAX_ULF_BYTES: number;
|
|
5
|
+
/** %PROGRAMDATA%\Unity\Unity_lic.ulf */
|
|
6
|
+
export declare function unityUlfFile(programData: string): string;
|
|
7
|
+
/** %LOCALAPPDATA%\Unity\licenses\UnityEntitlementLicense.xml */
|
|
8
|
+
export declare function unityEntitlementFile(localAppData: string): string;
|
|
9
|
+
export interface UlfSummary {
|
|
10
|
+
/** The serial as Unity masks it in the file (XXXX-...), never a full one. */
|
|
11
|
+
readonly serialMasked: string | null;
|
|
12
|
+
readonly updated: string | null;
|
|
13
|
+
readonly kind: 'personal' | 'paid' | 'unknown';
|
|
14
|
+
}
|
|
15
|
+
/** What a Unity_lic.ulf says (its SerialMasked and UpdateDate), or null when it holds neither. */
|
|
16
|
+
export declare function ulfSummary(text: string): UlfSummary | null;
|
|
17
|
+
/** One line for the status: the Unity ID, else what kind of licence file it is. Never a secret. */
|
|
18
|
+
export declare function unitySummary(p: UnityPayload): string;
|
|
19
|
+
export interface LicenceStatus {
|
|
20
|
+
readonly state: 'active' | 'none' | 'unknown';
|
|
21
|
+
readonly kind: string;
|
|
22
|
+
readonly detail: string;
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* What `unity license status --format json` printed. The shape is not documented (checked against the docs, not a live
|
|
26
|
+
* run), so anything unrecognised is 'unknown', never a guess.
|
|
27
|
+
*/
|
|
28
|
+
export declare function parseLicenceStatus(json: unknown): LicenceStatus;
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
// Unity's licence files and what they say, without secrets: the serial licence (Unity_lic.ulf), the named-user licence
|
|
2
|
+
// (UnityEntitlementLicense.xml) and the answer of `unity license status`. Pure text handling, every scan bounded.
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
/** The licence files' size limit (the payload's). */
|
|
5
|
+
export const MAX_ULF_BYTES = 128 * 1024;
|
|
6
|
+
const MAX_LINES = 4000;
|
|
7
|
+
const MAX_FIELD = 200;
|
|
8
|
+
/** %PROGRAMDATA%\Unity\Unity_lic.ulf */
|
|
9
|
+
export function unityUlfFile(programData) {
|
|
10
|
+
return path.win32.join(programData, 'Unity', 'Unity_lic.ulf');
|
|
11
|
+
}
|
|
12
|
+
/** %LOCALAPPDATA%\Unity\licenses\UnityEntitlementLicense.xml */
|
|
13
|
+
export function unityEntitlementFile(localAppData) {
|
|
14
|
+
return path.win32.join(localAppData, 'Unity', 'licenses', 'UnityEntitlementLicense.xml');
|
|
15
|
+
}
|
|
16
|
+
/** The value of `<Name Value="..."/>` on one line, by index scan (no backtracking). */
|
|
17
|
+
function valueOf(line, name) {
|
|
18
|
+
const at = line.indexOf('<' + name);
|
|
19
|
+
if (at < 0)
|
|
20
|
+
return null;
|
|
21
|
+
const key = line.indexOf('Value="', at);
|
|
22
|
+
if (key < 0)
|
|
23
|
+
return null;
|
|
24
|
+
const from = key + 7;
|
|
25
|
+
const end = line.indexOf('"', from);
|
|
26
|
+
if (end < 0 || end - from > MAX_FIELD)
|
|
27
|
+
return null;
|
|
28
|
+
return line.slice(from, end);
|
|
29
|
+
}
|
|
30
|
+
/** What a Unity_lic.ulf says (its SerialMasked and UpdateDate), or null when it holds neither. */
|
|
31
|
+
export function ulfSummary(text) {
|
|
32
|
+
let serialMasked = null;
|
|
33
|
+
let updated = null;
|
|
34
|
+
const lines = text.slice(0, MAX_ULF_BYTES).split('\n', MAX_LINES);
|
|
35
|
+
for (const line of lines) {
|
|
36
|
+
serialMasked ??= valueOf(line, 'SerialMasked');
|
|
37
|
+
updated ??= valueOf(line, 'UpdateDate');
|
|
38
|
+
if (serialMasked !== null && updated !== null)
|
|
39
|
+
break;
|
|
40
|
+
}
|
|
41
|
+
if (serialMasked === null && updated === null)
|
|
42
|
+
return null;
|
|
43
|
+
const kind = serialMasked === null ? 'unknown' : serialMasked.startsWith('U3-') ? 'personal' : 'paid';
|
|
44
|
+
return { serialMasked, updated, kind };
|
|
45
|
+
}
|
|
46
|
+
/** One line for the status: the Unity ID, else what kind of licence file it is. Never a secret. */
|
|
47
|
+
export function unitySummary(p) {
|
|
48
|
+
if (p.email)
|
|
49
|
+
return p.email;
|
|
50
|
+
if (p.ulf) {
|
|
51
|
+
const kind = ulfSummary(p.ulf)?.kind;
|
|
52
|
+
return kind === 'personal' ? 'licence file (Personal)' : 'licence file (Plus or Pro)';
|
|
53
|
+
}
|
|
54
|
+
if (p.entitlement)
|
|
55
|
+
return 'licence file (Personal)';
|
|
56
|
+
return 'serial';
|
|
57
|
+
}
|
|
58
|
+
function text(v) {
|
|
59
|
+
return typeof v === 'string' ? v.slice(0, MAX_FIELD) : '';
|
|
60
|
+
}
|
|
61
|
+
/**
|
|
62
|
+
* What `unity license status --format json` printed. The shape is not documented (checked against the docs, not a live
|
|
63
|
+
* run), so anything unrecognised is 'unknown', never a guess.
|
|
64
|
+
*/
|
|
65
|
+
export function parseLicenceStatus(json) {
|
|
66
|
+
const unknown = { state: 'unknown', kind: '', detail: 'not recognised' };
|
|
67
|
+
if (typeof json !== 'object' || json === null || Array.isArray(json))
|
|
68
|
+
return unknown;
|
|
69
|
+
const o = json;
|
|
70
|
+
const licences = Array.isArray(o.licenses)
|
|
71
|
+
? o.licenses
|
|
72
|
+
: Array.isArray(o.licences)
|
|
73
|
+
? o.licences
|
|
74
|
+
: null;
|
|
75
|
+
if (licences !== null) {
|
|
76
|
+
if (!licences.length)
|
|
77
|
+
return { state: 'none', kind: '', detail: 'no licence' };
|
|
78
|
+
const first = licences[0];
|
|
79
|
+
const f = typeof first === 'object' && first !== null ? first : {};
|
|
80
|
+
return { state: 'active', kind: text(f.type ?? f.kind), detail: text(f.detail ?? f.name ?? f.status) };
|
|
81
|
+
}
|
|
82
|
+
const status = text(o.status ?? o.state).toLowerCase();
|
|
83
|
+
if (status === 'active' || status === 'activated' || status === 'valid')
|
|
84
|
+
return { state: 'active', kind: text(o.type ?? o.kind), detail: text(o.detail ?? o.message) || status };
|
|
85
|
+
if (status === 'none' || status === 'inactive' || status === 'not activated' || status === 'unlicensed')
|
|
86
|
+
return { state: 'none', kind: '', detail: text(o.detail ?? o.message) || status };
|
|
87
|
+
return unknown;
|
|
88
|
+
}
|