windows-development-sandbox 1.28.0 → 1.29.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 (92) hide show
  1. package/README.md +5 -4
  2. package/dist/app/features/sandbox/sandbox.run.js +1 -1
  3. package/dist/bin/{chunk-UZ5S3EMB.js → chunk-O4EL32TM.js} +1 -1
  4. package/dist/bin/{chunk-CB44HTTN.js → chunk-QYRRWGFY.js} +1442 -1280
  5. package/dist/bin/sandbox.js +10772 -3
  6. package/dist/cli/commands/launch/launch.js +1 -1
  7. package/dist/cli/commands/launch/running.js +2 -2
  8. package/dist/cli/index.js +1 -1
  9. package/dist/cli/menu/pages/agents-page.js +1 -1
  10. package/dist/cli/services/sandbox-service.js +1 -1
  11. package/dist/core/schema/sandbox.js +1 -1
  12. package/dist/guest/apps/app-installer.js +1 -1
  13. package/dist/guest/apps/store-check.d.ts +2 -0
  14. package/dist/guest/apps/store-check.js +3 -1
  15. package/dist/guest/boot/guide-lines.d.ts +2 -0
  16. package/dist/guest/boot/guide-lines.js +7 -0
  17. package/dist/guest/boot/guide.js +2 -1
  18. package/dist/guest/toolchains/native/unreal.js +3 -22
  19. package/dist/host/accounts/deliver.js +16 -2
  20. package/dist/host/apps/store-package.d.ts +2 -0
  21. package/dist/host/apps/store-package.js +16 -15
  22. package/dist/host/channel/attach.js +1 -1
  23. package/dist/host/channel/channel.js +3 -3
  24. package/dist/host/guard/commands.js +1 -0
  25. package/dist/host/health/path-copies.d.ts +1 -1
  26. package/dist/host/health/path-copies.js +2 -2
  27. package/dist/host/install/index.d.ts +1 -1
  28. package/dist/host/install/index.js +1 -1
  29. package/dist/host/install/shims.d.ts +8 -1
  30. package/dist/host/install/shims.js +39 -1
  31. package/dist/host/launch/generate.js +4 -0
  32. package/dist/host/launch/index.d.ts +1 -0
  33. package/dist/host/launch/index.js +1 -0
  34. package/dist/host/launch/pc-logs.d.ts +6 -0
  35. package/dist/host/launch/pc-logs.js +129 -0
  36. package/dist/host/sandbox/platform.js +1 -1
  37. package/dist/host/sandbox/window.js +1 -1
  38. package/dist/runtime/agent.js +1 -1
  39. package/dist/runtime/daemon.js +72 -90
  40. package/dist/runtime/db.js +1 -1
  41. package/dist/runtime/main.js +35 -57
  42. package/dist/runtime/netproxy.js +1 -1
  43. package/dist/runtime/tool.js +1 -1
  44. package/dist/runtime/view.js +1 -1
  45. package/dist/selftest/cli.test.js +5 -5
  46. package/dist/selftest/config.test.js +1 -1
  47. package/dist/selftest/credentials.test.js +1 -1
  48. package/dist/selftest/engine.test.js +1 -1
  49. package/dist/selftest/generate.test.js +66 -1
  50. package/dist/selftest/hcs.test.js +1 -1
  51. package/dist/selftest/health.test.js +21 -10
  52. package/dist/selftest/heavy-load.test.js +1 -5
  53. package/dist/selftest/host-sim.test.js +13 -14
  54. package/dist/selftest/install.test.js +6 -2
  55. package/dist/selftest/store-app.test.js +62 -4
  56. package/dist/selftest/support/web-preview.js +2 -2
  57. package/dist/shared/accounts/kinds.d.ts +5 -0
  58. package/dist/shared/accounts/kinds.js +8 -0
  59. package/dist/shared/apps/index.d.ts +1 -1
  60. package/dist/shared/apps/index.js +1 -1
  61. package/dist/shared/apps/selection.d.ts +1 -0
  62. package/dist/shared/apps/selection.js +4 -3
  63. package/dist/shared/apps/store-package.d.ts +13 -0
  64. package/dist/shared/apps/store-package.js +24 -0
  65. package/dist/shared/memory/index.d.ts +0 -1
  66. package/dist/shared/memory/index.js +0 -1
  67. package/native/csharp/HcsHost/AssemblyInfo.cs +3 -3
  68. package/native/csharp/HostTools/AssemblyInfo.cs +3 -3
  69. package/native/csharp/HostTools/Window.cs +1 -1
  70. package/native/csharp/StorageHelper/AssemblyInfo.cs +3 -3
  71. package/native/scripts/get-store-package.ps1 +24 -13
  72. package/package.json +4 -14
  73. package/sandbox/scripts/install-store-package.ps1 +11 -4
  74. package/schemas/config.schema.json +1 -1
  75. package/schemas/sandbox-config.schema.json +1 -1
  76. package/dist/bin/agent-sandbox.d.ts +0 -2
  77. package/dist/bin/agent-sandbox.js +0 -9
  78. package/dist/bin/chunk-ZMZI3625.js +0 -10778
  79. package/dist/bin/claude-host.d.ts +0 -2
  80. package/dist/bin/claude-host.js +0 -11
  81. package/dist/bin/claude-sbx.d.ts +0 -2
  82. package/dist/bin/claude-sbx.js +0 -9
  83. package/dist/bin/codex-sbx.d.ts +0 -2
  84. package/dist/bin/codex-sbx.js +0 -9
  85. package/dist/bin/gemini-sbx.d.ts +0 -2
  86. package/dist/bin/gemini-sbx.js +0 -9
  87. package/dist/bin/sbx-hide.d.ts +0 -2
  88. package/dist/bin/sbx-hide.js +0 -9
  89. package/dist/bin/sbx-show.d.ts +0 -2
  90. package/dist/bin/sbx-show.js +0 -9
  91. package/dist/shared/memory/unreal-build.d.ts +0 -2
  92. package/dist/shared/memory/unreal-build.js +0 -21
@@ -417,7 +417,7 @@ class Launcher {
417
417
  console.log(headsUp + '\nFollow along: sandbox logs -f');
418
418
  if (hidden)
419
419
  console.log((await hidden)
420
- ? 'Sandbox running headless. Open the window: sbx-show Attach here: sandbox claude -t'
420
+ ? 'Sandbox running headless. Open the window: sandbox show Attach here: sandbox claude -t'
421
421
  : 'Could not find the sandbox window to hide (it may still be starting); run sandbox hide in a moment.');
422
422
  else
423
423
  console.log('Sandbox starting. ' +
@@ -114,7 +114,7 @@ export async function openSession(ctx, ready, joined, o) {
114
114
  session.id +
115
115
  ' opened in a new tab inside the sandbox. Attach here with: sandbox attach ' +
116
116
  session.id +
117
- (ready.headless ? ' (headless: sbx-show opens the window)' : ''));
117
+ (ready.headless ? ' (headless: sandbox show opens the window)' : ''));
118
118
  return 0;
119
119
  }
120
120
  /** The sessions a launch asks for: each --session, else one (the agent, in this folder). */
@@ -169,7 +169,7 @@ async function openSessions(ctx, ready, list) {
169
169
  if (!first)
170
170
  return 1;
171
171
  console.log('Each has its own tab inside the sandbox' +
172
- (ready.headless ? ' (headless: sbx-show opens the window)' : '') +
172
+ (ready.headless ? ' (headless: sandbox show opens the window)' : '') +
173
173
  '. Attach to any of them, from any terminal: sandbox attach');
174
174
  return results.every((r) => r.status === 'fulfilled') ? 0 : 1;
175
175
  }
package/dist/cli/index.js CHANGED
@@ -17,7 +17,7 @@ import { versionText } from './version.js';
17
17
  import { HANDLERS } from './commands/index.js';
18
18
  import { definitionHandler, operationHandler } from './operations/index.js';
19
19
  import { runUpdate } from '../app/features/updates/index.js';
20
- const entry = path.join(PACKAGE_ROOT, 'dist', 'bin', 'agent-sandbox.js');
20
+ const entry = path.join(PACKAGE_ROOT, 'dist', 'bin', 'sandbox.js');
21
21
  /** A first word that is no command and no agent, but close to one: "Did you mean: sandbox status?". */
22
22
  function failOnTypo(word, agents) {
23
23
  const names = [...COMMANDS.filter((c) => !c.hidden).map((c) => c.name), ...agents];
@@ -152,7 +152,7 @@ export function agentsOptions(m, cwd = process.cwd()) {
152
152
  category: 'options',
153
153
  section: 'This start',
154
154
  hint: WINDOW_TEXT[m.window],
155
- description: 'Hide or show the Windows Sandbox window (sbx-show brings a hidden one back).',
155
+ description: 'Hide or show the Windows Sandbox window (sandbox show brings a hidden one back).',
156
156
  },
157
157
  {
158
158
  value: 'opt:folder',
@@ -71,7 +71,7 @@ export class SandboxService {
71
71
  }
72
72
  async hideWindow() {
73
73
  if (!(await hideSandboxWindow(this.ctx.storageRoot, 5)))
74
- fail('Could not find a visible Windows Sandbox window.', 'It may already be hidden (sbx-show brings it back).');
74
+ fail('Could not find a visible Windows Sandbox window.', 'It may already be hidden (sandbox show brings it back).');
75
75
  }
76
76
  /** What stopping ends. Fails when this sandbox was not started by this tool. */
77
77
  stopPlan() {
@@ -62,7 +62,7 @@ export const SandboxSchema = z.strictObject({
62
62
  protectedClient: z.boolean().describe('Run the sandbox window in Protected Client mode (recommended).'),
63
63
  headless: z
64
64
  .boolean()
65
- .describe('Hide the sandbox window (sbx-show brings it back). A headless sandbox shuts down when its last session ends.'),
65
+ .describe('Hide the sandbox window ("Show the window" brings it back). A headless sandbox shuts down when its last session ends.'),
66
66
  keepAwake: z
67
67
  .boolean()
68
68
  .describe('Keep this PC from sleeping while a sandbox runs, so agents left working overnight keep going (the screen may still turn off; closing a laptop lid still sleeps it).'),
@@ -140,7 +140,7 @@ export class AppInstaller {
140
140
  }
141
141
  catch (e) {
142
142
  warn(entry.name + ' could not be installed and is skipped; the other apps are not affected. Reason: ' + errMsg(e));
143
- this.env.summary.push(entry.name + ': FAILED');
143
+ this.env.summary.push('store' in entry.source ? entry.name + ': not installed - ' + errMsg(e).slice(0, 300) : entry.name + ': FAILED');
144
144
  }
145
145
  }
146
146
  /** The command line an app's shortcut starts it with (Comet: its own profile, DevTools on 127.0.0.1). */
@@ -4,5 +4,7 @@ export interface CheckedPackage {
4
4
  readonly main: string;
5
5
  readonly dependencies: string[];
6
6
  }
7
+ /** Why a Store app has no package here, and what to do (the PC's launch notes say what went wrong there). */
8
+ export declare const MISSING_PACKAGE = "this PC did not hand over its package (it could not fetch it: see the launch notes). Fix: on this PC run \"winget download --id <product id> --source msstore\", then launch again";
7
9
  /** The package in `dir` when its manifest is valid and every file is there as hashed; throws otherwise. */
8
10
  export declare function checkedPackage(dir: string): Promise<CheckedPackage>;
@@ -16,11 +16,13 @@ export function fileSha256(file) {
16
16
  stream.on('end', () => resolve(hash.digest('hex')));
17
17
  });
18
18
  }
19
+ /** Why a Store app has no package here, and what to do (the PC's launch notes say what went wrong there). */
20
+ export const MISSING_PACKAGE = 'this PC did not hand over its package (it could not fetch it: see the launch notes). Fix: on this PC run "winget download --id <product id> --source msstore", then launch again';
19
21
  /** The package in `dir` when its manifest is valid and every file is there as hashed; throws otherwise. */
20
22
  export async function checkedPackage(dir) {
21
23
  const manifest = readValid(StorePackageManifestSchema, path.join(dir, STORE_PACKAGE_MANIFEST));
22
24
  if (!manifest)
23
- throw new Error('the package was not handed over (this PC could not fetch it: see the launch notes)');
25
+ throw new Error(MISSING_PACKAGE);
24
26
  for (const f of manifest.files) {
25
27
  if ((await fileSha256(path.join(dir, f.name)).catch(() => '')) !== f.sha256)
26
28
  throw new Error(f.name + ' is missing or changed');
@@ -23,3 +23,5 @@ export declare function projectFolderLines(workspace: string, workspaceHost: str
23
23
  * (a project that has not been set up yet is not listed: its tools are decided from its own files).
24
24
  */
25
25
  export declare function toolLines(primaryName: string, toolSummary: readonly string[], projects: readonly GuideProject[]): string[];
26
+ /** The PC logs line, only when the launch folder has them (a launch of the sandbox's own source repo). */
27
+ export declare function pcLogsGuideLine(launchDir: string, exists: (dir: string) => boolean): string[];
@@ -39,3 +39,10 @@ export function toolLines(primaryName, toolSummary, projects) {
39
39
  "- Tools by project: each project gets the tools its own files ask for; python and rust already installed here are on every project's PATH.",
40
40
  ];
41
41
  }
42
+ /** The PC logs line, only when the launch folder has them (a launch of the sandbox's own source repo). */
43
+ export function pcLogsGuideLine(launchDir, exists) {
44
+ const dir = path.win32.join(launchDir, 'pc-logs');
45
+ return exists(dir)
46
+ ? ['- PC crash reports and logs (this project only, read-only, never commit them): `' + dir + '`']
47
+ : [];
48
+ }
@@ -6,7 +6,7 @@ import { claudeDir, session, stateDir, workspace } from '../runtime/session.js';
6
6
  import { dataDir, launchDir, logsDir, toolsDir } from '../runtime/paths.js';
7
7
  import { addonGuideLine, APPS, headroomGuideLine, RTK_GUIDE_LINE } from '../../shared/apps/index.js';
8
8
  import { installedAddon, installedRtk } from '../setup/agent-addons.js';
9
- import { projectFolderLines, toolLines } from './guide-lines.js';
9
+ import { pcLogsGuideLine, projectFolderLines, toolLines } from './guide-lines.js';
10
10
  import { sandboxScriptsDir } from '../../shared/scripts.js';
11
11
  import { findUproject, unrealGuideLines } from '../../shared/unreal/index.js';
12
12
  import { DISK } from '../toolchains/engines/unreal-engines.js';
@@ -84,6 +84,7 @@ export function sandboxGuide(toolSummary, lan = 'blocked', projects = [], addons
84
84
  '`',
85
85
  '- RO: `' + launchDir + "` (this boot's scripts) · lost on close: everything else, incl. `C:\\nvm` (Node symlink)",
86
86
  ...mirrorLine(),
87
+ ...pcLogsGuideLine(launchDir, fs.existsSync),
87
88
  ...toolLines(session.project, toolSummary, projects),
88
89
  ...unrealLines(projects),
89
90
  ...unityLines(projects),
@@ -1,23 +1,20 @@
1
1
  // Unreal Engine development, machine-wide (toolchains.unreal, once per boot): the DirectX End-User Runtime the engine
2
2
  // lists and the Epic Games Launcher (real installers: they run every boot), the launcher pointed at the data disk for
3
- // engines and given back the engines it installed there, the Windows SDK debuggers for crash dumps, a check that
4
- // UnrealBuildTool finds Visual Studio the way it looks (the Setup Configuration API), and its parallel compiles capped.
5
- // Visual Studio itself is its own step (visual-studio.ts), which the plan runs first; each project's engine is set up
6
- // by engines/unreal-engines.ts.
3
+ // engines and given back the engines it installed there, the Windows SDK debuggers for crash dumps, and a check that
4
+ // UnrealBuildTool finds Visual Studio the way it looks (the Setup Configuration API). Visual Studio itself is its own
5
+ // step (visual-studio.ts), which the plan runs first; each project's engine is set up by engines/unreal-engines.ts.
7
6
  import fs from 'node:fs';
8
7
  import os from 'node:os';
9
8
  import path from 'node:path';
10
9
  import { errMsg } from '../../../core/util/errors.js';
11
10
  import { readValid } from '../../../shared/io/json.js';
12
11
  import { safeRemove } from '../../../shared/io/safe-remove.js';
13
- import { buildJobs, ubtBuildConfiguration, usableCpus } from '../../../shared/memory/index.js';
14
12
  import { VsSetupReportSchema, vsVerdict } from '../../../shared/unreal/index.js';
15
13
  import { runInstaller } from '../../runtime/pools.js';
16
14
  import { dryRun } from '../../runtime/paths.js';
17
15
  import { exists, mkdirp } from '../../runtime/fs.js';
18
16
  import { log, warn } from '../../runtime/log.js';
19
17
  import { runScriptAsync } from '../../runtime/process.js';
20
- import { session } from '../../runtime/session.js';
21
18
  import { ensureLauncher, fetchLauncher, placeEngines, restoreLauncherItems } from '../engines/epic-launcher.js';
22
19
  import { resetUnrealStatus, updateUnrealStatus } from '../engines/unreal-status.js';
23
20
  import { SIGNERS, signedInstaller } from '../installers.js';
@@ -71,18 +68,6 @@ async function checkVisualStudio() {
71
68
  fs.rmSync(dir, { recursive: true, force: true });
72
69
  }
73
70
  }
74
- /**
75
- * UnrealBuildTool's per-user BuildConfiguration.xml (%APPDATA%, wiped with the sandbox): compiles at once from the
76
- * sandbox's build budget, so a build start cannot outrun the memory Windows backs the sandbox with. Returns the cap.
77
- */
78
- function capUnrealBuilds() {
79
- const cpus = usableCpus(os.cpus().length, session.cpuLimitPercent ?? null);
80
- const jobs = buildJobs(cpus, os.totalmem() / 1024 ** 3);
81
- const appData = process.env.APPDATA ?? 'C:\\Users\\WDAGUtilityAccount\\AppData\\Roaming';
82
- const dir = mkdirp(path.join(appData, 'Unreal Engine', 'UnrealBuildTool'));
83
- fs.writeFileSync(path.join(dir, 'BuildConfiguration.xml'), ubtBuildConfiguration(jobs), 'utf8');
84
- return jobs;
85
- }
86
71
  /** Runs one part, warning (not failing the step) when it does not work. */
87
72
  async function part(what, work, fallback) {
88
73
  try {
@@ -101,9 +86,6 @@ export async function setupUnreal(env, config, _ctx) {
101
86
  return;
102
87
  }
103
88
  resetUnrealStatus();
104
- const jobs = await part('capping parallel compiles', async () => capUnrealBuilds(), 0);
105
- if (jobs)
106
- log('Unreal: UnrealBuildTool runs at most ' + jobs + ' compiles at once (the build budget)');
107
89
  await part('the DirectX End-User Runtime', installDirectX, undefined);
108
90
  const launcher = await part('the Epic Games Launcher', ensureLauncher, null);
109
91
  // Before anyone opens the launcher: engines go to the data disk, and it knows the ones it installed there before.
@@ -149,7 +131,6 @@ export async function setupUnreal(env, config, _ctx) {
149
131
  vs.version +
150
132
  (check.ok ? ' (UnrealBuildTool finds it)' : ' (UnrealBuildTool would NOT find it)')
151
133
  : 'no Visual Studio') +
152
- (jobs ? ', at most ' + jobs + ' compiles at once' : '') +
153
134
  ', DirectX End-User Runtime' +
154
135
  (launcher ? ', Epic Games Launcher' : '') +
155
136
  (debuggers ? ', cdb/windbg' : ''));
@@ -1,4 +1,4 @@
1
- import { fingerprint, SERVICE_KINDS, SERVICE_SPECS, serviceWanted, } from '../../shared/accounts/index.js';
1
+ import { appsNeededFor, fingerprint, SERVICE_KINDS, SERVICE_SPECS, serviceWanted, usedByText, } from '../../shared/accounts/index.js';
2
2
  import { setNameOf } from '../storage/index.js';
3
3
  import { takeHandoffs } from './handoff.js';
4
4
  import { AccountLeases } from './leases.js';
@@ -62,8 +62,22 @@ export function prepareServiceAccounts(i) {
62
62
  }
63
63
  for (const kind of SERVICE_KINDS) {
64
64
  const spec = SERVICE_SPECS[kind];
65
- if (!i.config.accounts[kind].share || !serviceWanted(kind, i.on))
65
+ if (!i.config.accounts[kind].share)
66
66
  continue;
67
+ if (!serviceWanted(kind, i.on)) {
68
+ const apps = appsNeededFor(kind);
69
+ if (apps.length)
70
+ notes.push('Your ' +
71
+ spec.label +
72
+ ' sign-in is on (accounts.' +
73
+ kind +
74
+ '.share) but ' +
75
+ usedByText(spec.usedBy) +
76
+ ' is not installed, so there is nothing to hand it to. Fix: turn the app on in Apps (apps.install: ' +
77
+ apps.join(', ') +
78
+ ').');
79
+ continue;
80
+ }
67
81
  const off = i.others.find((o) => !o.config.accounts[kind].share);
68
82
  if (off) {
69
83
  notes.push('Your ' + spec.label + ' sign-in was not handed over: ' + off.name + ' (also in this sandbox) turns it off.');
@@ -12,6 +12,8 @@ export declare const spawnStoreRunner: StoreRunner;
12
12
  /** What an app needs fetched (catalogue source.store). */
13
13
  export interface StoreApp {
14
14
  readonly id: string;
15
+ /** The name the notes use (the catalogue's); the id when not given. */
16
+ readonly name?: string;
15
17
  readonly productId: string;
16
18
  readonly packageName: string;
17
19
  }
@@ -10,7 +10,7 @@ import { spawn } from 'node:child_process';
10
10
  import { z } from 'zod';
11
11
  import { assertNoLinks, errMsg } from '../../core/util/index.js';
12
12
  import { readValid } from '../../shared/io/index.js';
13
- import { isStorePackageFile, STORE_PACKAGE_MANIFEST, StorePackageManifestSchema, storePackageRole, } from '../../shared/apps/index.js';
13
+ import { isStorePackageFile, STORE_PACKAGE_MANIFEST, StorePackageManifestSchema, assignStoreRoles, storePackageFitsX64, } from '../../shared/apps/index.js';
14
14
  import { SYSTEM } from '../../shared/system.js';
15
15
  import { nativeScript } from '../native.js';
16
16
  import { hostDataFolder } from '../storage/contained/index.js';
@@ -24,6 +24,8 @@ const ScriptResultSchema = z.object({
24
24
  ok: z.boolean(),
25
25
  message: z.string().max(2000),
26
26
  files: z.array(z.string().max(300)).max(64),
27
+ /** The files that came from winget's Dependencies folder (the frameworks); the rest is the app. */
28
+ dependencies: z.array(z.string().max(300)).max(64).default([]),
27
29
  });
28
30
  export const spawnStoreRunner = (exe, args, timeoutMs) => new Promise((resolve) => {
29
31
  const child = spawn(exe, [...args], { windowsHide: true });
@@ -117,16 +119,15 @@ export class StorePackages {
117
119
  }
118
120
  if (!parsed.ok)
119
121
  throw new Error(parsed.message);
120
- const files = parsed.files
121
- .filter((n) => isStorePackageFile(n) && n === path.basename(n) && fs.existsSync(path.join(work, n)))
122
- .map((name) => {
122
+ const frameworks = new Set(parsed.dependencies.map((n) => n.toLowerCase()));
123
+ // Only the builds that run on this sandbox (64-bit): the 32-bit and ARM frameworks winget may add are left out.
124
+ const names = parsed.files.filter((n) => isStorePackageFile(n) &&
125
+ n === path.basename(n) &&
126
+ fs.existsSync(path.join(work, n)) &&
127
+ !(frameworks.has(n.toLowerCase()) && !storePackageFitsX64(n)));
128
+ const files = assignStoreRoles(names, app.packageName, parsed.dependencies).map(({ name, role }) => {
123
129
  const full = path.join(work, name);
124
- return {
125
- name,
126
- role: storePackageRole(name, app.packageName),
127
- sha256: sha256Of(full),
128
- bytes: fs.statSync(full).size,
129
- };
130
+ return { name, role, sha256: sha256Of(full), bytes: fs.statSync(full).size };
130
131
  });
131
132
  const manifest = StorePackageManifestSchema.parse({
132
133
  productId: app.productId,
@@ -197,12 +198,12 @@ export async function prepareStorePackages(apps, o) {
197
198
  notes.push(app.id + ': package ' + note + ' (' + manifest.files.length + ' files)');
198
199
  }
199
200
  catch (e) {
200
- notes.push(app.id +
201
- ' is not prepared: ' +
202
- errMsg(e) +
203
- ' Fix: sign in to the Microsoft Store on this PC and check that winget works (winget download --id ' +
201
+ notes.push((app.name ?? app.id) +
202
+ ': NOT installed in this sandbox. ' +
203
+ errMsg(e).replace(/\.$/, '') +
204
+ '. Fix: sign in to the Microsoft Store on this PC and check that winget works (winget download --id ' +
204
205
  app.productId +
205
- ' --source msstore), then launch again.');
206
+ ' --source msstore --architecture x64 --download-directory <an empty folder>), then launch again.');
206
207
  }
207
208
  }
208
209
  return { notes };
@@ -61,7 +61,7 @@ export async function attachTerminal(session, o = {}) {
61
61
  const endpoint = hostEndpoint(session) ??
62
62
  fail("Refusing to connect: the sandbox reported no address on this PC's Hyper-V (vEthernet) network (" +
63
63
  session.attach.addresses.join(', ') +
64
- ').', 'Use the sandbox window instead (sbx-show); if this keeps happening, run sandbox doctor.');
64
+ ').', 'Use the sandbox window instead (sandbox show); if this keeps happening, run sandbox doctor.');
65
65
  const socket = await connect(endpoint);
66
66
  console.log('Attaching to ' +
67
67
  safeText(session.harness) +
@@ -50,7 +50,7 @@ export class SandboxChannel {
50
50
  fail('Windows Sandbox closed (or is not running).', 'Start it again with sandbox claude; sandbox logs shows what happened.');
51
51
  await sleep(500);
52
52
  }
53
- fail(timeoutMessage, 'Check what the sandbox is doing with sandbox logs -f (or sbx-show to see its window).');
53
+ fail(timeoutMessage, 'Check what the sandbox is doing with sandbox logs -f (or sandbox show to see its window).');
54
54
  }
55
55
  /** Deletes an answer once read; a planted link means the answer is read and nothing is deleted. */
56
56
  dropResponse(requestId) {
@@ -73,7 +73,7 @@ export class SandboxChannel {
73
73
  const status = this.run.readSandboxStatus();
74
74
  if (!status || (launchId !== null && status.launchId !== launchId)) {
75
75
  if (bootSilent(Date.now() - started, seen))
76
- fail('The sandbox has not reported anything for ' + Math.round(FIRST_STATUS_MS / 60000) + ' minutes.', 'It may still be starting, or it cannot reach its folders. Look at its window (sbx-show), the run folder ' +
76
+ fail('The sandbox has not reported anything for ' + Math.round(FIRST_STATUS_MS / 60000) + ' minutes.', 'It may still be starting, or it cannot reach its folders. Look at its window (sandbox show), the run folder ' +
77
77
  this.run.root +
78
78
  ' and the data folder ' +
79
79
  this.dataPath +
@@ -87,7 +87,7 @@ export class SandboxChannel {
87
87
  last = status.message;
88
88
  }
89
89
  if (status.phase === 'failed')
90
- fail('The sandbox failed to start: ' + status.message, 'See sandbox logs for details; the sandbox window shows the error too (sbx-show if headless).');
90
+ fail('The sandbox failed to start: ' + status.message, 'See sandbox logs for details; the sandbox window shows the error too (sandbox show if headless).');
91
91
  return status.phase === 'ready' ? status : null;
92
92
  }, 'Timed out waiting for the sandbox to finish provisioning.');
93
93
  }
@@ -16,6 +16,7 @@ export const COMMANDS = new RegExp('^(' +
16
16
  // this tool's own commands (package.json "bin")
17
17
  'sandbox',
18
18
  'windows-development-sandbox',
19
+ // Removed commands, still blocked: a planted claude-sbx.cmd would run on a PC where nothing else has that name.
19
20
  'agent-sandbox',
20
21
  'claude-sbx',
21
22
  'claude-sandbox',
@@ -1,4 +1,4 @@
1
- export declare const COMMAND_NAMES: readonly ["sandbox", "agent-sandbox", "claude-sbx"];
1
+ export declare const COMMAND_NAMES: readonly ["sandbox"];
2
2
  export type CopyKind = 'managed-machine' | 'managed-user' | 'npm' | 'old-scripts' | 'other';
3
3
  /** One folder on the PATH that holds the commands. */
4
4
  export interface CommandCopy {
@@ -1,10 +1,10 @@
1
- // Every copy of the sandbox commands on the PATH (sandbox, agent-sandbox, claude-sbx), in the order Windows finds them:
1
+ // Every copy of the sandbox commands on the PATH (sandbox), in the order Windows finds them:
2
2
  // the machine PATH, then yours, and in each folder PATHEXT order. Read only: nothing is run, and a file is only checked
3
3
  // for existing. Used by doctor to say which copy runs and how to remove each of the others.
4
4
  import fs from 'node:fs';
5
5
  import path from 'node:path';
6
6
  import { expandVariables } from '../../core/util/index.js';
7
- export const COMMAND_NAMES = ['sandbox', 'agent-sandbox', 'claude-sbx'];
7
+ export const COMMAND_NAMES = ['sandbox'];
8
8
  const normal = (p) => path
9
9
  .normalize(expandVariables(p.trim().replace(/^"|"$/g, '')))
10
10
  .replace(/[\\/]+$/, '')
@@ -1,5 +1,5 @@
1
1
  export { type InstallScope, InstallLayout, compareVersions, VERSION_PATTERN, NODE_PATTERN } from './layout.js';
2
- export { shimText, readBins, writeShims } from './shims.js';
2
+ export { shimText, readBins, writeShims, removeStaleShims, REMOVED_COMMANDS } from './shims.js';
3
3
  export { InstallRecordFile, InstallRecordSchema, type InstallRecord } from './install-record.js';
4
4
  export { UpdateTask } from './scheduled-task.js';
5
5
  export { machineInstall, userInstall, managedInstalls, describeCopy, hiddenInstallLine, takeHiddenInstallNotice, } from './copies.js';
@@ -1,5 +1,5 @@
1
1
  export { InstallLayout, compareVersions, VERSION_PATTERN, NODE_PATTERN } from './layout.js';
2
- export { shimText, readBins, writeShims } from './shims.js';
2
+ export { shimText, readBins, writeShims, removeStaleShims, REMOVED_COMMANDS } from './shims.js';
3
3
  export { InstallRecordFile, InstallRecordSchema } from './install-record.js';
4
4
  export { UpdateTask } from './scheduled-task.js';
5
5
  export { machineInstall, userInstall, managedInstalls, describeCopy, hiddenInstallLine, takeHiddenInstallNotice, } from './copies.js';
@@ -6,5 +6,12 @@ import type { InstallLayout } from './layout.js';
6
6
  export declare function shimText(name: string, kind: 'cmd' | 'sh', target?: string): string;
7
7
  /** The commands a package.json declares (name -> program path inside the package). Invalid names are refused. */
8
8
  export declare function readBins(packageJson: unknown): Record<string, string>;
9
- /** Writes <bin>.cmd and <bin> for every command in `bins` (name -> program path inside the package). */
9
+ /** Commands earlier versions installed (name -> program they ran). Their shims go when nothing declares them. */
10
+ export declare const REMOVED_COMMANDS: Readonly<Record<string, string>>;
11
+ /** Deletes the shim of a removed command, only when the file is exactly the text we wrote for it. */
12
+ export declare function removeStaleShims(layout: InstallLayout, keep?: Readonly<Record<string, string>>): void;
13
+ /**
14
+ * Writes <bin>.cmd and <bin> for every command in `bins` (name -> program path inside the package), and removes the
15
+ * shims of commands that no longer exist.
16
+ */
10
17
  export declare function writeShims(layout: InstallLayout, bins: Record<string, string>): void;
@@ -1,6 +1,7 @@
1
1
  // The commands on PATH. Each one is a tiny script that reads the two pointer files and runs the version they name, so
2
2
  // an update never has to rewrite a shim: it only moves the pointers. A .cmd for cmd.exe and PowerShell, and a POSIX
3
3
  // script (same name, no extension) for Git Bash.
4
+ import fs from 'node:fs';
4
5
  import path from 'node:path';
5
6
  import { z } from 'zod';
6
7
  import { fail } from '../../core/util/index.js';
@@ -46,10 +47,47 @@ export function readBins(packageJson) {
46
47
  fail('package.json declares a command with an invalid name: ' + name);
47
48
  return bins;
48
49
  }
49
- /** Writes <bin>.cmd and <bin> for every command in `bins` (name -> program path inside the package). */
50
+ /** Commands earlier versions installed (name -> program they ran). Their shims go when nothing declares them. */
51
+ export const REMOVED_COMMANDS = {
52
+ 'agent-sandbox': 'dist/bin/agent-sandbox.js',
53
+ 'claude-sbx': 'dist/bin/claude-sbx.js',
54
+ 'claude-sandbox': 'dist/bin/claude-sbx.js',
55
+ 'codex-sbx': 'dist/bin/codex-sbx.js',
56
+ 'codex-sandbox': 'dist/bin/codex-sbx.js',
57
+ 'gemini-sbx': 'dist/bin/gemini-sbx.js',
58
+ 'gemini-sandbox': 'dist/bin/gemini-sbx.js',
59
+ 'claude-host': 'dist/bin/claude-host.js',
60
+ 'sbx-show': 'dist/bin/sbx-show.js',
61
+ 'sbx-hide': 'dist/bin/sbx-hide.js',
62
+ };
63
+ /** Deletes the shim of a removed command, only when the file is exactly the text we wrote for it. */
64
+ export function removeStaleShims(layout, keep = {}) {
65
+ for (const [name, target] of Object.entries(REMOVED_COMMANDS)) {
66
+ if (name in keep)
67
+ continue;
68
+ for (const [file, kind] of [
69
+ [name + '.cmd', 'cmd'],
70
+ [name, 'sh'],
71
+ ]) {
72
+ const full = path.join(layout.binDir, file);
73
+ try {
74
+ if (fs.readFileSync(full, 'utf8') === shimText(name, kind, target))
75
+ fs.rmSync(full, { force: true });
76
+ }
77
+ catch {
78
+ // Not there, or not readable: nothing of ours to remove.
79
+ }
80
+ }
81
+ }
82
+ }
83
+ /**
84
+ * Writes <bin>.cmd and <bin> for every command in `bins` (name -> program path inside the package), and removes the
85
+ * shims of commands that no longer exist.
86
+ */
50
87
  export function writeShims(layout, bins) {
51
88
  for (const [name, target] of Object.entries(bins)) {
52
89
  writeFileAtomic(path.join(layout.binDir, name + '.cmd'), shimText(name, 'cmd', target));
53
90
  writeFileAtomic(path.join(layout.binDir, name), shimText(name, 'sh', target));
54
91
  }
92
+ removeStaleShims(layout, bins);
55
93
  }
@@ -15,6 +15,7 @@ import { folderAttachment } from '../storage/providers/index.js';
15
15
  import { assertTestWritable } from '../test-guard.js';
16
16
  import { writeLoadSettings } from '../life/watchdog.js';
17
17
  import { loadProfileOf } from './risk-notes.js';
18
+ import { launchHasSandboxSource, stagePcLogs } from './pc-logs.js';
18
19
  import { planBootProjects } from './boot-projects.js';
19
20
  import { planDatabases } from './database-plan.js';
20
21
  import { collectMappings } from './mappings.js';
@@ -135,6 +136,9 @@ opened) {
135
136
  fs.writeFileSync(path.join(launchPath, 'env.cmd'), logonEnv(session.workspace), 'utf8');
136
137
  copyRuntime(o.packageRoot, launchPath);
137
138
  stageHooks(o.packageRoot, launchPath, o.run.scripts);
139
+ // Developing the sandbox inside a sandbox: this PC's recent crash reports and logs (never for other projects).
140
+ if (launchHasSandboxSource([workspaceHost, ...boot.projects.map((p) => p.host)], o.packageRoot))
141
+ stagePcLogs(storageRoot, dataPath, launchPath);
138
142
  const enginePlan = engine.plan(buildLaunchPlan({
139
143
  config,
140
144
  memoryMb,
@@ -10,3 +10,4 @@ export { msvcHint, projectEntries, readProjectFiles, uvHint } from './project-sc
10
10
  export { hostDatabases, pcLanguages } from './pc-scan.js';
11
11
  export { collectMappings } from './mappings.js';
12
12
  export { RUNTIME_FILES, copyRuntime, gitIdentity, stageGitIdentity, stageHooks, stageScripts } from './stage.js';
13
+ export { isSandboxSourceRepo, launchHasSandboxSource, stagePcLogs } from './pc-logs.js';
@@ -10,3 +10,4 @@ export { msvcHint, projectEntries, readProjectFiles, uvHint } from './project-sc
10
10
  export { hostDatabases, pcLanguages } from './pc-scan.js';
11
11
  export { collectMappings } from './mappings.js';
12
12
  export { RUNTIME_FILES, copyRuntime, gitIdentity, stageGitIdentity, stageHooks, stageScripts } from './stage.js';
13
+ export { isSandboxSourceRepo, launchHasSandboxSource, stagePcLogs } from './pc-logs.js';
@@ -0,0 +1,6 @@
1
+ /** Whether a folder is a checkout (or git worktree) of this sandbox's own source repo, judged by the installed copy. */
2
+ export declare function isSandboxSourceRepo(folder: string, packageRoot: string): boolean;
3
+ /** Whether this launch has the sandbox's own source repo among its projects (the launch folder or a mapped project). */
4
+ export declare function launchHasSandboxSource(folders: readonly string[], packageRoot: string): boolean;
5
+ /** Copies the PC's recent crash reports and log tails into <launchPath>\pc-logs. Returns the folder. */
6
+ export declare function stagePcLogs(storageRoot: string, dataPath: string, launchPath: string): string;