windows-development-sandbox 1.23.2 → 1.25.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 (85) hide show
  1. package/config.example.json +0 -1
  2. package/dist/bin/agent-sandbox.js +2 -2
  3. package/dist/bin/{chunk-USTRSHIE.js → chunk-IZRGTSQ5.js} +158 -131
  4. package/dist/bin/{chunk-QFSJX2KW.js → chunk-KH4MI2U5.js} +1 -1
  5. package/dist/bin/{chunk-7YPEDAJD.js → chunk-SUYYVIDW.js} +1 -1
  6. package/dist/bin/claude-host.js +2 -2
  7. package/dist/bin/claude-sbx.js +2 -2
  8. package/dist/bin/codex-sbx.js +2 -2
  9. package/dist/bin/gemini-sbx.js +2 -2
  10. package/dist/bin/sandbox.js +2 -2
  11. package/dist/bin/sbx-hide.js +2 -2
  12. package/dist/bin/sbx-show.js +2 -2
  13. package/dist/core/config/defaults.js +0 -1
  14. package/dist/core/detect/toolchains.js +0 -6
  15. package/dist/core/schema/config.d.ts +1 -2
  16. package/dist/core/schema/toolchains.d.ts +1 -2
  17. package/dist/core/schema/toolchains.js +3 -6
  18. package/dist/guest/agents/agent-files.d.ts +11 -7
  19. package/dist/guest/agents/agent-files.js +37 -11
  20. package/dist/guest/agents/local-state.d.ts +2 -1
  21. package/dist/guest/agents/local-state.js +2 -1
  22. package/dist/guest/boot/boot-console.d.ts +28 -0
  23. package/dist/guest/boot/boot-console.js +98 -0
  24. package/dist/guest/boot/boot.d.ts +8 -1
  25. package/dist/guest/boot/boot.js +66 -42
  26. package/dist/guest/boot/graph.d.ts +9 -5
  27. package/dist/guest/boot/graph.js +8 -2
  28. package/dist/guest/boot/progress-view.d.ts +116 -0
  29. package/dist/guest/boot/progress-view.js +456 -0
  30. package/dist/guest/boot/runner.d.ts +20 -10
  31. package/dist/guest/boot/runner.js +39 -47
  32. package/dist/guest/runtime/log-line.d.ts +7 -0
  33. package/dist/guest/runtime/log-line.js +18 -0
  34. package/dist/guest/runtime/log.d.ts +17 -4
  35. package/dist/guest/runtime/log.js +40 -17
  36. package/dist/guest/runtime/process.d.ts +9 -0
  37. package/dist/guest/runtime/process.js +69 -8
  38. package/dist/guest/setup/environment.js +9 -4
  39. package/dist/guest/setup/hooks.d.ts +2 -0
  40. package/dist/guest/setup/hooks.js +4 -0
  41. package/dist/guest/setup/path-plan.d.ts +12 -0
  42. package/dist/guest/setup/path-plan.js +18 -0
  43. package/dist/guest/toolchains/native/unreal.js +4 -6
  44. package/dist/guest/toolchains/native/visual-studio.d.ts +14 -14
  45. package/dist/guest/toolchains/native/visual-studio.js +145 -174
  46. package/dist/guest/toolchains/native/vs-persist.d.ts +189 -0
  47. package/dist/guest/toolchains/native/vs-persist.js +256 -0
  48. package/dist/guest/toolchains/native/vs-request.d.ts +6 -26
  49. package/dist/guest/toolchains/native/vs-request.js +16 -50
  50. package/dist/guest/toolchains/plan.js +11 -14
  51. package/dist/host/migrate/migrations/drop-vs-install-files.d.ts +5 -0
  52. package/dist/host/migrate/migrations/drop-vs-install-files.js +34 -0
  53. package/dist/host/migrate/registry.js +7 -1
  54. package/dist/runtime/agent.js +67 -26
  55. package/dist/runtime/daemon.js +958 -716
  56. package/dist/runtime/db.js +67 -26
  57. package/dist/runtime/main.js +1792 -1038
  58. package/dist/runtime/netproxy.js +3 -6
  59. package/dist/runtime/tool.js +44 -24
  60. package/dist/runtime/view.js +4 -7
  61. package/dist/selftest/agent-paths.test.js +17 -1
  62. package/dist/selftest/agent-settings.test.js +36 -5
  63. package/dist/selftest/boot-console.test.d.ts +2 -0
  64. package/dist/selftest/boot-console.test.js +291 -0
  65. package/dist/selftest/boot-runner.test.js +10 -12
  66. package/dist/selftest/guest-boot.test.js +7 -1
  67. package/dist/selftest/index.js +2 -0
  68. package/dist/selftest/layout.test.js +4 -0
  69. package/dist/selftest/migrate.test.js +21 -0
  70. package/dist/selftest/persisted-installs.test.js +169 -33
  71. package/dist/selftest/support/engine-golden.js +2 -0
  72. package/dist/selftest/support/fixtures.d.ts +1 -2
  73. package/dist/shared/channel/schema.d.ts +3 -6
  74. package/dist/shared/pty/terminal.js +2 -1
  75. package/dist/sync/watcher.js +5 -1
  76. package/native/csharp/HcsHost/AssemblyInfo.cs +3 -3
  77. package/native/csharp/HostTools/AssemblyInfo.cs +3 -3
  78. package/native/csharp/StorageHelper/AssemblyInfo.cs +3 -3
  79. package/package.json +1 -1
  80. package/sandbox/scripts/restore-visual-studio.ps1 +78 -0
  81. package/sandbox/scripts/save-visual-studio.ps1 +106 -0
  82. package/schemas/config.schema.json +3 -7
  83. package/schemas/sandbox-config.schema.json +3 -7
  84. package/dist/guest/toolchains/native/autosdk.d.ts +0 -3
  85. package/dist/guest/toolchains/native/autosdk.js +0 -50
@@ -34,7 +34,13 @@ export const guestBootSuite = {
34
34
  ];
35
35
  const headers = [1, 2, 3, 4, 5, 6, 7, 8, 9].map((n) => boot.out.indexOf('[' + n + '/9] '));
36
36
  t.check(headers.every((at, i) => at >= 0 && (i === 0 || at > headers[i - 1])), 'the boot prints its nine stages, [1/9] to [9/9], in order', headers.join(', '));
37
- t.check(boot.out.includes('Boot ready in ') && boot.out.includes('] Developer tools: done in '), 'the boot says how long it took and how long each task took');
37
+ t.check(/^Ready in \d+\.\ds/m.test(boot.out) && /^\[3\/9\] Developer tools +\d+\.\ds$/m.test(boot.out), 'the boot says how long it took and how long each stage took', boot.out);
38
+ const tools = boot.out.indexOf('\nTools\n');
39
+ t.check(!boot.out.includes(': started') &&
40
+ !/^\s*[A-Z][\w -]+ \| /m.test(boot.out) &&
41
+ !boot.out.includes('\x1b[') &&
42
+ tools > headers[8] &&
43
+ /\n {2}gh {2,}off \(not listed in toolchains\.cli\)\n/.test(boot.out.slice(tools)), 'the dry run prints plain lines: no per-task lines, no prefixes, no colours; the tool list once, after the stages', boot.out);
38
44
  t.equal(steps.filter((s) => !boot.out.includes(s)), [], 'the guest provisions every enabled toolchain');
39
45
  t.check(boot.out.includes('AgentSandbox-Block-Private') &&
40
46
  boot.out.includes('remoteport=1-52,54-65535') &&
@@ -80,6 +80,7 @@ import { uiTextSuite } from './ui-text.test.js';
80
80
  import { menuCoverageSuite } from './menu-coverage.test.js';
81
81
  import { auditSuite } from './audit.test.js';
82
82
  import { bootRunnerSuite } from './boot-runner.test.js';
83
+ import { bootConsoleSuite } from './boot-console.test.js';
83
84
  import { layoutSuite } from './layout.test.js';
84
85
  import { menuSuite } from './menu.test.js';
85
86
  import { menuPerfSuite } from './menu-perf.test.js';
@@ -133,6 +134,7 @@ export const SUITES = [
133
134
  // pure helpers and the command line
134
135
  layoutSuite,
135
136
  bootRunnerSuite,
137
+ bootConsoleSuite,
136
138
  toolchainsSuite,
137
139
  persistedInstallsSuite,
138
140
  steamSuite,
@@ -19,6 +19,8 @@ const PURE_GUEST = [
19
19
  'guest/boot/project-env.js',
20
20
  'guest/boot/runner.js',
21
21
  'guest/boot/graph.js',
22
+ 'guest/boot/progress-view.js',
23
+ 'guest/runtime/log-line.js',
22
24
  'guest/toolchains/split.js',
23
25
  'guest/toolchains/step-scheduler.js',
24
26
  'guest/audit/parse.js',
@@ -30,6 +32,8 @@ const PURE_GUEST = [
30
32
  'guest/governor/decide.js',
31
33
  'guest/governor/core.js',
32
34
  'guest/daemon/live-folders.js',
35
+ 'guest/toolchains/native/vs-request.js',
36
+ 'guest/toolchains/native/vs-persist.js',
33
37
  ];
34
38
  const SIDE_EFFECTS = ['guest/runtime/session.js', 'guest/runtime/log.js'];
35
39
  const readModules = (dist) => {
@@ -159,6 +159,27 @@ export const migrateSuite = {
159
159
  finally {
160
160
  fs.rmSync(machineFile);
161
161
  }
162
+ t.section('drop-vs-install-files');
163
+ const dropVs = MIGRATIONS.find((m) => m.id === '2026-11-drop-vs-install-files');
164
+ if (!dropVs)
165
+ return t.check(false, 'the drop-vs-install-files migration exists');
166
+ const oldVs = t.write(t.at('vs', 'old.json'), '// mine\n' +
167
+ JSON.stringify({
168
+ toolchains: { unreal: { enabled: true, keepInstallFiles: true, visualStudio: 'autosdk', components: ['X'] } },
169
+ }));
170
+ const fullVs = t.write(t.at('vs', 'full.json'), JSON.stringify({ toolchains: { unreal: { visualStudio: 'full' } } }));
171
+ const vsNotes = [];
172
+ const vsWanted = dropVs.applies(ctx(t.at('vs'), oldVs)) && !dropVs.applies(ctx(t.at('vs'), fullVs));
173
+ await dropVs.run(ctx(t.at('vs'), oldVs), (line) => vsNotes.push(line));
174
+ const vsAfter = parse(readText(oldVs));
175
+ t.check(vsWanted &&
176
+ vsAfter.toolchains.unreal.keepInstallFiles === undefined &&
177
+ vsAfter.toolchains.unreal.visualStudio === undefined &&
178
+ vsAfter.toolchains.unreal.enabled === true &&
179
+ readText(oldVs).includes('// mine') &&
180
+ vsNotes.join().includes('toolchains.unreal.keepInstallFiles') &&
181
+ loadConfig(oldVs).config.toolchains.unreal.visualStudio === 'full' &&
182
+ !dropVs.applies(ctx(t.at('vs'), oldVs)), 'the retired Visual Studio settings are removed (a chosen "full" stays), the file loads again, and it is idempotent');
162
183
  t.section('sandbox migrate');
163
184
  const cmdUser = t.dir('cmd-user');
164
185
  const cmdConfig = t.write(t.at('cmd-user', 'config.json'), JSON.stringify({ ...JSON.parse(JSON.stringify(old.defaults)), sandbox: { memoryMb: 4096 } }, null, 2));
@@ -1,12 +1,13 @@
1
1
  // Large installs kept between sandboxes (guest/toolchains/persisted.ts): the staging folder and its TEMP, the marker,
2
2
  // the refusal when the disk is short (an injected statfs), keeping only keepVersions, the records `sandbox data`
3
- // reads, and which Visual Studio the toolchains ask for (vs-request.ts). Temp folders only.
3
+ // reads, which Visual Studio the toolchains ask for (vs-request.ts), and how it is saved on the data disk and put
4
+ // back (vs-persist.ts: folders, marker, instance state, registry). Temp folders only.
4
5
  import fs from 'node:fs';
5
6
  import path from 'node:path';
6
7
  import { DEFAULTS } from '../core/config/index.js';
7
8
  import { assertLocalRoom, ensurePersisted, installRecords, InstallRecordsSchema, LOCAL_RESERVE_BYTES, recordUse, } from '../guest/toolchains/persisted.js';
8
- import { BUILD_TOOLS_COMPONENTS, UNREAL_COMPONENTS, visualStudioWanted, } from '../guest/toolchains/native/vs-request.js';
9
- import { autosdkLayout, layoutNextStep, STAGING_RESERVE_BYTES, unrealUsesAutosdk, vsExitText, } from '../guest/toolchains/native/vs-request.js';
9
+ import { BUILD_TOOLS_COMPONENTS, UNREAL_BUILD_TOOLS_COMPONENTS, UNREAL_COMPONENTS, visualStudioWanted, vsExitText, } from '../guest/toolchains/native/vs-request.js';
10
+ import { markerFrom, newestInstance, persistCovers, rebase, registryPlan, restoreInput, rewriteInstanceState, SaveResultSchema, saveInput, SETUP_CLSID, VsPersistMarkerSchema, } from '../guest/toolchains/native/vs-persist.js';
10
11
  import { freeBytesAt } from '../shared/io/disk.js';
11
12
  import { SIGNERS, signerMatches } from '../shared/signer.js';
12
13
  import { decideToolchains } from '../core/detect/toolchains.js';
@@ -17,7 +18,7 @@ import { collectDataEntries } from '../cli/services/data-collect.js';
17
18
  const GB = 1024 ** 3;
18
19
  export const persistedInstallsSuite = {
19
20
  name: 'persisted-installs',
20
- title: 'large persisted installs: staging, marker, room checks, pruning, records; the Visual Studio request',
21
+ title: 'large persisted installs: staging, marker, room checks, pruning, records; Visual Studio: request, save, restore',
21
22
  run: async (t) => {
22
23
  t.section('installing a version');
23
24
  const tools = t.dir('persisted-tools');
@@ -110,28 +111,172 @@ export const persistedInstallsSuite = {
110
111
  all.for.join() === 'unreal,flutter,unity' &&
111
112
  UNREAL_COMPONENTS.every((c) => all.components.includes(c)) &&
112
113
  all.components.includes('Microsoft.VisualStudio.Component.VC.Llvm.Clang'), 'with Unreal: one Community install serves all of them');
113
- t.section('Unreal on the portable MSVC (autosdk)');
114
- tc.unreal.visualStudio = 'autosdk';
115
- const noUnreal = visualStudioWanted(tc);
116
- t.check(noUnreal?.product === 'buildtools' && noUnreal.for.join() === 'flutter,unity', 'autosdk: Unreal asks for no Visual Studio (the others still get the Build Tools)');
117
- tc.flutter.enabled = false;
118
- tc.unity.enabled = false;
119
- t.equal(visualStudioWanted(tc), null, 'autosdk alone: no Visual Studio at all');
120
- t.check(unrealUsesAutosdk(tc, (s) => s === true) && !unrealUsesAutosdk(DEFAULTS.toolchains), 'autosdk is only when asked');
121
- const pairs = autosdkLayout('T:\\msvc', '14.44.35207', '10.0.26100.0', 'T:\\autosdk');
122
- t.check(pairs.length === 4 &&
123
- pairs[0].to === 'T:\\autosdk\\HostWin64\\Win64\\VS2022\\14.44.35207' &&
124
- pairs[0].from === 'T:\\msvc\\VC\\Tools\\MSVC\\14.44.35207' &&
125
- pairs[1].from === 'T:\\msvc\\Windows Kits\\10\\Include\\10.0.26100.0' &&
126
- pairs[1].to === 'T:\\autosdk\\HostWin64\\Win64\\Windows Kits\\10\\Include\\10.0.26100.0', 'the AutoSDK tree is MSVC under VS2022\\<version> and the SDK under Windows Kits\\10 (copies)');
114
+ t.section('Unreal on the Build Tools');
115
+ tc.unreal.visualStudio = 'buildtools';
116
+ const bt = visualStudioWanted(tc);
117
+ t.check(bt?.product === 'buildtools' &&
118
+ bt.for.join() === 'unreal,flutter,unity' &&
119
+ bt.licence === 'toolchains.unreal.acceptLicense' &&
120
+ UNREAL_BUILD_TOOLS_COMPONENTS.every((c) => bt.components.includes(c)) &&
121
+ BUILD_TOOLS_COMPONENTS.every((c) => bt.components.includes(c)) &&
122
+ bt.components.includes('Microsoft.VisualStudio.Component.VC.Llvm.Clang'), '"buildtools": the Build Tools with what UnrealBuildTool needs, shared with Flutter and Unity');
123
+ t.equal(DEFAULTS.toolchains.unreal.visualStudio, 'full', 'Community is the default (the IDE, its debugger and Unreal integration)');
127
124
  const on = DEFAULTS.toolchains;
128
- const auto = {
129
- ...structuredClone(on),
130
- unreal: { ...on.unreal, visualStudio: 'autosdk', acceptLicense: true },
125
+ const decided = decideToolchains({ ...structuredClone(on), unreal: { ...on.unreal, acceptLicense: true } }, ['Game.uproject'], []);
126
+ t.check(decided.unreal.on && !decided.msvc.on, 'an Unreal project does not turn the portable MSVC on (Visual Studio brings MSVC)');
127
+ t.section('Visual Studio kept on the data disk');
128
+ const places = {
129
+ programFiles: 'C:\\Program Files',
130
+ programFilesX86: 'C:\\Program Files (x86)',
131
+ programData: 'C:\\ProgramData',
131
132
  };
132
- const decided = decideToolchains(auto, ['Game.uproject'], []);
133
- t.check(decided.unreal.on && decided.msvc.on, 'an Unreal project with autosdk turns the portable MSVC on, under its licence');
134
- t.section('the Visual Studio layout');
133
+ const root = 'C:\\AgentData\\tools\\visualstudio\\persist';
134
+ const community = 'C:\\Program Files\\Microsoft Visual Studio\\2022\\Community';
135
+ const instances = 'C:\\ProgramData\\Microsoft\\VisualStudio\\Packages\\_Instances';
136
+ const vsAt = { product: 'community', installationPath: community, instanceId: 'a190d188' };
137
+ const save = saveInput(vsAt, root, places, 'T:\\result.json');
138
+ const saved = (name) => save.copies.find((c) => c.name === name);
139
+ t.check(saved('Community')?.from === community &&
140
+ saved('Community')?.to === root + '\\Community' &&
141
+ saved('WindowsKits10')?.from === 'C:\\Program Files (x86)\\Windows Kits\\10' &&
142
+ saved('instance')?.from === instances + '\\a190d188' &&
143
+ saved('instance')?.to === root + '\\instance' &&
144
+ saved('Setup')?.from === 'C:\\ProgramData\\Microsoft\\VisualStudio\\Setup' &&
145
+ saved('Installer')?.optional === false &&
146
+ saved('ReferenceAssemblies')?.optional === true &&
147
+ save.resultFile === 'T:\\result.json', 'the capture mirrors the install, the Windows SDK, the installer, the Setup API and the instance onto the data disk');
148
+ t.check(save.registry.length === 8 &&
149
+ save.registry.filter((r) => r.view === '32').length === 4 &&
150
+ save.registry.some((r) => r.key.endsWith('NETFXSDK') && r.recurse) &&
151
+ save.registry.some((r) => r.key.endsWith('Installed Roots') && r.names?.join() === 'KitsRoot10'), 'it reads the SDK registry values in both views (NETFXSDK with its subkeys, only KitsRoot10 of the kits roots)');
152
+ await t.throws(() => Promise.resolve(saveInput({ ...vsAt, instanceId: '..\\x' }, root, places, 'r')), 'not a Visual Studio instance ID', 'an instance ID is never a path');
153
+ const captured = [
154
+ {
155
+ view: '64',
156
+ key: 'SOFTWARE\\Microsoft\\Windows Kits\\Installed Roots',
157
+ name: 'KitsRoot10',
158
+ kind: 'string',
159
+ value: 'C:\\Program Files (x86)\\Windows Kits\\10\\',
160
+ },
161
+ {
162
+ view: '32',
163
+ key: 'SOFTWARE\\Microsoft\\Microsoft SDKs\\Windows\\v10.0',
164
+ name: 'ProductVersion',
165
+ kind: 'string',
166
+ value: '10.0.22621',
167
+ },
168
+ {
169
+ view: '32',
170
+ key: 'SOFTWARE\\Microsoft\\Microsoft SDKs\\NETFXSDK\\4.8',
171
+ name: 'KitsInstallationFolder',
172
+ kind: 'string',
173
+ value: 'C:\\Program Files (x86)\\Windows Kits\\NETFXSDK\\4.8\\',
174
+ },
175
+ {
176
+ view: '32',
177
+ key: 'SOFTWARE\\Microsoft\\Microsoft SDKs\\NETFXSDK\\4.8\\WinSDK-NetFx40Tools-x64',
178
+ name: 'InstallationFolder',
179
+ kind: 'string',
180
+ value: 'c:\\program files (x86)\\Microsoft SDKs\\Windows\\v10.0A\\bin\\NETFX 4.8 Tools\\x64\\',
181
+ },
182
+ {
183
+ view: '64',
184
+ key: 'SOFTWARE\\Microsoft\\VisualStudio\\Setup',
185
+ name: 'KeepDownloadedPayloads',
186
+ kind: 'dword',
187
+ value: 0,
188
+ },
189
+ {
190
+ view: '64',
191
+ key: 'SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\Run',
192
+ name: 'planted',
193
+ kind: 'string',
194
+ value: 'C:\\planted.exe',
195
+ },
196
+ ];
197
+ const result = SaveResultSchema.parse({
198
+ copied: [
199
+ 'Community',
200
+ 'WindowsKits10',
201
+ 'NETFXSDK',
202
+ 'MicrosoftSDKs-v10.0A',
203
+ 'ReferenceAssemblies',
204
+ 'Installer',
205
+ ].concat(['Setup', 'instance']),
206
+ registry: captured,
207
+ });
208
+ const marker = VsPersistMarkerSchema.parse(markerFrom({ product: 'community', components: [...UNREAL_COMPONENTS] }, { installationPath: community, instanceId: 'a190d188', version: '17.14.37827.9' }, result, '2026-11-04T00:00:00.000Z'));
209
+ t.check(marker.sdkVersion === '10.0.22621' && marker.instanceId === 'a190d188' && marker.folders.length === 8, 'the marker keeps the instance ID, what was saved and the Windows SDK version from the registry');
210
+ t.check(!VsPersistMarkerSchema.safeParse({ ...marker, instanceId: '..\\..\\Windows' }).success &&
211
+ !VsPersistMarkerSchema.safeParse({ ...marker, folders: ['..'] }).success, 'a marker with a path for its instance ID, or a folder it does not know, is refused');
212
+ const plan = registryPlan(marker, root, places);
213
+ const value = (view, key, name) => plan.find((e) => e.view === view && e.key.toLowerCase() === key.toLowerCase() && e.name === name)?.value;
214
+ const kits = root + '\\WindowsKits10\\';
215
+ const sdk = 'SOFTWARE\\Microsoft\\Microsoft SDKs\\Windows\\v10.0';
216
+ const netfx = 'SOFTWARE\\Microsoft\\Microsoft SDKs\\NETFXSDK\\4.8';
217
+ t.check(['64', '32'].every((v) => value(v, 'SOFTWARE\\Microsoft\\Windows Kits\\Installed Roots', 'KitsRoot10') === kits &&
218
+ value(v, sdk, 'InstallationFolder') === kits &&
219
+ value(v, sdk, 'ProductVersion') === '10.0.22621' &&
220
+ value(v, netfx, 'KitsInstallationFolder') === root + '\\NETFXSDK\\4.8\\' &&
221
+ value(v, netfx + '\\WinSDK-NetFx40Tools-x64', 'InstallationFolder') ===
222
+ root + '\\MicrosoftSDKs-v10.0A\\bin\\NETFX 4.8 Tools\\x64\\' &&
223
+ value(v, 'SOFTWARE\\Microsoft\\VisualStudio\\Setup', 'KeepDownloadedPayloads') === 0), 'both registry views point the Windows SDK and the .NET Framework SDK at the data disk (any case); the rest stays');
224
+ const clsid = 'SOFTWARE\\Classes\\CLSID\\' + SETUP_CLSID + '\\InprocServer32';
225
+ const setupDll = 'Microsoft.VisualStudio.Setup.Configuration.Native.dll';
226
+ t.check(value('64', clsid, '') === 'C:\\ProgramData\\Microsoft\\VisualStudio\\Setup\\x64\\' + setupDll &&
227
+ value('32', clsid, '') === 'C:\\ProgramData\\Microsoft\\VisualStudio\\Setup\\x86\\' + setupDll &&
228
+ value('64', clsid, 'ThreadingModel') === 'Both' &&
229
+ value('32', clsid, 'ThreadingModel') === 'Both', 'the Setup Configuration COM class is registered (x64 and x86), so vswhere and UnrealBuildTool find the instance');
230
+ t.check(!plan.some((e) => /CurrentVersion\\Run/i.test(e.key)), 'a value the capture never reads is never written back (a tampered marker)');
231
+ const back = restoreInput(marker, root, places);
232
+ const copied = (name) => back.copies.find((c) => c.name === name);
233
+ t.check(!copied('Community') &&
234
+ !copied('WindowsKits10') &&
235
+ !copied('NETFXSDK') &&
236
+ !copied('Shared') &&
237
+ copied('Installer')?.to === 'C:\\Program Files (x86)\\Microsoft Visual Studio\\Installer' &&
238
+ copied('Installer')?.mode === 'all' &&
239
+ copied('ReferenceAssemblies')?.mode === 'missing' &&
240
+ copied('instance')?.from === root + '\\instance' &&
241
+ copied('instance')?.to === instances + '\\a190d188' &&
242
+ back.registry.length === plan.length, 'a boot copies back only the small fixed-path folders that were saved (the instance under its ID); the big ones stay');
243
+ t.check(persistCovers(marker, { product: 'community', components: [...UNREAL_COMPONENTS] }) &&
244
+ persistCovers(marker, { product: 'community', components: ['component.unreal'] }) &&
245
+ !persistCovers(marker, {
246
+ product: 'community',
247
+ components: [...UNREAL_COMPONENTS, 'Microsoft.VisualStudio.Component.VC.Llvm.Clang'],
248
+ }) &&
249
+ persistCovers(marker, { product: 'buildtools', components: [...BUILD_TOOLS_COMPONENTS] }) &&
250
+ !persistCovers({ ...marker, product: 'buildtools' }, { product: 'community', components: [] }) &&
251
+ !persistCovers({ ...marker, folders: marker.folders.filter((f) => f !== 'Setup') }, {
252
+ product: 'community',
253
+ components: [],
254
+ }), 'a saved copy serves requests with no more components (Community serves the Build Tools too); one without the Setup API none');
255
+ const state = JSON.stringify({
256
+ installationPath: community,
257
+ resolvedInstallationPath: community.toLowerCase(),
258
+ nested: community + '\\Common7\\IDE',
259
+ longer: community + '2',
260
+ launchParams: { fileName: 'Common7\\IDE\\devenv.exe' },
261
+ enginePath: 'C:\\Program Files (x86)\\Microsoft Visual Studio\\Installer\\resources\\app',
262
+ });
263
+ const moved = JSON.parse(rewriteInstanceState(state, community, root + '\\Community'));
264
+ t.check(moved.installationPath === root + '\\Community' &&
265
+ moved.resolvedInstallationPath === root + '\\Community' &&
266
+ moved.nested === root + '\\Community\\Common7\\IDE' &&
267
+ moved.longer === community + '2' &&
268
+ moved.enginePath === 'C:\\Program Files (x86)\\Microsoft Visual Studio\\Installer\\resources\\app', "the instance's state names the saved folder (JSON-escaped, any case); a longer name that only starts the same stays");
269
+ const buildTools = 'C:\\Program Files (x86)\\Microsoft Visual Studio\\2022\\BuildTools';
270
+ const movedBt = JSON.parse(rewriteInstanceState(JSON.stringify({ installationPath: buildTools }), buildTools + '\\', root + '\\BuildTools'));
271
+ t.check(movedBt.installationPath === root + '\\BuildTools' &&
272
+ rebase('D:\\Other\\x', [{ from: 'C:\\Program Files', to: 'D:\\y' }]) === 'D:\\Other\\x', 'the Build Tools too (a trailing backslash does not matter); a value outside the saved folders is kept');
273
+ t.equal(newestInstance([
274
+ { installationVersion: '17.9.1', id: 'a' },
275
+ { installationVersion: '17.14.37827.9', id: 'b' },
276
+ { installationVersion: '17.14.3', id: 'c' },
277
+ ])?.id, 'b', 'the newest instance is chosen by version (vswhere -latest fails on two instances installed at the same time)');
278
+ t.equal(newestInstance([]), null, 'no instance: none');
279
+ t.section('the Visual Studio installer');
135
280
  t.check(vsExitText(3762504530).includes('crashed') &&
136
281
  vsExitText(-1).includes('exit 4294967295') &&
137
282
  vsExitText(0) === 'installed' &&
@@ -139,15 +284,6 @@ export const persistedInstallsSuite = {
139
284
  vsExitText(8006).includes('running') &&
140
285
  vsExitText(3010).includes('restart') &&
141
286
  vsExitText(5007).includes('refused'), 'installer exit codes are told in words (0xE0434352 is a crash)');
142
- const reserve = LOCAL_RESERVE_BYTES;
143
- const room = (free) => freeBytesAt(t.dir('layout-room'), () => ({ bavail: free, bsize: 1 })) ?? 0;
144
- t.check(layoutNextStep(0, 0, reserve) === 'done' &&
145
- layoutNextStep(3762504530, room(40 * GB), reserve) === 'stage' &&
146
- layoutNextStep(3762504530, room(20 * GB), reserve) === 'skip' &&
147
- layoutNextStep(null, room(40 * GB), reserve) === 'stage', 'a failed layout is built on the sandbox disk only with room for it (8 GB plus the reserve)');
148
- t.check(layoutNextStep(null, room(14 * GB), STAGING_RESERVE_BYTES) === 'stage' &&
149
- layoutNextStep(null, room(9.5 * GB), STAGING_RESERVE_BYTES) === 'stage' &&
150
- layoutNextStep(null, room(9 * GB), STAGING_RESERVE_BYTES) === 'skip', 'before Visual Studio is installed, the ~14 GB free at boot is enough to build the install files (9.5 GB needed)');
151
287
  t.equal(freeBytesAt(path.join(t.dir('layout-room'), 'not', 'there'), () => ({ bavail: 3, bsize: 2 })), 6, 'free room of a folder that does not exist asks its parent');
152
288
  t.equal(freeBytesAt(t.dir('layout-room'), () => {
153
289
  throw new Error('no');
@@ -80,6 +80,8 @@ export const LAUNCH_FILES_GOLDEN = [
80
80
  'sandbox/scripts/link-profile-dirs.ps1',
81
81
  'sandbox/scripts/new-shortcuts.ps1',
82
82
  'sandbox/scripts/repair-wmi-services.ps1',
83
+ 'sandbox/scripts/restore-visual-studio.ps1',
84
+ 'sandbox/scripts/save-visual-studio.ps1',
83
85
  'sandbox/scripts/set-desktop.ps1',
84
86
  'sandbox/scripts/set-dns-servers.ps1',
85
87
  'sandbox/scripts/set-machine-environment.ps1',
@@ -199,8 +199,7 @@ export declare function createFixtures(t: Harness): {
199
199
  enabled: boolean | "auto";
200
200
  acceptLicense: boolean;
201
201
  components: string[];
202
- keepInstallFiles: boolean;
203
- visualStudio: "full" | "autosdk";
202
+ visualStudio: "full" | "buildtools";
204
203
  persistEngines: boolean;
205
204
  derivedDataCache: "persisted" | "local";
206
205
  };
@@ -109,10 +109,9 @@ export declare const ProjectSpecSchema: z.ZodObject<{
109
109
  enabled: z.ZodUnion<readonly [z.ZodBoolean, z.ZodLiteral<"auto">]>;
110
110
  acceptLicense: z.ZodBoolean;
111
111
  components: z.ZodArray<z.ZodString>;
112
- keepInstallFiles: z.ZodBoolean;
113
112
  visualStudio: z.ZodEnum<{
114
113
  full: "full";
115
- autosdk: "autosdk";
114
+ buildtools: "buildtools";
116
115
  }>;
117
116
  persistEngines: z.ZodBoolean;
118
117
  derivedDataCache: z.ZodEnum<{
@@ -346,10 +345,9 @@ export declare const GuestRequestSchema: z.ZodDiscriminatedUnion<[z.ZodObject<{
346
345
  enabled: z.ZodUnion<readonly [z.ZodBoolean, z.ZodLiteral<"auto">]>;
347
346
  acceptLicense: z.ZodBoolean;
348
347
  components: z.ZodArray<z.ZodString>;
349
- keepInstallFiles: z.ZodBoolean;
350
348
  visualStudio: z.ZodEnum<{
351
349
  full: "full";
352
- autosdk: "autosdk";
350
+ buildtools: "buildtools";
353
351
  }>;
354
352
  persistEngines: z.ZodBoolean;
355
353
  derivedDataCache: z.ZodEnum<{
@@ -603,10 +601,9 @@ export declare const GuestRequestSchema: z.ZodDiscriminatedUnion<[z.ZodObject<{
603
601
  enabled: z.ZodUnion<readonly [z.ZodBoolean, z.ZodLiteral<"auto">]>;
604
602
  acceptLicense: z.ZodBoolean;
605
603
  components: z.ZodArray<z.ZodString>;
606
- keepInstallFiles: z.ZodBoolean;
607
604
  visualStudio: z.ZodEnum<{
608
605
  full: "full";
609
- autosdk: "autosdk";
606
+ buildtools: "buildtools";
610
607
  }>;
611
608
  persistEngines: z.ZodBoolean;
612
609
  derivedDataCache: z.ZodEnum<{
@@ -185,7 +185,8 @@ options = {}) {
185
185
  socket.on('close', () => finish({ reason: 'closed' }));
186
186
  socket.on('error', () => finish({ reason: 'closed' }));
187
187
  const [c, r] = size();
188
- stdout.write('\x1b[2J\x1b[3J\x1b[H');
188
+ // A clean screen, but the scrollback stays: the boot's summary (warnings, tools, Ready) is just above the session.
189
+ stdout.write('\x1b[2J\x1b[H');
189
190
  socket.write(helloFrame(token, c, r));
190
191
  if (stdin.isTTY)
191
192
  stdin.setRawMode(true);
@@ -68,7 +68,11 @@ export function watcherRunning(dataPath) {
68
68
  }
69
69
  }
70
70
  /** The ways the sandbox itself ends badly (the PC going down takes the watcher with it, so it never gets here). */
71
- const RESTARTABLE = ['sandboxCrash', 'memory', 'gpu', 'hyperv', 'vanished'];
71
+ /**
72
+ * Ends that start the sandbox again: only those Windows logged. A "vanished" end (busy, no word of leaving, nothing
73
+ * in the log) is also what closing the Windows Sandbox window looks like, so it gets a report but no restart.
74
+ */
75
+ const RESTARTABLE = ['sandboxCrash', 'memory', 'gpu', 'hyperv'];
72
76
  /**
73
77
  * The background watcher of one data folder: while the sandbox runs it syncs history back every minute, guards the
74
78
  * projects' git folders, keeps filtered workspaces in step, forwards ports, marks downloaded files, records the audit
@@ -8,9 +8,9 @@ using System.Runtime.InteropServices;
8
8
  [assembly: AssemblyCompany("windows-development-sandbox (open source)")]
9
9
  [assembly: AssemblyProduct("Windows Development Sandbox")]
10
10
  [assembly: AssemblyCopyright("MIT License, windows-development-sandbox contributors")]
11
- [assembly: AssemblyVersion("1.23.2.0")]
12
- [assembly: AssemblyFileVersion("1.23.2.0")]
13
- [assembly: AssemblyInformationalVersion("1.23.2")]
11
+ [assembly: AssemblyVersion("1.25.0.0")]
12
+ [assembly: AssemblyFileVersion("1.25.0.0")]
13
+ [assembly: AssemblyInformationalVersion("1.25.0")]
14
14
  [assembly: ComVisible(false)]
15
15
  // computecore.dll and computestorage.dll are not KnownDLLs: load them from System32 only, never from the exe's folder.
16
16
  [assembly: DefaultDllImportSearchPaths(DllImportSearchPath.System32)]
@@ -8,7 +8,7 @@ using System.Runtime.InteropServices;
8
8
  [assembly: AssemblyCompany("windows-development-sandbox (open source)")]
9
9
  [assembly: AssemblyProduct("Windows Development Sandbox")]
10
10
  [assembly: AssemblyCopyright("MIT License, windows-development-sandbox contributors")]
11
- [assembly: AssemblyVersion("1.23.2.0")]
12
- [assembly: AssemblyFileVersion("1.23.2.0")]
13
- [assembly: AssemblyInformationalVersion("1.23.2")]
11
+ [assembly: AssemblyVersion("1.25.0.0")]
12
+ [assembly: AssemblyFileVersion("1.25.0.0")]
13
+ [assembly: AssemblyInformationalVersion("1.25.0")]
14
14
  [assembly: ComVisible(false)]
@@ -9,7 +9,7 @@ using System.Runtime.InteropServices;
9
9
  [assembly: AssemblyCompany("windows-development-sandbox (open source)")]
10
10
  [assembly: AssemblyProduct("Windows Development Sandbox")]
11
11
  [assembly: AssemblyCopyright("MIT License, windows-development-sandbox contributors")]
12
- [assembly: AssemblyVersion("1.23.2.0")]
13
- [assembly: AssemblyFileVersion("1.23.2.0")]
14
- [assembly: AssemblyInformationalVersion("1.23.2")]
12
+ [assembly: AssemblyVersion("1.25.0.0")]
13
+ [assembly: AssemblyFileVersion("1.25.0.0")]
14
+ [assembly: AssemblyInformationalVersion("1.25.0")]
15
15
  [assembly: ComVisible(false)]
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "windows-development-sandbox",
3
- "version": "1.23.2",
3
+ "version": "1.25.0",
4
4
  "description": "A disposable Windows Sandbox for development, with or without an AI agent (Claude Code, Codex, Gemini, opencode, ...): the folder you run it from is the workspace, toolchains and databases for your project install themselves, and nothing else on your PC is reachable.",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -0,0 +1,78 @@
1
+ #Requires -Version 5.1
2
+ # Puts a saved Visual Studio back at boot, in seconds (guest/toolchains/native/visual-studio.ts; what goes where is
3
+ # decided in vs-persist.ts): the small folders programs look for at fixed paths are copied back, then the registry
4
+ # values that point at the big folders on the data disk and the Setup Configuration COM class are written. -PlanFile is
5
+ # JSON written by the runtime:
6
+ # copies: [{ name, from, to, mode: 'all' | 'missing' }] 'all': robocopy /E (only what differs is copied);
7
+ # 'missing': each subfolder only when the target has none
8
+ # registry: [{ view: '64'|'32', key, name, kind: 'string'|'expand'|'dword', value }] under HKLM ('' = default)
9
+ # Safe to run again. Exit 0 when everything is in place, 1 when a copy failed, 2 when a registry value could not be set.
10
+ param([Parameter(Mandatory = $true)][string]$PlanFile)
11
+ $ErrorActionPreference = 'Stop'
12
+
13
+ $plan = Get-Content -LiteralPath $PlanFile -Raw -Encoding UTF8 | ConvertFrom-Json
14
+ $robocopy = Join-Path $env:SystemRoot 'System32\Robocopy.exe'
15
+
16
+ function Copy-Folder([string]$From, [string]$To) {
17
+ New-Item -ItemType Directory -Force -Path $To | Out-Null
18
+ & $robocopy $From $To /E /COPY:DAT /XJ /R:1 /W:1 /MT:16 /NP /NFL /NDL /NJH /NJS | Where-Object { $_ -and $_.Trim() } | Out-Host
19
+ return $LASTEXITCODE
20
+ }
21
+
22
+ $copied = 0
23
+ $failed = New-Object System.Collections.ArrayList
24
+ foreach ($copy in @($plan.copies)) {
25
+ if (-not (Test-Path -LiteralPath $copy.from -PathType Container)) {
26
+ [void]$failed.Add($copy.name + ' (' + $copy.from + ' is missing)')
27
+ continue
28
+ }
29
+ if ($copy.mode -eq 'missing') {
30
+ foreach ($child in Get-ChildItem -LiteralPath $copy.from -Directory) {
31
+ $target = Join-Path $copy.to $child.Name
32
+ if (Test-Path -LiteralPath $target) { continue }
33
+ $code = Copy-Folder -From $child.FullName -To $target
34
+ if ($code -ge 8) { [void]$failed.Add($copy.name + '\' + $child.Name + ' (robocopy ' + $code + ')') }
35
+ }
36
+ $copied++
37
+ continue
38
+ }
39
+ $code = Copy-Folder -From $copy.from -To $copy.to
40
+ if ($code -ge 8) { [void]$failed.Add($copy.name + ' (robocopy ' + $code + ')') } else { $copied++ }
41
+ }
42
+ if ($failed.Count) {
43
+ Write-Error ('restore-visual-studio: could not copy ' + ($failed -join ', '))
44
+ exit 1
45
+ }
46
+
47
+ $set = 0
48
+ $bad = New-Object System.Collections.ArrayList
49
+ foreach ($entry in @($plan.registry)) {
50
+ $view = [Microsoft.Win32.RegistryView]::Registry64
51
+ if ($entry.view -eq '32') { $view = [Microsoft.Win32.RegistryView]::Registry32 }
52
+ $base = [Microsoft.Win32.RegistryKey]::OpenBaseKey([Microsoft.Win32.RegistryHive]::LocalMachine, $view)
53
+ try {
54
+ $key = $base.CreateSubKey([string]$entry.key)
55
+ try {
56
+ if ($entry.kind -eq 'dword') {
57
+ $key.SetValue([string]$entry.name, [int]$entry.value, [Microsoft.Win32.RegistryValueKind]::DWord)
58
+ } elseif ($entry.kind -eq 'expand') {
59
+ $key.SetValue([string]$entry.name, [string]$entry.value, [Microsoft.Win32.RegistryValueKind]::ExpandString)
60
+ } else {
61
+ $key.SetValue([string]$entry.name, [string]$entry.value, [Microsoft.Win32.RegistryValueKind]::String)
62
+ }
63
+ $set++
64
+ } finally {
65
+ $key.Close()
66
+ }
67
+ } catch {
68
+ [void]$bad.Add($entry.view + ':' + $entry.key + '\' + $entry.name + ' (' + $_.Exception.Message + ')')
69
+ } finally {
70
+ $base.Close()
71
+ }
72
+ }
73
+ if ($bad.Count) {
74
+ Write-Error ('restore-visual-studio: could not set ' + ($bad -join '; '))
75
+ exit 2
76
+ }
77
+ Write-Output ('Restored Visual Studio: ' + $copied + ' folders in place, ' + $set + ' registry values set')
78
+ exit 0