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
@@ -0,0 +1,18 @@
1
+ // The lines of the guest's log files: plain text (no colours), one timestamped line per line of the message. Pure.
2
+ /** Colour and cursor sequences (CSI ... letter), bounded so a hostile line costs linear time. */
3
+ const ESCAPES = /\x1b\[[0-9;?]{0,32}[A-Za-z]/g;
4
+ /** Text without its terminal colour and cursor sequences. */
5
+ export function plainText(text) {
6
+ return text.replace(ESCAPES, '');
7
+ }
8
+ /**
9
+ * The file lines for one message: each line of it with an ISO timestamp (UTC, zero-padded:
10
+ * `2026-11-04T12:07:27.930Z`) and the task that logged it, if any; blank lines are dropped.
11
+ */
12
+ export function fileLines(message, at = new Date(), task) {
13
+ const stamp = at.toISOString() + ' ' + (task ? '[' + task + '] ' : '');
14
+ return plainText(message)
15
+ .split(/\r?\n/)
16
+ .filter((line) => line.trim() !== '')
17
+ .map((line) => stamp + line.trimEnd());
18
+ }
@@ -1,15 +1,28 @@
1
- /** Who is logging: the task title (a prefix while several tasks run) and whether the console stays silent. */
1
+ /** Who is logging: the boot task (its stage and title) and whether the console stays silent. */
2
2
  export interface LogContext {
3
- readonly prefix?: () => string;
3
+ readonly stage?: number;
4
+ readonly task?: string;
4
5
  /** File only: nothing may print once the first session owns the console. */
5
6
  readonly quiet?: boolean;
6
7
  }
8
+ /** Takes the console's lines while the boot draws its progress (boot/boot-console.ts). */
9
+ export interface ConsoleSink {
10
+ /** An info line; `stage` is the stage of the task that logged it. */
11
+ line(message: string, stage: number | undefined): void;
12
+ warning(message: string): void;
13
+ /** A line the user should read once the boot is ready (shown with the summary). */
14
+ notice(message: string): void;
15
+ }
7
16
  /** Runs `work` so that everything it logs (also after awaits) carries `ctx`. */
8
17
  export declare function withLogContext<T>(ctx: LogContext, work: () => T): T;
9
18
  /** Runs `work` logging to the file only. */
10
19
  export declare function runQuiet<T>(work: () => T): T;
11
- /** One line of the file: an ISO timestamp first (the console shows no times). */
12
- export declare function fileLine(message: string, at?: Date): string;
20
+ /** Sends the console's lines to `next` (null: straight to the console again). */
21
+ export declare function routeConsole(next: ConsoleSink | null): void;
13
22
  export declare function log(message: string): void;
14
23
  export declare function warn(message: string): void;
24
+ /** A line for the user once the boot is ready (printed at once outside the boot). */
25
+ export declare function notice(message: string): void;
26
+ /** A line for the log file only. */
27
+ export declare function record(message: string): void;
15
28
  export declare const logPath: string;
@@ -1,10 +1,14 @@
1
+ // The guest's log: every line goes to the log file (timestamped, plain) and, unless it is quiet, to the console, or
2
+ // to the boot's progress view while that owns the console.
1
3
  import fs from 'node:fs';
2
4
  import path from 'node:path';
3
5
  import { AsyncLocalStorage } from 'node:async_hooks';
4
6
  import { logsDir } from './paths.js';
7
+ import { fileLines } from './log-line.js';
5
8
  fs.mkdirSync(logsDir, { recursive: true });
6
9
  const logFile = path.join(logsDir, (process.env.AGENT_LOG_PREFIX ?? 'guest') + '-' + new Date().toISOString().replace(/[:.]/g, '-') + '.log');
7
10
  const context = new AsyncLocalStorage();
11
+ let sink = null;
8
12
  /** Runs `work` so that everything it logs (also after awaits) carries `ctx`. */
9
13
  export function withLogContext(ctx, work) {
10
14
  return context.run(ctx, work);
@@ -13,34 +17,53 @@ export function withLogContext(ctx, work) {
13
17
  export function runQuiet(work) {
14
18
  return context.run({ ...context.getStore(), quiet: true }, work);
15
19
  }
16
- /** One line of the file: an ISO timestamp first (the console shows no times). */
17
- export function fileLine(message, at = new Date()) {
18
- return at.toISOString() + ' ' + message;
20
+ /** Sends the console's lines to `next` (null: straight to the console again). */
21
+ export function routeConsole(next) {
22
+ sink = next;
19
23
  }
20
- const append = (line) => {
24
+ const append = (message, ctx) => {
21
25
  try {
22
- fs.appendFileSync(logFile, fileLine(line) + '\n');
26
+ const lines = fileLines(message, new Date(), ctx?.task);
27
+ if (lines.length)
28
+ fs.appendFileSync(logFile, lines.join('\n') + '\n');
23
29
  }
24
30
  catch {
25
31
  /* logging must never break the run */
26
32
  }
27
33
  };
28
- const tagged = (message, ctx) => {
29
- const prefix = ctx?.prefix?.();
30
- return prefix ? prefix + ' | ' + message : message;
31
- };
32
34
  export function log(message) {
33
35
  const ctx = context.getStore();
34
- const line = tagged(message, ctx);
35
- if (!ctx?.quiet)
36
- console.log(line);
37
- append(line);
36
+ append(message, ctx);
37
+ if (ctx?.quiet)
38
+ return;
39
+ if (sink)
40
+ sink.line(message, ctx?.stage);
41
+ else
42
+ console.log(message);
38
43
  }
39
44
  export function warn(message) {
40
45
  const ctx = context.getStore();
41
- const line = tagged(message, ctx);
42
- if (!ctx?.quiet)
43
- console.warn('\x1b[33mWARNING: ' + line + '\x1b[0m');
44
- append('WARNING: ' + line);
46
+ append('WARNING: ' + message, ctx);
47
+ if (ctx?.quiet)
48
+ return;
49
+ if (sink)
50
+ sink.warning(message);
51
+ else
52
+ console.warn('\x1b[33mWARNING: ' + message + '\x1b[0m');
53
+ }
54
+ /** A line for the user once the boot is ready (printed at once outside the boot). */
55
+ export function notice(message) {
56
+ const ctx = context.getStore();
57
+ append(message, ctx);
58
+ if (ctx?.quiet)
59
+ return;
60
+ if (sink)
61
+ sink.notice(message);
62
+ else
63
+ console.log(message);
64
+ }
65
+ /** A line for the log file only. */
66
+ export function record(message) {
67
+ append(message, context.getStore());
45
68
  }
46
69
  export const logPath = logFile;
@@ -1,4 +1,13 @@
1
1
  import { type SpawnOptions, type SpawnSyncOptions } from 'node:child_process';
2
+ /** Takes the output of the programs started while the boot draws its progress (boot/boot-console.ts). */
3
+ export interface OutputSink {
4
+ /** Whether programs started now hand their output over instead of writing to the console. */
5
+ capturing(): boolean;
6
+ /** One line of a program's output (to the log). */
7
+ line(text: string): void;
8
+ }
9
+ /** Hands the output of programs started from now on to `sink` while it is capturing (null: the console again). */
10
+ export declare function routeOutput(sink: OutputSink | null): void;
2
11
  /** Runs a program, streaming output, and throws with a readable message when it fails. */
3
12
  export declare function exec(file: string, args: string[], options?: SpawnSyncOptions): void;
4
13
  /** exec without blocking: other toolchains keep downloading while this one runs an installer. */
@@ -8,9 +8,48 @@ import { robocopyErrors, robocopyFailed, robocopyMeaning } from '../../shared/io
8
8
  import { SYSTEM } from '../../shared/system.js';
9
9
  import { scriptArgs } from '../../shared/scripts.js';
10
10
  import { isWin } from './paths.js';
11
+ // Set by the boot only: this module stays free of the log (pure helpers such as tool-env import it).
12
+ let outputSink = null;
13
+ /** Hands the output of programs started from now on to `sink` while it is capturing (null: the console again). */
14
+ export function routeOutput(sink) {
15
+ outputSink = sink;
16
+ }
17
+ const captureOutput = () => outputSink?.capturing() ?? false;
18
+ /** A program's captured output, line by line (its blank lines dropped). */
19
+ function logOutput(text) {
20
+ for (const line of text.split(/\r?\n/))
21
+ if (line.trim())
22
+ outputSink?.line(line.trimEnd());
23
+ }
24
+ /** What a program may print while the boot holds its output for the log (a larger output stops it). */
25
+ const CAPTURE_BYTES = 64 * 1024 * 1024;
26
+ /** A captured line longer than this is cut (an installer's progress bar without line ends). */
27
+ const LINE_CHARS = 2000;
28
+ /** Feeds a stream to the log line by line as it arrives. */
29
+ function logLines(stream) {
30
+ if (!stream)
31
+ return;
32
+ let partial = '';
33
+ stream.setEncoding('utf8');
34
+ stream.on('data', (chunk) => {
35
+ const lines = (partial + chunk).split(/\r?\n/);
36
+ partial = lines.pop().slice(0, LINE_CHARS);
37
+ logOutput(lines.map((l) => l.slice(0, LINE_CHARS)).join('\n'));
38
+ });
39
+ stream.on('end', () => logOutput(partial));
40
+ }
11
41
  /** Runs a program, streaming output, and throws with a readable message when it fails. */
12
42
  export function exec(file, args, options = {}) {
13
- const result = spawnSync(file, args, { stdio: 'inherit', windowsHide: true, ...options });
43
+ // While the boot draws its progress, the program's output goes to the log (it would break the drawing).
44
+ const capture = captureOutput();
45
+ const result = spawnSync(file, args, {
46
+ stdio: capture ? ['inherit', 'pipe', 'pipe'] : 'inherit',
47
+ windowsHide: true,
48
+ ...(capture ? { maxBuffer: CAPTURE_BYTES } : {}),
49
+ ...options,
50
+ });
51
+ if (capture)
52
+ logOutput(String(result.stdout ?? '') + '\n' + String(result.stderr ?? ''));
14
53
  if (result.error)
15
54
  throw result.error;
16
55
  if (result.status !== 0)
@@ -18,36 +57,59 @@ export function exec(file, args, options = {}) {
18
57
  }
19
58
  /** exec without blocking: other toolchains keep downloading while this one runs an installer. */
20
59
  export async function execAsync(file, args, options = {}) {
21
- const code = await spawnAsync(file, args, { stdio: 'inherit', ...options });
60
+ const code = captureOutput()
61
+ ? await new Promise((resolve, reject) => {
62
+ const child = spawn(file, args, { stdio: ['inherit', 'pipe', 'pipe'], windowsHide: true, ...options });
63
+ logLines(child.stdout);
64
+ logLines(child.stderr);
65
+ child.once('error', reject);
66
+ child.once('close', (c) => resolve(c ?? -1));
67
+ })
68
+ : await spawnAsync(file, args, { stdio: 'inherit', ...options });
22
69
  if (code !== 0)
23
70
  throw new Error(path.basename(file) + ' ' + args.slice(0, 3).join(' ') + ' failed with exit code ' + code);
24
71
  }
72
+ /** A script's error output: to the log while the boot holds the console, else to the console's error stream. */
73
+ function scriptErrors(errors) {
74
+ if (!errors)
75
+ return;
76
+ if (captureOutput())
77
+ logOutput(errors);
78
+ else
79
+ process.stderr.write(errors + '\n');
80
+ }
25
81
  export function execOutput(file, args, options = {}) {
26
82
  const result = spawnSync(file, args, { encoding: 'utf8', windowsHide: true, ...options });
27
83
  return result.status === 0 ? String(result.stdout ?? '').trim() : '';
28
84
  }
29
85
  /** Runs sandbox\scripts\<name> (a .ps1 in the launch folder) with `args`; throws when it fails. */
30
86
  export function runScript(name, args = []) {
87
+ const capture = captureOutput();
31
88
  const result = spawnSync(SYSTEM.powershell(), scriptArgs(name, args), {
32
- stdio: ['inherit', 'inherit', 'pipe'],
89
+ stdio: ['inherit', capture ? 'pipe' : 'inherit', 'pipe'],
33
90
  encoding: 'utf8',
34
91
  windowsHide: true,
92
+ ...(capture ? { maxBuffer: CAPTURE_BYTES } : {}),
35
93
  });
94
+ if (capture)
95
+ logOutput(String(result.stdout ?? ''));
36
96
  if (result.error)
37
97
  throw result.error;
38
98
  const errors = String(result.stderr ?? '').trim();
39
- if (errors)
40
- process.stderr.write(errors + '\n');
99
+ scriptErrors(errors);
41
100
  if (result.status !== 0)
42
101
  throw new Error(name + ' failed with exit code ' + result.status + (errors ? ': ' + errors.slice(0, 400) : ''));
43
102
  }
44
103
  /** runScript without blocking the event loop (other boot tasks keep going while the script runs). */
45
104
  export function runScriptAsync(name, args = []) {
46
105
  return new Promise((resolve, reject) => {
106
+ const capture = captureOutput();
47
107
  const child = spawn(SYSTEM.powershell(), scriptArgs(name, args), {
48
- stdio: ['inherit', 'inherit', 'pipe'],
108
+ stdio: ['inherit', capture ? 'pipe' : 'inherit', 'pipe'],
49
109
  windowsHide: true,
50
110
  });
111
+ if (capture)
112
+ logLines(child.stdout);
51
113
  let errors = '';
52
114
  child.stderr?.setEncoding('utf8');
53
115
  child.stderr?.on('data', (chunk) => {
@@ -57,8 +119,7 @@ export function runScriptAsync(name, args = []) {
57
119
  child.once('error', reject);
58
120
  child.once('close', (code) => {
59
121
  errors = errors.trim();
60
- if (errors)
61
- process.stderr.write(errors + '\n');
122
+ scriptErrors(errors);
62
123
  if (code !== 0)
63
124
  reject(new Error(name + ' failed with exit code ' + code + (errors ? ': ' + errors.slice(0, 400) : '')));
64
125
  else
@@ -1,7 +1,7 @@
1
1
  // The environment every agent session, hook and terminal tab in the sandbox gets.
2
2
  import path from 'node:path';
3
3
  import { claudeDir, session, stateDir, workspace } from '../runtime/session.js';
4
- import { dataDir, dryRun, isWin, testMode, toolsDir } from '../runtime/paths.js';
4
+ import { dataDir, dryRun, isWin, localDir, testMode, toolsDir } from '../runtime/paths.js';
5
5
  import { exists, mkdirp } from '../runtime/fs.js';
6
6
  import { execOutput, runScriptWithJson, runScriptWithJsonAsync } from '../runtime/process.js';
7
7
  import { SYSTEM } from '../../shared/system.js';
@@ -9,7 +9,7 @@ import { warn } from '../runtime/log.js';
9
9
  import { errMsg } from '../../core/util/errors.js';
10
10
  import { localStateVars } from '../agents/local-state.js';
11
11
  import { agentPaths } from '../agents/agent-files.js';
12
- import { appendMissing, expandVars, isPinnedNodeDir, msvcBeforeGitUsr, parseRegPath, pathKey, planPath, } from './path-plan.js';
12
+ import { appendMissing, expandVars, isPinnedNodeDir, parseRegPath, pathKey, planPath, sessionPath, } from './path-plan.js';
13
13
  /** Older launch folders have no agentHomes: these two were always set. */
14
14
  const LEGACY_HOMES = { CODEX_HOME: 'codex', GEMINI_CLI_HOME: 'gemini' };
15
15
  /** Every agent's home variable (CODEX_HOME ...) with the folder it points to in the persistent state. */
@@ -57,8 +57,13 @@ export function agentEnv(tools, target = { workspace, project: session.project,
57
57
  // The runner's own Node (bootstrap.ps1 puts it first on PATH) is not exposed: `node` is whatever nvm selected.
58
58
  const runners = [path.join(toolsDir, 'node'), path.dirname(process.execPath)].map((d) => d.toLowerCase());
59
59
  const base = (target.base.PATH ?? '').split(path.delimiter).filter((d) => d && !runners.includes(d.toLowerCase()));
60
- // MSVC's link.exe before Git's coreutils link.exe, wherever each came from.
61
- env.PATH = msvcBeforeGitUsr([...new Set([...tools.prepend, ...base, ...tools.append])]).join(path.delimiter);
60
+ // The sandbox's commands (agent shims, sbx-db) first; MSVC's link.exe before Git's coreutils link.exe.
61
+ env.PATH = sessionPath({
62
+ commands: path.join(localDir, 'bin'),
63
+ prepend: tools.prepend,
64
+ base,
65
+ append: tools.append,
66
+ }).join(path.delimiter);
62
67
  return env;
63
68
  }
64
69
  /** Makes the agent environment machine-wide, so new terminal tabs and anything the agent starts see the same tools. */
@@ -4,6 +4,8 @@ export interface HookTarget {
4
4
  cwd: string;
5
5
  }
6
6
  export declare function projectHooks(name: string, cwd: string): HookTarget;
7
+ /** Whether `stage` has hooks to run (they write to the terminal themselves). */
8
+ export declare function hasHooks(stage: string, target?: HookTarget): boolean;
7
9
  export declare function runHooks(stage: string, env: NodeJS.ProcessEnv, target?: HookTarget): void;
8
10
  /** The same without blocking: the session daemon keeps answering other requests while a project's hooks run. */
9
11
  export declare function runHooksAsync(stage: string, env: NodeJS.ProcessEnv, target: HookTarget): Promise<void>;
@@ -45,6 +45,10 @@ const failed = (stage, name, status) => {
45
45
  throw new Error(message);
46
46
  warn(message);
47
47
  };
48
+ /** Whether `stage` has hooks to run (they write to the terminal themselves). */
49
+ export function hasHooks(stage, target = BOOT) {
50
+ return hooksOf(stage, target).length > 0;
51
+ }
48
52
  export function runHooks(stage, env, target = BOOT) {
49
53
  for (const { name, command } of hooksOf(stage, target)) {
50
54
  log('Hook [' + stage + ']: ' + name);
@@ -13,6 +13,18 @@ export interface PathPlanInput {
13
13
  }
14
14
  /** Ordered, de-duplicated (case, trailing slash) PATH folders; missing and dropped folders are left out. */
15
15
  export declare function planPath(input: PathPlanInput): string[];
16
+ /**
17
+ * The PATH folders of the agent environment (sessions, the machine PATH, terminal tabs): the sandbox's own commands
18
+ * folder first, because its `claude` / `codex` shims carry the sandbox's flags and must beat a copy installed with
19
+ * `npm i -g` in nvm's node folder; then the toolchains' folders, the process's own and the appended ones. No
20
+ * duplicates (matched like pathKey); MSVC's tools before Git's usr\bin.
21
+ */
22
+ export declare function sessionPath(o: {
23
+ readonly commands: string;
24
+ readonly prepend: readonly string[];
25
+ readonly base: readonly string[];
26
+ readonly append: readonly string[];
27
+ }): string[];
16
28
  /** `base` with the folders of `extra` it lacks added at the end (a process keeps its own order and entries). */
17
29
  export declare function appendMissing(base: string, extra: string, skip?: (folder: string) => boolean): string;
18
30
  /**
@@ -32,6 +32,24 @@ export function planPath(input) {
32
32
  }
33
33
  return msvcBeforeGitUsr(result);
34
34
  }
35
+ /**
36
+ * The PATH folders of the agent environment (sessions, the machine PATH, terminal tabs): the sandbox's own commands
37
+ * folder first, because its `claude` / `codex` shims carry the sandbox's flags and must beat a copy installed with
38
+ * `npm i -g` in nvm's node folder; then the toolchains' folders, the process's own and the appended ones. No
39
+ * duplicates (matched like pathKey); MSVC's tools before Git's usr\bin.
40
+ */
41
+ export function sessionPath(o) {
42
+ const seen = new Set();
43
+ const result = [];
44
+ for (const folder of [o.commands, ...o.prepend, ...o.base, ...o.append]) {
45
+ const key = pathKey(folder);
46
+ if (!folder.trim() || seen.has(key))
47
+ continue;
48
+ seen.add(key);
49
+ result.push(folder);
50
+ }
51
+ return msvcBeforeGitUsr(result);
52
+ }
35
53
  /** `base` with the folders of `extra` it lacks added at the end (a process keeps its own order and entries). */
36
54
  export function appendMissing(base, extra, skip = () => false) {
37
55
  const seen = new Set(splitPath(base).map(pathKey));
@@ -1,7 +1,8 @@
1
1
  // Unreal Engine development (toolchains.unreal): the DirectX End-User Runtime the engine lists (a real installer: it
2
2
  // runs every boot), then the engines (engines/unreal-engines.ts: the launcher's install folder on the data disk, PC
3
- // engines from pcInstalls, the derived data cache). Visual Studio Community with the C++ game workload is its own step
4
- // (visual-studio.ts), which the plan runs first. The engine itself comes from the Epic Games Launcher or its source.
3
+ // engines from pcInstalls, the derived data cache). Visual Studio (Community with the C++ game workload, or the Build
4
+ // Tools) is its own step (visual-studio.ts), which the plan runs first. The engine itself comes from the Epic Games
5
+ // Launcher or its source.
5
6
  import os from 'node:os';
6
7
  import path from 'node:path';
7
8
  import { runInstaller } from '../../runtime/pools.js';
@@ -12,7 +13,6 @@ import { log } from '../../runtime/log.js';
12
13
  import { setupUnrealEngines } from '../engines/unreal-engines.js';
13
14
  import { SIGNERS, signedInstaller } from '../installers.js';
14
15
  import { tool } from '../tool-env.js';
15
- import { setupAutosdk } from './autosdk.js';
16
16
  import { installedVs } from './visual-studio.js';
17
17
  export { UNREAL_COMPONENTS } from './vs-request.js';
18
18
  const DIRECTX_URL = 'https://download.microsoft.com/download/8/4/A/84A35BF1-DAFE-4AE8-82AF-AD2AE20B6B14/directx_Jun2010_redist.exe';
@@ -50,11 +50,9 @@ export async function setupUnreal(env, config, ctx) {
50
50
  return;
51
51
  }
52
52
  await installDirectX();
53
- if (config.visualStudio === 'autosdk')
54
- await setupAutosdk(env);
55
53
  await setupUnrealEngines(env, config, ctx);
56
54
  const vs = installedVs();
57
55
  env.summary.push('unreal: ' +
58
- (vs ? 'Visual Studio ' + vs.version + ' (C++ game workload, Windows 11 SDK, Unreal support); ' : '') +
56
+ (vs ? 'Visual Studio ' + vs.version + ' at ' + vs.path + '; ' : '') +
59
57
  'DirectX End-User Runtime; get the engine from the Epic Games Launcher (Start menu)');
60
58
  }
@@ -1,18 +1,18 @@
1
1
  import { type ToolEnv } from '../tool-env.js';
2
2
  import { type VsRequest } from './vs-request.js';
3
- export { BUILD_TOOLS_COMPONENTS, UNREAL_COMPONENTS, unrealUsesAutosdk, visualStudioWanted, type VsRequest, } from './vs-request.js';
4
- /** The newest Visual Studio (or Build Tools) with the C++ compiler: its folder and version, or null. */
5
- export declare function installedVs(): {
6
- path: string;
7
- version: string;
8
- } | null;
9
- /** Downloads the bootstrapper now, so the install later starts without waiting for it. */
3
+ export { BUILD_TOOLS_COMPONENTS, UNREAL_BUILD_TOOLS_COMPONENTS, UNREAL_COMPONENTS, visualStudioWanted, type VsRequest, } from './vs-request.js';
4
+ export interface InstalledVs {
5
+ readonly path: string;
6
+ readonly version: string;
7
+ readonly instanceId: string;
8
+ }
9
+ /** The newest Visual Studio (or Build Tools) with the C++ compiler, installed or put back: where it is, or null. */
10
+ export declare function installedVs(): InstalledVs | null;
11
+ /** Whether a later boot only puts Visual Studio back (quick enough to wait for) instead of installing it. */
12
+ export declare function visualStudioSaved(req: VsRequest): boolean;
13
+ /** Downloads the bootstrapper now, so the install later starts without waiting (not needed with a saved copy). */
10
14
  export declare function prefetchVisualStudio(req: VsRequest): Promise<void>;
11
- /** Visual Studio as asked for: installed at boot when it is not there yet; its folder and version. */
12
- export declare function ensureVisualStudio(env: ToolEnv, req: VsRequest, o: {
13
- keepInstallFiles: boolean;
15
+ /** Visual Studio as asked for: put back from the data disk, or installed (and saved) the first time. */
16
+ export declare function ensureVisualStudio(env: ToolEnv, req: VsRequest, o?: {
14
17
  lowPriority?: boolean;
15
- }): Promise<{
16
- path: string;
17
- version: string;
18
- }>;
18
+ }): Promise<InstalledVs>;