windows-development-sandbox 1.25.1 → 1.27.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (196) hide show
  1. package/config.example.json +12 -3
  2. package/dist/app/features/accounts/services.def.d.ts +1 -1
  3. package/dist/app/features/accounts/services.def.js +1 -1
  4. package/dist/app/features/defs.d.ts +198 -3
  5. package/dist/app/features/defs.js +8 -0
  6. package/dist/app/features/settings/categories.def.js +1 -0
  7. package/dist/app/features/unity/index.d.ts +2 -0
  8. package/dist/app/features/unity/index.js +3 -0
  9. package/dist/app/features/unity/unity.def.d.ts +294 -0
  10. package/dist/app/features/unity/unity.def.js +471 -0
  11. package/dist/app/features/unity/unity.run.d.ts +142 -0
  12. package/dist/app/features/unity/unity.run.js +363 -0
  13. package/dist/app/registry.js +6 -0
  14. package/dist/bin/agent-sandbox.js +2 -2
  15. package/dist/bin/{chunk-F4PVBN23.js → chunk-6WWMBCT2.js} +3580 -1962
  16. package/dist/bin/{chunk-3GAXLATK.js → chunk-JXL6ZOO3.js} +72 -5
  17. package/dist/bin/{chunk-G3TG4ZT7.js → chunk-ZXCAVVS4.js} +1 -1
  18. package/dist/bin/claude-host.js +2 -2
  19. package/dist/bin/claude-sbx.js +2 -2
  20. package/dist/bin/codex-sbx.js +2 -2
  21. package/dist/bin/gemini-sbx.js +2 -2
  22. package/dist/bin/sandbox.js +2 -2
  23. package/dist/bin/sbx-hide.js +2 -2
  24. package/dist/bin/sbx-show.js +2 -2
  25. package/dist/cli/commands/launch/launch.js +12 -0
  26. package/dist/cli/commands/registry.js +3 -0
  27. package/dist/cli/manifest.d.ts +33 -1
  28. package/dist/cli/manifest.js +32 -1
  29. package/dist/cli/menu/coverage-exempt.js +1 -0
  30. package/dist/cli/menu/pages/sandbox-page.d.ts +3 -0
  31. package/dist/cli/menu/pages/sandbox-page.js +40 -3
  32. package/dist/cli/menu/views.js +2 -0
  33. package/dist/core/config/defaults.js +16 -3
  34. package/dist/core/schema/accounts.d.ts +3 -0
  35. package/dist/core/schema/accounts.js +1 -0
  36. package/dist/core/schema/config.d.ts +29 -13
  37. package/dist/core/schema/github.d.ts +1 -0
  38. package/dist/core/schema/github.js +3 -0
  39. package/dist/core/schema/pc-installs.d.ts +2 -1
  40. package/dist/core/schema/pc-installs.js +6 -1
  41. package/dist/core/schema/toolchains.d.ts +8 -0
  42. package/dist/core/schema/toolchains.js +19 -4
  43. package/dist/guest/accounts/adapters.js +2 -0
  44. package/dist/guest/accounts/apply.d.ts +4 -2
  45. package/dist/guest/accounts/kinds/perplexityapp.d.ts +7 -0
  46. package/dist/guest/accounts/kinds/perplexityapp.js +53 -0
  47. package/dist/guest/accounts/kinds/unity.d.ts +27 -0
  48. package/dist/guest/accounts/kinds/unity.js +164 -7
  49. package/dist/guest/apps/app-installer.js +18 -0
  50. package/dist/guest/apps/store-app.d.ts +4 -0
  51. package/dist/guest/apps/store-app.js +19 -0
  52. package/dist/guest/apps/store-check.d.ts +8 -0
  53. package/dist/guest/apps/store-check.js +33 -0
  54. package/dist/guest/boot/boot.js +1 -1
  55. package/dist/guest/boot/guide.js +18 -0
  56. package/dist/guest/browser/comet.d.ts +3 -1
  57. package/dist/guest/browser/comet.js +1 -1
  58. package/dist/guest/browser/perplexity-app.d.ts +12 -0
  59. package/dist/guest/browser/perplexity-app.js +60 -0
  60. package/dist/guest/daemon/daemon.js +2 -0
  61. package/dist/guest/devtools/devtool.d.ts +1 -1
  62. package/dist/guest/devtools/registry.js +1 -2
  63. package/dist/guest/entries/tool.js +1 -1
  64. package/dist/guest/setup/addon-mcp.d.ts +7 -2
  65. package/dist/guest/setup/addon-mcp.js +19 -9
  66. package/dist/guest/setup/github-token.js +4 -4
  67. package/dist/guest/setup/unity-agent.d.ts +11 -0
  68. package/dist/guest/setup/unity-agent.js +36 -0
  69. package/dist/guest/toolchains/engines/unity-cli.d.ts +24 -0
  70. package/dist/guest/toolchains/engines/unity-cli.js +74 -0
  71. package/dist/guest/toolchains/engines/unity-hub.d.ts +2 -0
  72. package/dist/guest/toolchains/engines/unity-hub.js +27 -0
  73. package/dist/guest/toolchains/engines/unity-release.d.ts +4 -15
  74. package/dist/guest/toolchains/engines/unity-release.js +17 -5
  75. package/dist/guest/toolchains/engines/unity-status.d.ts +11 -0
  76. package/dist/guest/toolchains/engines/unity-status.js +39 -0
  77. package/dist/guest/toolchains/engines/unity.d.ts +20 -1
  78. package/dist/guest/toolchains/engines/unity.js +250 -3
  79. package/dist/guest/toolchains/plan.js +5 -1
  80. package/dist/guest/toolchains/tools/cli.js +2 -2
  81. package/dist/host/accounts/deliver.js +4 -0
  82. package/dist/host/accounts/kinds/perplexityapp.d.ts +2 -0
  83. package/dist/host/accounts/kinds/perplexityapp.js +8 -0
  84. package/dist/host/accounts/kinds/unity.d.ts +1 -1
  85. package/dist/host/accounts/kinds/unity.js +60 -8
  86. package/dist/host/accounts/leases.d.ts +2 -1
  87. package/dist/host/accounts/sources.js +2 -0
  88. package/dist/host/apps/index.d.ts +1 -0
  89. package/dist/host/apps/index.js +2 -0
  90. package/dist/host/apps/store-package.d.ts +51 -0
  91. package/dist/host/apps/store-package.js +209 -0
  92. package/dist/host/github/deliver.d.ts +8 -3
  93. package/dist/host/github/deliver.js +42 -29
  94. package/dist/host/github/index.d.ts +1 -0
  95. package/dist/host/github/index.js +1 -0
  96. package/dist/host/github/pc-login.d.ts +27 -0
  97. package/dist/host/github/pc-login.js +129 -0
  98. package/dist/host/pc-installs/discover.d.ts +4 -0
  99. package/dist/host/pc-installs/discover.js +16 -4
  100. package/dist/host/pc-installs/source.d.ts +1 -1
  101. package/dist/host/pc-installs/unity-assets.d.ts +5 -0
  102. package/dist/host/pc-installs/unity-assets.js +18 -0
  103. package/dist/host/pc-installs/unity.d.ts +20 -0
  104. package/dist/host/pc-installs/unity.js +135 -2
  105. package/dist/runtime/agent.js +116 -11
  106. package/dist/runtime/daemon.js +2170 -976
  107. package/dist/runtime/db.js +116 -11
  108. package/dist/runtime/main.js +2543 -1364
  109. package/dist/runtime/netproxy.js +116 -11
  110. package/dist/runtime/tool.js +133 -31
  111. package/dist/runtime/view.js +116 -11
  112. package/dist/selftest/agents-web.test.js +2 -1
  113. package/dist/selftest/engines.test.js +2 -2
  114. package/dist/selftest/github.test.js +64 -1
  115. package/dist/selftest/index.js +10 -0
  116. package/dist/selftest/menu-screens.js +3 -1
  117. package/dist/selftest/persisted-installs.test.js +1 -1
  118. package/dist/selftest/service-accounts.test.js +1 -1
  119. package/dist/selftest/store-app.test.d.ts +2 -0
  120. package/dist/selftest/store-app.test.js +162 -0
  121. package/dist/selftest/support/engine-golden.js +7 -0
  122. package/dist/selftest/support/fixtures.d.ts +10 -1
  123. package/dist/selftest/support/unity-sample.d.ts +2 -0
  124. package/dist/selftest/support/unity-sample.js +80 -0
  125. package/dist/selftest/unity-accounts.test.d.ts +2 -0
  126. package/dist/selftest/unity-accounts.test.js +106 -0
  127. package/dist/selftest/unity-agent.test.d.ts +2 -0
  128. package/dist/selftest/unity-agent.test.js +47 -0
  129. package/dist/selftest/unity-page.test.d.ts +2 -0
  130. package/dist/selftest/unity-page.test.js +177 -0
  131. package/dist/selftest/unity-setup.test.d.ts +2 -0
  132. package/dist/selftest/unity-setup.test.js +49 -0
  133. package/dist/selftest/unity.test.js +238 -3
  134. package/dist/shared/accounts/index.d.ts +1 -0
  135. package/dist/shared/accounts/index.js +1 -0
  136. package/dist/shared/accounts/kinds/unity.d.ts +28 -0
  137. package/dist/shared/accounts/kinds/unity.js +88 -0
  138. package/dist/shared/accounts/kinds.d.ts +2 -2
  139. package/dist/shared/accounts/kinds.js +23 -1
  140. package/dist/shared/accounts/payloads.d.ts +52 -0
  141. package/dist/shared/accounts/payloads.js +14 -2
  142. package/dist/shared/apps/catalogue-agents-web.d.ts +7 -0
  143. package/dist/shared/apps/catalogue-agents-web.js +23 -0
  144. package/dist/shared/apps/catalogue-gamedev.d.ts +3 -0
  145. package/dist/shared/apps/catalogue-gamedev.js +31 -0
  146. package/dist/shared/apps/catalogue.d.ts +8 -0
  147. package/dist/shared/apps/index.d.ts +3 -2
  148. package/dist/shared/apps/index.js +3 -2
  149. package/dist/shared/apps/selection.d.ts +6 -0
  150. package/dist/shared/apps/selection.js +9 -0
  151. package/dist/shared/apps/store-package.d.ts +31 -0
  152. package/dist/shared/apps/store-package.js +34 -0
  153. package/dist/shared/channel/schema.d.ts +24 -0
  154. package/dist/shared/channel/secrets.d.ts +17 -0
  155. package/dist/shared/paths/pc-installs.d.ts +1 -1
  156. package/dist/shared/paths/pc-installs.js +1 -0
  157. package/dist/shared/readiness.d.ts +2 -2
  158. package/dist/shared/readiness.js +2 -2
  159. package/dist/shared/unity/asset-cache.d.ts +20 -0
  160. package/dist/shared/unity/asset-cache.js +44 -0
  161. package/dist/shared/unity/cli.d.ts +40 -0
  162. package/dist/shared/unity/cli.js +77 -0
  163. package/dist/shared/unity/editors.d.ts +46 -0
  164. package/dist/shared/unity/editors.js +89 -0
  165. package/dist/shared/unity/guide.d.ts +9 -0
  166. package/dist/shared/unity/guide.js +84 -0
  167. package/dist/shared/unity/index.d.ts +9 -0
  168. package/dist/shared/unity/index.js +11 -0
  169. package/dist/shared/unity/manifest.d.ts +10 -0
  170. package/dist/shared/unity/manifest.js +53 -0
  171. package/dist/shared/unity/mcp.d.ts +8 -0
  172. package/dist/shared/unity/mcp.js +12 -0
  173. package/dist/shared/unity/release.d.ts +28 -0
  174. package/dist/shared/unity/release.js +83 -0
  175. package/dist/shared/unity/status.d.ts +59 -0
  176. package/dist/shared/unity/status.js +45 -0
  177. package/dist/shared/unity/version.d.ts +27 -0
  178. package/dist/shared/unity/version.js +75 -0
  179. package/dist/web/index.html +1 -1
  180. package/dist/web/{main-FNE6XEHA.js → main-VPMYTSUL.js} +67 -67
  181. package/native/csharp/HcsHost/AssemblyInfo.cs +3 -3
  182. package/native/csharp/HostTools/AssemblyInfo.cs +3 -3
  183. package/native/csharp/StorageHelper/AssemblyInfo.cs +3 -3
  184. package/native/scripts/get-store-package.ps1 +58 -0
  185. package/package.json +1 -1
  186. package/sandbox/addons/openupm-cli/package-lock.json +2921 -0
  187. package/sandbox/addons/openupm-cli/package.json +7 -0
  188. package/sandbox/addons/unity-mcp/requirements.in +1 -0
  189. package/sandbox/addons/unity-mcp/requirements.txt +1165 -0
  190. package/sandbox/csharp/AppActivation.cs +38 -0
  191. package/sandbox/scripts/install-store-package.ps1 +28 -0
  192. package/sandbox/scripts/start-store-app.ps1 +29 -0
  193. package/schemas/config.schema.json +61 -6
  194. package/schemas/sandbox-config.schema.json +38 -5
  195. package/dist/guest/devtools/unity.d.ts +0 -2
  196. package/dist/guest/devtools/unity.js +0 -7
@@ -0,0 +1,39 @@
1
+ // tools\unity\status.json: what the sandbox found for Unity, for the guide and the Unity page on the PC. The boot's
2
+ // project step writes its editor choice, the licence step the licence, the daemon the editors; every write keeps what
3
+ // the other parts wrote.
4
+ import path from 'node:path';
5
+ import { readValid, writeJsonAtomic } from '../../../shared/io/json.js';
6
+ import { UNITY_STATUS_FILE, UnityStatusSchema } from '../../../shared/unity/index.js';
7
+ import { mkdirp } from '../../runtime/fs.js';
8
+ import { tool } from '../tool-env.js';
9
+ export const UNITY_DIR = tool('unity');
10
+ /** The Unity CLI, one folder per version (unity.exe). */
11
+ export const CLI_DIR = path.join(UNITY_DIR, 'cli');
12
+ /** The editors on the data disk: one folder per version (Editor\Unity.exe). */
13
+ export const EDITORS_DIR = path.join(UNITY_DIR, 'editors');
14
+ /** Asset Store downloads made in the sandbox (assetCache "persisted"). */
15
+ export const ASSET_DIR = path.join(UNITY_DIR, 'asset-store');
16
+ const FILE = path.join(UNITY_DIR, UNITY_STATUS_FILE);
17
+ const EMPTY = {
18
+ updatedAt: '',
19
+ cli: null,
20
+ editors: [],
21
+ projects: [],
22
+ licence: { state: 'unknown', kind: '', detail: 'not checked', user: null },
23
+ assetCache: { mode: 'local', dir: null },
24
+ pcAssets: [],
25
+ hub: { installed: false, path: null },
26
+ mcp: null,
27
+ skills: false,
28
+ problem: null,
29
+ };
30
+ export function readUnityStatus() {
31
+ return readValid(UnityStatusSchema, FILE) ?? EMPTY;
32
+ }
33
+ /** Changes the status with `change` and writes it (whole, atomically). */
34
+ export function updateUnityStatus(change) {
35
+ const next = { ...change(readUnityStatus()), updatedAt: new Date().toISOString() };
36
+ mkdirp(UNITY_DIR);
37
+ writeJsonAtomic(FILE, next);
38
+ return next;
39
+ }
@@ -1,4 +1,23 @@
1
+ import { type UnityEditor, type WantedEditor } from '../../../shared/unity/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
- export declare function setupUnity(env: ToolEnv, _config: Toolchains['unity'], _ctx: ToolCtx): Promise<void>;
5
+ import { type UnityCli } from './unity-cli.js';
6
+ /** Every editor this sandbox has. */
7
+ export declare function sandboxEditors(ctx: Pick<ToolCtx, 'pcInstalls'>): UnityEditor[];
8
+ /**
9
+ * The editor a project asks for: its ProjectVersion.txt (read bounded) always wins, since opening a project in another
10
+ * version upgrades its files; the version the setting names is only for folders without one.
11
+ */
12
+ export declare function wantedEditor(config: Pick<Toolchains['unity'], 'version'>, workspace: string): WantedEditor | null;
13
+ /** Whether the project's editor is already here (a first install goes to the background). */
14
+ export declare function unityEditorSaved(config: Toolchains['unity'], ctx: Pick<ToolCtx, 'pcInstalls'>): boolean;
15
+ /**
16
+ * Installs one editor version on the data disk with the CLI (into a staging folder with TEMP inside it) and returns its
17
+ * folder. One heavy slot while the CLI runs.
18
+ */
19
+ export declare function ensureUnityEditor(cli: UnityCli, version: string, changeset: string | null, modules: readonly string[], keepVersions: number): Promise<string>;
20
+ /** While the sandbox runs: lists the editors again (the user may have installed one with Hub) for the status. */
21
+ export declare function captureUnityEditors(): Promise<void>;
22
+ /** The editors, the project's choice, the CLI, the licence, the asset cache: set up and written to the status. */
23
+ export declare function setupUnity(env: ToolEnv, config: Toolchains['unity'], ctx: ToolCtx): Promise<void>;
@@ -1,4 +1,251 @@
1
- import { notAvailableYet } from '../not-yet.js';
2
- export async function setupUnity(env, _config, _ctx) {
3
- notAvailableYet(env, 'Unity');
1
+ // Unity (toolchains.unity), for each project: the Unity CLI on PATH (pinned, checked), every editor the sandbox has
2
+ // (this PC's, read-only, and the data disk's) registered with it, the editor the project's ProjectVersion.txt names
3
+ // (installed on the data disk when nothing has that exact version), UNITY_EDITOR, the Asset Store cache, the licence,
4
+ // Unity Hub when asked and the agent's MCP server and skills. Machine-wide writes only add; a project that joins
5
+ // later only adds its own editor choice. A project is never opened or changed here.
6
+ import fs from 'node:fs';
7
+ import path from 'node:path';
8
+ import { readBoundedFile } from '../../../shared/io/bounded.js';
9
+ import { chooseEditor, editorsAddArgs, findEditors, installArgs, installModulesArgs, installNeedBytes, installPathArgs, unitySummaryLine, wantedVersion, } from '../../../shared/unity/index.js';
10
+ import { errMsg } from '../../../core/util/errors.js';
11
+ import { activateUnityLicence } from '../../accounts/kinds/unity.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 { heavy } from '../../runtime/pools.js';
16
+ import { runScriptWithJsonAsync } from '../../runtime/process.js';
17
+ import { projectWorkspace } from '../../runtime/project-context.js';
18
+ import { session } from '../../runtime/session.js';
19
+ import { setupUnityAgentTools } from '../../setup/unity-agent.js';
20
+ import { ensurePersisted, installRecords } from '../persisted.js';
21
+ import { ensureUnityCli, runUnityCli, runUnityCliChecked } from './unity-cli.js';
22
+ import { ensureUnityHub } from './unity-hub.js';
23
+ import { findUnityRelease } from './unity-release.js';
24
+ import { ASSET_DIR, EDITORS_DIR, readUnityStatus, updateUnityStatus } from './unity-status.js';
25
+ const MAX_PROJECT_VERSION_BYTES = 64 * 1024;
26
+ const MARKER = 'Editor\\Unity.exe';
27
+ const TERMS = 'https://unity.com/legal/terms-of-service';
28
+ const FALLBACK_NEED_BYTES = 15 * 1024 ** 3;
29
+ /** The real disk, for the editor search. */
30
+ const DISK = {
31
+ exists: (file) => exists(file),
32
+ list: (dir) => {
33
+ try {
34
+ return fs.readdirSync(dir).slice(0, 500);
35
+ }
36
+ catch {
37
+ return []; // Not a folder: nothing in it.
38
+ }
39
+ },
40
+ };
41
+ /** This PC's editors the host mapped (session.json's pcInstalls; the id is the version). */
42
+ function mappedEditors(ctx) {
43
+ return ctx.pcInstalls.items.filter((i) => i.kind === 'unity').map((i) => ({ guest: i.guest, id: i.id }));
44
+ }
45
+ /** Every editor this sandbox has. */
46
+ export function sandboxEditors(ctx) {
47
+ return findEditors({ mapped: mappedEditors(ctx), dataRoot: EDITORS_DIR }, DISK);
48
+ }
49
+ /**
50
+ * The editor a project asks for: its ProjectVersion.txt (read bounded) always wins, since opening a project in another
51
+ * version upgrades its files; the version the setting names is only for folders without one.
52
+ */
53
+ export function wantedEditor(config, workspace) {
54
+ const file = path.join(workspace, 'ProjectSettings', 'ProjectVersion.txt');
55
+ const bytes = readBoundedFile(file, MAX_PROJECT_VERSION_BYTES);
56
+ return wantedVersion(bytes ? bytes.toString('utf8') : null, config.version);
57
+ }
58
+ /** Whether the project's editor is already here (a first install goes to the background). */
59
+ export function unityEditorSaved(config, ctx) {
60
+ const wanted = wantedEditor(config, projectWorkspace());
61
+ if (!wanted)
62
+ return true;
63
+ return sandboxEditors(ctx).some((e) => e.version === wanted.version);
64
+ }
65
+ function sizeOf(e) {
66
+ if (e.origin !== 'data')
67
+ return null;
68
+ return installRecords().find((r) => r.name === 'unity/editors' && r.version === e.version)?.sizeBytes ?? null;
69
+ }
70
+ const statusEditor = (e) => ({
71
+ version: e.version,
72
+ root: e.root,
73
+ origin: e.origin,
74
+ writable: e.writable,
75
+ modules: [...e.modules],
76
+ sizeBytes: sizeOf(e),
77
+ });
78
+ /**
79
+ * Installs one editor version on the data disk with the CLI (into a staging folder with TEMP inside it) and returns its
80
+ * folder. One heavy slot while the CLI runs.
81
+ */
82
+ export async function ensureUnityEditor(cli, version, changeset, modules, keepVersions) {
83
+ const release = await findUnityRelease(version);
84
+ const cs = changeset ?? release?.changeset ?? null;
85
+ const need = release ? installNeedBytes(release, modules) : FALLBACK_NEED_BYTES;
86
+ const installed = await ensurePersisted({ name: 'unity/editors', version, needBytes: need, marker: MARKER, what: 'Unity ' + version, keepVersions }, async (staging, temp) => {
87
+ log('Installing Unity ' + version + ' on the data disk (several GB; this takes a while) ...');
88
+ await heavy.run(() => runUnityCliChecked(cli.exe, installArgs(version, cs, modules, staging), { temp }));
89
+ });
90
+ return installed.dir;
91
+ }
92
+ /** Adds modules to an editor on the data disk. */
93
+ async function addModules(cli, editor, modules) {
94
+ const temp = path.join(editor.root, '.temp');
95
+ mkdirp(temp);
96
+ try {
97
+ log('Adding to Unity ' + editor.version + ': ' + modules.join(', ') + ' ...');
98
+ const args = installModulesArgs(editor.version, modules, editor.root);
99
+ await heavy.run(() => runUnityCliChecked(cli.exe, args, { temp }));
100
+ }
101
+ finally {
102
+ fs.rmSync(temp, { recursive: true, force: true });
103
+ }
104
+ }
105
+ /** While the sandbox runs: lists the editors again (the user may have installed one with Hub) for the status. */
106
+ export function captureUnityEditors() {
107
+ const unity = session.toolchains.unity;
108
+ if (dryRun || unity.enabled !== true || !exists(EDITORS_DIR))
109
+ return Promise.resolve();
110
+ const editors = sandboxEditors({ pcInstalls: session.pcInstalls ?? { items: [] } });
111
+ const describe = (list) => list.map((e) => e.root + '|' + e.modules.join(',')).join(';');
112
+ if (describe(editors) !== describe(readUnityStatus().editors))
113
+ updateUnityStatus((s) => ({ ...s, editors: editors.slice(0, 32).map(statusEditor) }));
114
+ return Promise.resolve();
115
+ }
116
+ /** The editors, the project's choice, the CLI, the licence, the asset cache: set up and written to the status. */
117
+ export async function setupUnity(env, config, ctx) {
118
+ if (!config.acceptLicense)
119
+ throw new Error('Unity is set up only with toolchains.unity.acceptLicense = true: you accept the Unity terms of service (' +
120
+ TERMS +
121
+ ') and the editor needs a Unity licence (Personal is free).');
122
+ const workspace = projectWorkspace();
123
+ if (dryRun) {
124
+ log('(dry run) would set up Unity ' + config.version + ' for ' + workspace);
125
+ return;
126
+ }
127
+ const cli = await ensureUnityCli(config.cliVersion);
128
+ env.prepend.push(path.dirname(cli.exe));
129
+ let problem = null;
130
+ const quiet = (what) => (e) => warn('Unity CLI: ' + what + ' (' + errMsg(e) + ')');
131
+ // Settings that only keep the CLI quiet: a failure is not worth stopping for.
132
+ await runUnityCli(cli.exe, ['config', 'update-check', 'off'], { timeoutMs: 60_000 }).catch(quiet('update-check'));
133
+ await runUnityCli(cli.exe, ['analytics', 'opt-out'], { timeoutMs: 60_000 }).catch(quiet('analytics'));
134
+ mkdirp(EDITORS_DIR);
135
+ await runUnityCli(cli.exe, installPathArgs(EDITORS_DIR), { timeoutMs: 60_000 }).catch(quiet('install-path'));
136
+ let editors = sandboxEditors(ctx);
137
+ if (editors.length)
138
+ await runUnityCli(cli.exe, editorsAddArgs(editors.map((e) => e.root)), { timeoutMs: 120_000 }).catch(quiet('editors add'));
139
+ const wanted = wantedEditor(config, workspace);
140
+ let choice = chooseEditor(editors, wanted, config.modules, true);
141
+ const install = choice.install;
142
+ if (install) {
143
+ try {
144
+ const { version, changeset } = install;
145
+ await ensureUnityEditor(cli, version, changeset, config.modules, config.keepVersions);
146
+ editors = sandboxEditors(ctx);
147
+ choice = chooseEditor(editors, wanted, config.modules, true);
148
+ }
149
+ catch (e) {
150
+ problem = 'Unity ' + install.version + ' was not installed: ' + errMsg(e);
151
+ warn(problem);
152
+ }
153
+ }
154
+ let editor = choice.editor;
155
+ if (editor) {
156
+ const have = editor.modules;
157
+ const lack = config.modules.filter((m) => !have.includes(m));
158
+ if (lack.length && editor.writable) {
159
+ try {
160
+ await addModules(cli, editor, lack);
161
+ editors = sandboxEditors(ctx);
162
+ const root = editor.root;
163
+ editor = editors.find((e) => e.root === root) ?? editor;
164
+ }
165
+ catch (e) {
166
+ problem = 'Unity modules were not added: ' + errMsg(e);
167
+ warn(problem);
168
+ }
169
+ }
170
+ else if (lack.length) {
171
+ problem =
172
+ 'Unity ' +
173
+ editor.version +
174
+ ' from this PC has no ' +
175
+ lack[0] +
176
+ " module: add it with Unity Hub on your PC, or remove that editor from This PC's editors to install one here.";
177
+ warn(problem);
178
+ }
179
+ env.vars.UNITY_EDITOR = editor.exe;
180
+ }
181
+ // The Asset Store cache: where downloads made here go (HKCU too, so an editor started from Explorer agrees).
182
+ // For "pc" the mapped Asset Store folder (guest of the unity-assets item) sits inside the cache root.
183
+ const pcAssets = ctx.pcInstalls.items.filter((i) => i.kind === 'unity-assets').map((i) => i.guest);
184
+ let cacheDir = null;
185
+ if (config.assetCache === 'persisted')
186
+ cacheDir = mkdirp(ASSET_DIR);
187
+ else if (config.assetCache === 'pc' && pcAssets[0])
188
+ cacheDir = path.win32.dirname(pcAssets[0]);
189
+ if (cacheDir) {
190
+ env.vars.ASSETSTORE_CACHE_PATH = cacheDir;
191
+ if (!ctx.duringSession)
192
+ await runScriptWithJsonAsync('set-registry-values.ps1', 'PlanFile', {
193
+ values: [{ hive: 'HKCU', key: 'Environment', name: 'ASSETSTORE_CACHE_PATH', value: cacheDir }],
194
+ }).catch((e) => warn('Unity: the Asset Store cache was not set for Explorer (' + errMsg(e) + ')'));
195
+ }
196
+ const licence = await activateUnityLicence({ editorExe: editor?.exe ?? null, unityExe: cli.exe }).catch((e) => {
197
+ warn('Unity licence: ' + errMsg(e));
198
+ return readUnityStatus().licence;
199
+ });
200
+ let hub = readUnityStatus().hub;
201
+ if (config.hub)
202
+ try {
203
+ hub = { installed: true, path: await ensureUnityHub() };
204
+ }
205
+ catch (e) {
206
+ warn('Unity Hub was not installed: ' + errMsg(e));
207
+ }
208
+ const agent = await setupUnityAgentTools({
209
+ unityExe: cli.exe,
210
+ projectPath: workspace,
211
+ mcp: config.mcp,
212
+ skills: config.agentSkills,
213
+ }).catch((e) => {
214
+ warn('Unity agent tools: ' + errMsg(e));
215
+ return { mcp: null, skills: false };
216
+ });
217
+ const chosen = editor;
218
+ const status = updateUnityStatus((s) => {
219
+ const mine = s.projects.find((p) => p.workspace.toLowerCase() === workspace.toLowerCase());
220
+ return {
221
+ ...s,
222
+ cli: { version: cli.version, path: cli.exe },
223
+ editors: editors.slice(0, 32).map(statusEditor),
224
+ projects: [
225
+ ...s.projects.filter((p) => p !== mine),
226
+ {
227
+ workspace,
228
+ version: wanted?.version ?? null,
229
+ editor: chosen?.root ?? null,
230
+ reason: choice.reason,
231
+ bridge: mine?.bridge ?? null,
232
+ },
233
+ ].slice(-64),
234
+ licence,
235
+ assetCache: { mode: config.assetCache, dir: cacheDir },
236
+ pcAssets: pcAssets.slice(0, 4),
237
+ hub,
238
+ mcp: agent.mcp ?? (ctx.duringSession ? s.mcp : null),
239
+ skills: agent.skills || (ctx.duringSession && s.skills),
240
+ problem,
241
+ };
242
+ });
243
+ env.summary.push(unitySummaryLine(status, workspace));
244
+ log('Unity: ' +
245
+ editors.length +
246
+ ' editor(s); ' +
247
+ (editor ? editor.version + ' at ' + editor.root : 'none for this project') +
248
+ ' (' +
249
+ choice.reason +
250
+ ')');
4
251
  }
@@ -26,7 +26,8 @@ import { ensureVisualStudio, prefetchVisualStudio, visualStudioSaved, visualStud
26
26
  import { duringSession } from '../runtime/project-context.js';
27
27
  import { setupSteam } from './steam/steam.js';
28
28
  import { setupGodot } from './engines/godot.js';
29
- import { setupUnity } from './engines/unity.js';
29
+ import { setupUnity, unityEditorSaved } from './engines/unity.js';
30
+ import { prefetchUnityCli } from './engines/unity-cli.js';
30
31
  import { setupFivem } from './fivem/fivem.js';
31
32
  import { setupAndroid } from './mobile/android.js';
32
33
  import { setupFlutter } from './mobile/flutter.js';
@@ -146,6 +147,9 @@ export function plan(config, versions, o) {
146
147
  name: 'Unity',
147
148
  enabled: wanted(config.unity.enabled, 'unity'),
148
149
  after: ['Visual Studio', 'Android SDK'],
150
+ ...ahead(() => prefetchUnityCli(config.unity.cliVersion)),
151
+ // Only the first install of an editor (several GB) goes to the background; a saved one is back in seconds.
152
+ background: deferred && !unityEditorSaved(config.unity, ctx),
149
153
  install: (env) => setupUnity(env, config.unity, ctx),
150
154
  },
151
155
  {
@@ -34,13 +34,13 @@ export function ghExePath() {
34
34
  * gh keeps its token in Windows Credential Manager unless told otherwise, and that is wiped with the sandbox: only a
35
35
  * login made with --insecure-storage (the token in GH_CONFIG_DIR, which persists) is still there next time.
36
36
  */
37
- const GH_NOTE = ' (log in once with: gh auth login --web --insecure-storage; it is kept between sandboxes)';
37
+ const GH_NOTE = ' (signed in on your PC? turn on github.usePcLogin and the login comes from there; otherwise log in once with: gh auth login --web --insecure-storage, which is kept between sandboxes)';
38
38
  /** A saved gh account whose token was in Credential Manager (gone now): say how to log in so that it lasts. */
39
39
  function warnKeyringLogin() {
40
40
  try {
41
41
  const hosts = fs.readFileSync(path.join(ghConfigDir(), 'hosts.yml'), 'utf8');
42
42
  if (/^\s*user:/m.test(hosts) && !/oauth_token:/.test(hosts))
43
- warn('gh: the GitHub login from an earlier sandbox was kept in Windows Credential Manager, which is wiped with each sandbox. Log in again with: gh auth login --web --insecure-storage (then it lasts).');
43
+ warn('gh: the GitHub login from an earlier sandbox was kept in Windows Credential Manager, which is wiped with each sandbox. Log in again with: gh auth login --web --insecure-storage (then it lasts), or sign gh in on your PC and turn on github.usePcLogin.');
44
44
  }
45
45
  catch {
46
46
  // No saved login: nothing to say.
@@ -9,8 +9,12 @@ function howToSave(spec) {
9
9
  return "sign in to the Epic Games Launcher in this sandbox once, or take this PC's: sandbox account import epic";
10
10
  if (spec.kind === 'steam')
11
11
  return 'sign in once in this sandbox with sbx-steam login';
12
+ if (spec.kind === 'unity')
13
+ return 'sign in to Unity once in this sandbox (Unity page: Sign in to Unity), or: sandbox account login unity';
12
14
  if (spec.kind === 'comet')
13
15
  return 'sign in to Comet once in this sandbox, and it is kept for the next ones';
16
+ if (spec.kind === 'perplexityapp')
17
+ return 'sign in to the Perplexity app once in this sandbox, and it is kept for the next ones';
14
18
  if (spec.returns)
15
19
  return 'sign in once in this sandbox, or: sandbox account import ' + spec.kind;
16
20
  return spec.sources.includes('typed')
@@ -0,0 +1,2 @@
1
+ import type { HostSource } from '../source.js';
2
+ export declare const perplexityAppSource: HostSource<'perplexityapp'>;
@@ -0,0 +1,8 @@
1
+ // Perplexity desktop app: nothing to take from this PC. The app is an Electron program in a Microsoft Store package;
2
+ // its cookies are sealed to your Windows account (Chromium's cookie key is protected by DPAPI), so they cannot open in
3
+ // a sandbox, and reading them would be what password stealers do. The sign-in only ever comes from a sandbox you
4
+ // signed in in (the same way as Comet's).
5
+ import { cometSummary } from '../../../shared/accounts/index.js';
6
+ export const perplexityAppSource = {
7
+ summary: (p) => cometSummary(p),
8
+ };
@@ -1,2 +1,2 @@
1
- import { type HostSource } from '../source.js';
1
+ import type { HostSource } from '../source.js';
2
2
  export declare const unitySource: HostSource<'unity'>;
@@ -1,13 +1,65 @@
1
- import { notAvailableYet } from '../source.js';
1
+ // Unity: this PC's licence file (Unity_lic.ulf, or the named-user UnityEntitlementLicense.xml), a licence file you
2
+ // name, or the Unity ID (and serial) typed for a sandbox to sign in with. Unity ties a licence file to the computer, so
3
+ // a sandbox may refuse one taken from this PC; the typed Unity ID is the way that always works.
4
+ import path from 'node:path';
5
+ import { MAX_ULF_BYTES, unityEntitlementFile, unitySummary, unityUlfFile, UnityPayloadSchema, } from '../../../shared/accounts/index.js';
6
+ import { fail } from '../../../core/util/index.js';
7
+ import { decodeText, readBoundedFile } from '../../../shared/io/index.js';
8
+ const NOT_A_LICENCE = 'That is not a Unity licence file (Unity_lic.ulf) or entitlement file.';
9
+ const WHERE = 'Pick the file from C:\\ProgramData\\Unity or %LOCALAPPDATA%\\Unity\\licenses.';
10
+ function readText(file) {
11
+ const bytes = readBoundedFile(file, MAX_ULF_BYTES);
12
+ return bytes ? decodeText(bytes).text : null;
13
+ }
14
+ /** The email, password and serial answers that were typed (trimmed, empty ones left out). */
15
+ function typed(answers) {
16
+ const out = {};
17
+ const email = (answers.email ?? '').trim();
18
+ const password = answers.password ?? '';
19
+ const serial = (answers.serial ?? '').trim();
20
+ if (email)
21
+ out.email = email;
22
+ if (password)
23
+ out.password = password;
24
+ if (serial)
25
+ out.serial = serial;
26
+ return out;
27
+ }
28
+ function checked(candidate) {
29
+ const parsed = UnityPayloadSchema.safeParse(candidate);
30
+ if (!parsed.success)
31
+ fail('That Unity sign-in is not valid (check the email, and that the licence file is not too big).', WHERE);
32
+ return parsed.data;
33
+ }
2
34
  export const unitySource = {
3
- fromPc(_pc, _from) {
4
- return notAvailableYet('unity');
35
+ fromPc(pc, from) {
36
+ const ulf = (pc.programData || from ? readText(unityUlfFile(from ? path.resolve(from) : pc.programData)) : null) ??
37
+ (pc.localAppData
38
+ ? readText(path.win32.join(pc.localAppData, 'VirtualStore', 'ProgramData', 'Unity', 'Unity_lic.ulf'))
39
+ : null);
40
+ const entitlement = pc.localAppData ? readText(unityEntitlementFile(pc.localAppData)) : null;
41
+ if (ulf === null && entitlement === null)
42
+ return null;
43
+ return { ...(ulf !== null ? { ulf } : {}), ...(entitlement !== null ? { entitlement } : {}) };
5
44
  },
6
- fromFile(_bytes, _answers) {
7
- return notAvailableYet('unity');
45
+ fromFile(bytes, answers) {
46
+ const text = decodeText(bytes).text;
47
+ const candidate = { ...typed(answers) };
48
+ if (text.includes('Entitlement'))
49
+ candidate.entitlement = text;
50
+ else if (text.includes('<License'))
51
+ candidate.ulf = text;
52
+ else
53
+ fail(NOT_A_LICENCE, WHERE);
54
+ return checked(candidate);
8
55
  },
9
- fromAnswers(_answers) {
10
- return notAvailableYet('unity');
56
+ fromAnswers(answers) {
57
+ const t = typed(answers);
58
+ if (t.serial && !(t.email && t.password))
59
+ fail('A serial needs your Unity ID email and password.', 'Type them too, or leave the serial empty.');
60
+ if (!t.email || !t.password)
61
+ fail('A Unity sign-in needs your Unity ID email and password.', 'Type both: sandbox account login unity');
62
+ return checked(t);
11
63
  },
12
- summary: () => null,
64
+ summary: unitySummary,
13
65
  };
@@ -2,16 +2,17 @@ import { z } from 'zod';
2
2
  import { type ServiceKind } from '../../shared/accounts/index.js';
3
3
  declare const LeaseSchema: z.ZodObject<{
4
4
  kind: z.ZodEnum<{
5
- unity: "unity";
6
5
  keystore: "keystore";
7
6
  play: "play";
8
7
  expo: "expo";
9
8
  appstore: "appstore";
9
+ unity: "unity";
10
10
  steam: "steam";
11
11
  epic: "epic";
12
12
  cfx: "cfx";
13
13
  perplexity: "perplexity";
14
14
  comet: "comet";
15
+ perplexityapp: "perplexityapp";
15
16
  }>;
16
17
  dataSet: z.ZodString;
17
18
  launchId: z.ZodString;
@@ -4,6 +4,7 @@ import { cometSource } from './kinds/comet.js';
4
4
  import { epicSource } from './kinds/epic.js';
5
5
  import { expoSource } from './kinds/expo.js';
6
6
  import { keystoreSource } from './kinds/keystore.js';
7
+ import { perplexityAppSource } from './kinds/perplexityapp.js';
7
8
  import { perplexitySource } from './kinds/perplexity.js';
8
9
  import { playSource } from './kinds/play.js';
9
10
  import { steamSource } from './kinds/steam.js';
@@ -19,4 +20,5 @@ export const HOST_SOURCES = {
19
20
  appstore: appstoreSource,
20
21
  perplexity: perplexitySource,
21
22
  comet: cometSource,
23
+ perplexityapp: perplexityAppSource,
22
24
  };
@@ -0,0 +1 @@
1
+ export { prepareStorePackages, spawnStoreRunner, stagedPackageDir, StorePackages, STORE_PACKAGE_MAX_AGE_MS, type ScriptRun, type StoreApp, type StoreRunner, type StoreStaging, } from './store-package.js';
@@ -0,0 +1,2 @@
1
+ // Store apps for the sandbox (host side): the package fetched on this PC and staged in the data folder.
2
+ export { prepareStorePackages, spawnStoreRunner, stagedPackageDir, StorePackages, STORE_PACKAGE_MAX_AGE_MS, } from './store-package.js';
@@ -0,0 +1,51 @@
1
+ import { type StorePackageManifest } from '../../shared/apps/index.js';
2
+ /** How long a fetched package is used before a launch looks for a newer one (a failed look keeps the old one). */
3
+ export declare const STORE_PACKAGE_MAX_AGE_MS: number;
4
+ export interface ScriptRun {
5
+ readonly status: number | null;
6
+ readonly stdout: string;
7
+ readonly stderr: string;
8
+ }
9
+ /** Runs a program and collects its output; the self-test passes a fake. */
10
+ export type StoreRunner = (exe: string, args: readonly string[], timeoutMs: number) => Promise<ScriptRun>;
11
+ export declare const spawnStoreRunner: StoreRunner;
12
+ /** What an app needs fetched (catalogue source.store). */
13
+ export interface StoreApp {
14
+ readonly id: string;
15
+ readonly productId: string;
16
+ readonly packageName: string;
17
+ }
18
+ export interface StoreStaging {
19
+ readonly storageRoot: string;
20
+ readonly dataPath: string;
21
+ readonly run?: StoreRunner;
22
+ readonly now?: () => number;
23
+ }
24
+ /** Where the data folder keeps an app's package for the sandbox. */
25
+ export declare function stagedPackageDir(dataPath: string, appId: string): string;
26
+ export declare class StorePackages {
27
+ private readonly o;
28
+ constructor(o: StoreStaging);
29
+ cacheDir(productId: string): string;
30
+ /** The cached package when its files are all there at the size hashed; null otherwise. */
31
+ cached(productId: string): StorePackageManifest | null;
32
+ private fresh;
33
+ /** Fetches the package into the cache with this PC's Store tooling and returns what was kept. */
34
+ fetch(app: StoreApp): Promise<StorePackageManifest>;
35
+ /** Checks what the script fetched into `work`, hashes it and moves it into the cache. */
36
+ private keep;
37
+ /** The package to use: the cache when fresh, else a new fetch (the old one stands when that fails). */
38
+ ensure(app: StoreApp): Promise<{
39
+ manifest: StorePackageManifest;
40
+ note: string;
41
+ }>;
42
+ /** Copies the cached package into the data folder for the sandbox (files already there at the same size stay). */
43
+ stage(app: StoreApp, manifest: StorePackageManifest): string;
44
+ }
45
+ /**
46
+ * For a launch: every Store app chosen gets its package fetched (or kept) and staged. A failure never stops the launch:
47
+ * it is a note, and the sandbox skips that app.
48
+ */
49
+ export declare function prepareStorePackages(apps: readonly StoreApp[], o: StoreStaging): Promise<{
50
+ readonly notes: readonly string[];
51
+ }>;