windows-development-sandbox 1.23.2 → 1.25.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (211) hide show
  1. package/config.example.json +5 -3
  2. package/dist/app/features/data/data.def.d.ts +4 -4
  3. package/dist/app/features/data/data.run.d.ts +1 -1
  4. package/dist/app/features/defs.d.ts +161 -6
  5. package/dist/app/features/defs.js +7 -0
  6. package/dist/app/features/projects/projects.def.d.ts +1 -1
  7. package/dist/app/features/projects/projects.run.d.ts +1 -1
  8. package/dist/app/features/unreal/index.d.ts +2 -0
  9. package/dist/app/features/unreal/index.js +3 -0
  10. package/dist/app/features/unreal/unreal.def.d.ts +236 -0
  11. package/dist/app/features/unreal/unreal.def.js +379 -0
  12. package/dist/app/features/unreal/unreal.run.d.ts +96 -0
  13. package/dist/app/features/unreal/unreal.run.js +217 -0
  14. package/dist/app/registry.js +5 -0
  15. package/dist/bin/agent-sandbox.js +2 -2
  16. package/dist/bin/{chunk-7YPEDAJD.js → chunk-3GAXLATK.js} +58 -5
  17. package/dist/bin/{chunk-USTRSHIE.js → chunk-F4PVBN23.js} +2615 -1515
  18. package/dist/bin/{chunk-QFSJX2KW.js → chunk-G3TG4ZT7.js} +1 -1
  19. package/dist/bin/claude-host.js +2 -2
  20. package/dist/bin/claude-sbx.js +2 -2
  21. package/dist/bin/codex-sbx.js +2 -2
  22. package/dist/bin/gemini-sbx.js +2 -2
  23. package/dist/bin/sandbox.js +2 -2
  24. package/dist/bin/sbx-hide.js +2 -2
  25. package/dist/bin/sbx-show.js +2 -2
  26. package/dist/cli/commands/launch/launch.js +5 -2
  27. package/dist/cli/commands/registry.js +3 -0
  28. package/dist/cli/manifest.d.ts +18 -0
  29. package/dist/cli/manifest.js +26 -0
  30. package/dist/cli/menu/coverage-exempt.js +1 -0
  31. package/dist/cli/menu/pages/sandbox-page.d.ts +3 -0
  32. package/dist/cli/menu/pages/sandbox-page.js +34 -4
  33. package/dist/cli/menu/views.js +2 -0
  34. package/dist/core/config/defaults.js +18 -5
  35. package/dist/core/detect/toolchains.js +0 -6
  36. package/dist/core/schema/config.d.ts +10 -9
  37. package/dist/core/schema/pc-installs.d.ts +3 -1
  38. package/dist/core/schema/pc-installs.js +10 -1
  39. package/dist/core/schema/toolchains.d.ts +1 -2
  40. package/dist/core/schema/toolchains.js +4 -7
  41. package/dist/guest/accounts/kinds/epic.js +18 -13
  42. package/dist/guest/agents/agent-files.d.ts +11 -7
  43. package/dist/guest/agents/agent-files.js +37 -11
  44. package/dist/guest/agents/local-state.d.ts +2 -1
  45. package/dist/guest/agents/local-state.js +2 -1
  46. package/dist/guest/apps/after-install.d.ts +1 -1
  47. package/dist/guest/apps/after-install.js +6 -1
  48. package/dist/guest/apps/app-installer.d.ts +1 -0
  49. package/dist/guest/apps/app-installer.js +13 -3
  50. package/dist/guest/apps/comet-unpack.js +3 -2
  51. package/dist/guest/apps/resolvers-gamedev.js +55 -1
  52. package/dist/guest/boot/boot-console.d.ts +28 -0
  53. package/dist/guest/boot/boot-console.js +98 -0
  54. package/dist/guest/boot/boot.d.ts +8 -1
  55. package/dist/guest/boot/boot.js +80 -43
  56. package/dist/guest/boot/graph.d.ts +9 -5
  57. package/dist/guest/boot/graph.js +8 -2
  58. package/dist/guest/boot/guide.js +24 -0
  59. package/dist/guest/boot/progress-view.d.ts +116 -0
  60. package/dist/guest/boot/progress-view.js +456 -0
  61. package/dist/guest/boot/runner.d.ts +20 -10
  62. package/dist/guest/boot/runner.js +39 -47
  63. package/dist/guest/daemon/daemon.js +3 -0
  64. package/dist/guest/runtime/log-line.d.ts +7 -0
  65. package/dist/guest/runtime/log-line.js +18 -0
  66. package/dist/guest/runtime/log.d.ts +17 -4
  67. package/dist/guest/runtime/log.js +40 -17
  68. package/dist/guest/runtime/process.d.ts +9 -0
  69. package/dist/guest/runtime/process.js +69 -8
  70. package/dist/guest/setup/addon-mcp.d.ts +7 -0
  71. package/dist/guest/setup/addon-mcp.js +19 -4
  72. package/dist/guest/setup/browser-gpu.js +14 -11
  73. package/dist/guest/setup/environment.js +9 -4
  74. package/dist/guest/setup/headroom.js +4 -0
  75. package/dist/guest/setup/hooks.d.ts +2 -0
  76. package/dist/guest/setup/hooks.js +4 -0
  77. package/dist/guest/setup/path-plan.d.ts +12 -0
  78. package/dist/guest/setup/path-plan.js +18 -0
  79. package/dist/guest/toolchains/engines/epic-launcher.d.ts +26 -0
  80. package/dist/guest/toolchains/engines/epic-launcher.js +127 -0
  81. package/dist/guest/toolchains/engines/unreal-engines.d.ts +21 -1
  82. package/dist/guest/toolchains/engines/unreal-engines.js +163 -3
  83. package/dist/guest/toolchains/engines/unreal-status.d.ts +13 -0
  84. package/dist/guest/toolchains/engines/unreal-status.js +41 -0
  85. package/dist/guest/toolchains/native/debuggers.d.ts +6 -0
  86. package/dist/guest/toolchains/native/debuggers.js +57 -0
  87. package/dist/guest/toolchains/native/mesa.js +15 -8
  88. package/dist/guest/toolchains/native/rust-extras.d.ts +7 -0
  89. package/dist/guest/toolchains/native/rust-extras.js +23 -1
  90. package/dist/guest/toolchains/native/unreal.d.ts +2 -2
  91. package/dist/guest/toolchains/native/unreal.js +96 -20
  92. package/dist/guest/toolchains/native/visual-studio.d.ts +14 -14
  93. package/dist/guest/toolchains/native/visual-studio.js +145 -174
  94. package/dist/guest/toolchains/native/vs-persist.d.ts +189 -0
  95. package/dist/guest/toolchains/native/vs-persist.js +256 -0
  96. package/dist/guest/toolchains/native/vs-request.d.ts +6 -26
  97. package/dist/guest/toolchains/native/vs-request.js +16 -50
  98. package/dist/guest/toolchains/plan.js +23 -15
  99. package/dist/guest/toolchains/tools/vscode-profile.js +2 -0
  100. package/dist/host/accounts/kinds/epic.js +11 -5
  101. package/dist/host/accounts/source.d.ts +2 -0
  102. package/dist/host/accounts/source.js +1 -0
  103. package/dist/host/migrate/migrations/drop-vs-install-files.d.ts +5 -0
  104. package/dist/host/migrate/migrations/drop-vs-install-files.js +34 -0
  105. package/dist/host/migrate/registry.js +7 -1
  106. package/dist/host/pc-installs/discover.d.ts +2 -0
  107. package/dist/host/pc-installs/discover.js +25 -5
  108. package/dist/host/pc-installs/epic.d.ts +3 -0
  109. package/dist/host/pc-installs/epic.js +33 -2
  110. package/dist/host/pc-installs/source.d.ts +1 -1
  111. package/dist/host/pc-installs/unreal-vault.d.ts +11 -0
  112. package/dist/host/pc-installs/unreal-vault.js +78 -0
  113. package/dist/host/pc-installs/unreal.d.ts +36 -0
  114. package/dist/host/pc-installs/unreal.js +145 -2
  115. package/dist/host/profile/claude-profile.d.ts +8 -0
  116. package/dist/host/profile/claude-profile.js +43 -4
  117. package/dist/host/profile/index.d.ts +1 -1
  118. package/dist/host/profile/index.js +1 -1
  119. package/dist/runtime/agent.js +148 -33
  120. package/dist/runtime/daemon.js +3146 -1546
  121. package/dist/runtime/db.js +148 -33
  122. package/dist/runtime/main.js +3982 -2049
  123. package/dist/runtime/netproxy.js +84 -13
  124. package/dist/runtime/tool.js +125 -31
  125. package/dist/runtime/view.js +85 -14
  126. package/dist/selftest/agent-paths.test.js +18 -1
  127. package/dist/selftest/agent-settings.test.js +36 -5
  128. package/dist/selftest/agents-web.test.js +12 -0
  129. package/dist/selftest/boot-console.test.d.ts +2 -0
  130. package/dist/selftest/boot-console.test.js +291 -0
  131. package/dist/selftest/boot-runner.test.js +10 -12
  132. package/dist/selftest/engines.test.js +602 -3
  133. package/dist/selftest/guest-boot.test.js +7 -1
  134. package/dist/selftest/heavy-load.test.js +3 -1
  135. package/dist/selftest/hostile-input.test.js +11 -1
  136. package/dist/selftest/index.js +4 -0
  137. package/dist/selftest/layout.test.js +4 -0
  138. package/dist/selftest/menu-screens.js +3 -1
  139. package/dist/selftest/migrate.test.js +21 -0
  140. package/dist/selftest/mixamo.test.d.ts +2 -0
  141. package/dist/selftest/mixamo.test.js +60 -0
  142. package/dist/selftest/persisted-installs.test.js +169 -33
  143. package/dist/selftest/secrets.test.js +36 -1
  144. package/dist/selftest/support/engine-golden.js +5 -0
  145. package/dist/selftest/support/fixtures.d.ts +4 -3
  146. package/dist/selftest/support/unreal-sample.d.ts +2 -0
  147. package/dist/selftest/support/unreal-sample.js +83 -0
  148. package/dist/selftest/toolchains.test.js +32 -2
  149. package/dist/shared/accounts/index.d.ts +1 -1
  150. package/dist/shared/accounts/index.js +1 -1
  151. package/dist/shared/accounts/kinds/epic.d.ts +6 -1
  152. package/dist/shared/accounts/kinds/epic.js +11 -1
  153. package/dist/shared/apps/associations.d.ts +7 -0
  154. package/dist/shared/apps/associations.js +26 -0
  155. package/dist/shared/apps/catalogue-agents-web.d.ts +4 -1
  156. package/dist/shared/apps/catalogue-agents-web.js +5 -2
  157. package/dist/shared/apps/catalogue-gamedev.js +71 -1
  158. package/dist/shared/apps/index.d.ts +2 -0
  159. package/dist/shared/apps/index.js +2 -0
  160. package/dist/shared/apps/preview.d.ts +2 -0
  161. package/dist/shared/apps/preview.js +10 -0
  162. package/dist/shared/browser-policy.d.ts +14 -0
  163. package/dist/shared/browser-policy.js +22 -0
  164. package/dist/shared/channel/schema.d.ts +3 -6
  165. package/dist/shared/io/index.d.ts +1 -0
  166. package/dist/shared/io/index.js +1 -0
  167. package/dist/shared/io/rotate-log.d.ts +2 -0
  168. package/dist/shared/io/rotate-log.js +16 -0
  169. package/dist/shared/memory/policy.d.ts +5 -0
  170. package/dist/shared/memory/policy.js +7 -0
  171. package/dist/shared/paths/pc-installs.d.ts +13 -2
  172. package/dist/shared/paths/pc-installs.js +1 -0
  173. package/dist/shared/pty/terminal.js +2 -1
  174. package/dist/shared/readiness.d.ts +0 -1
  175. package/dist/shared/readiness.js +0 -1
  176. package/dist/shared/signer.d.ts +4 -0
  177. package/dist/shared/signer.js +2 -0
  178. package/dist/shared/unreal/engines.d.ts +161 -0
  179. package/dist/shared/unreal/engines.js +308 -0
  180. package/dist/shared/unreal/guide.d.ts +19 -0
  181. package/dist/shared/unreal/guide.js +105 -0
  182. package/dist/shared/unreal/index.d.ts +7 -0
  183. package/dist/shared/unreal/index.js +8 -0
  184. package/dist/shared/unreal/launcher-files.d.ts +88 -0
  185. package/dist/shared/unreal/launcher-files.js +292 -0
  186. package/dist/shared/unreal/mixamo.d.ts +5 -0
  187. package/dist/shared/unreal/mixamo.js +16 -0
  188. package/dist/shared/unreal/room.d.ts +17 -0
  189. package/dist/shared/unreal/room.js +34 -0
  190. package/dist/shared/unreal/status.d.ts +54 -0
  191. package/dist/shared/unreal/status.js +57 -0
  192. package/dist/shared/unreal/vault.d.ts +21 -0
  193. package/dist/shared/unreal/vault.js +67 -0
  194. package/dist/shared/unreal/vs-check.d.ts +39 -0
  195. package/dist/shared/unreal/vs-check.js +79 -0
  196. package/dist/sync/watcher.js +5 -1
  197. package/dist/web/index.html +1 -1
  198. package/dist/web/{main-CCILLX2M.js → main-FNE6XEHA.js} +84 -84
  199. package/native/csharp/HcsHost/AssemblyInfo.cs +3 -3
  200. package/native/csharp/HostTools/AssemblyInfo.cs +3 -3
  201. package/native/csharp/StorageHelper/AssemblyInfo.cs +3 -3
  202. package/package.json +1 -1
  203. package/sandbox/csharp/VsSetup.cs +210 -0
  204. package/sandbox/scripts/get-vs-setup.ps1 +93 -0
  205. package/sandbox/scripts/restore-visual-studio.ps1 +78 -0
  206. package/sandbox/scripts/save-visual-studio.ps1 +106 -0
  207. package/sandbox/scripts/set-registry-values.ps1 +35 -0
  208. package/schemas/config.schema.json +38 -9
  209. package/schemas/sandbox-config.schema.json +11 -8
  210. package/dist/guest/toolchains/native/autosdk.d.ts +0 -3
  211. package/dist/guest/toolchains/native/autosdk.js +0 -50
@@ -63,10 +63,17 @@ export function bootResetPaths(agents, stateDir) {
63
63
  }
64
64
  /** The marker the sandbox puts above the lines it writes into config.toml. */
65
65
  const CODEX_MARKER = '# Set by the sandbox';
66
+ /** The full marker line: what it is and where to change it. */
67
+ const CODEX_MARKER_LINE = CODEX_MARKER + ' from its codex settings (`sandbox config`); a line of your own without it wins.';
68
+ /** Markers whose lines count as the sandbox's: CODEX_MARKER, and a hand-made fix's (`# agent-sandbox:`). */
69
+ const CODEX_MARKERS = [CODEX_MARKER, '# agent-sandbox:'];
70
+ /** The top-level keys the sandbox writes under its marker. */
71
+ const CODEX_OWNED_KEYS = ['sandbox_mode', 'approval_policy', 'model', 'model_reasoning_effort'];
66
72
  /**
67
- * The flags every Codex start gets. Flags beat config.toml and a resumed thread's saved permissions, which is why the
68
- * sandbox does not rely on the file. Full access with no questions is the one flag that skips Codex's Windows sandbox
69
- * setup for certain. Pure.
73
+ * The flags every Codex start through the sandbox gets (the codex.cmd shim, the daemon's sessions). Flags beat
74
+ * config.toml and a resumed thread's saved permissions; config.toml carries the same settings for a start without
75
+ * them (codexConfigOwned). Full access with no questions is the one flag that skips Codex's Windows sandbox setup for
76
+ * certain. Pure.
70
77
  */
71
78
  export function codexArguments(c) {
72
79
  const out = c.sandboxMode === 'danger-full-access' && c.approvalPolicy === 'never'
@@ -85,10 +92,13 @@ export function agentArguments(name, harness, codex) {
85
92
  /** The largest config.toml the sandbox edits. */
86
93
  export const MAX_CODEX_CONFIG_BYTES = 1024 * 1024;
87
94
  /**
88
- * Codex's config.toml as the sandbox wants it, every other line byte for byte: the lines an older version wrote above
89
- * the first table (sandbox_mode, approval_policy, under the marker comment) are gone, `[windows] sandbox` follows
90
- * windowsSandbox ("off" removes the line, so Codex does not set up its own sandbox users), and
91
- * mcp_oauth_credentials_store = "file" is added when it is not set. Running it again changes nothing. Pure.
95
+ * Codex's config.toml as the sandbox wants it, every other line byte for byte. Above the first table, under the
96
+ * marker comment: sandbox_mode, approval_policy (and model, model_reasoning_effort when set), so ANY start of Codex
97
+ * obeys the codex settings, also one without the shim's flags (a copy installed with npm -g). The marked lines are
98
+ * replaced each time; a key the user set on a line of their own above the first table is left to them.
99
+ * `[windows] sandbox` follows windowsSandbox ("off" removes the line, which Codex adds back after its own setup, so it
100
+ * does not set up its own sandbox users), and mcp_oauth_credentials_store = "file" is added when it is not set.
101
+ * Keeps the file's line endings; running it again changes nothing. Pure.
92
102
  */
93
103
  export function codexConfigOwned(toml, c) {
94
104
  const eol = toml.includes('\r\n') ? '\r\n' : '\n';
@@ -96,15 +106,31 @@ export function codexConfigOwned(toml, c) {
96
106
  let top = firstTable < 0 ? toml : toml.slice(0, firstTable);
97
107
  const tables = firstTable < 0 ? '' : toml.slice(firstTable);
98
108
  top = top.replace(MARKED_LINES, '');
99
- if (c.mcpLoginsInFile && !/^[ \t]*mcp_oauth_credentials_store[ \t]*=/m.test(top)) {
109
+ if (c.mcpLoginsInFile && !hasKey(top, 'mcp_oauth_credentials_store')) {
100
110
  if (top && !top.endsWith('\n'))
101
111
  top += eol;
102
112
  top += 'mcp_oauth_credentials_store = "file"' + eol + (tables ? eol : '');
103
113
  }
104
- return top + codexWindowsSandbox(tables, c.windowsSandbox, eol);
114
+ const values = {
115
+ sandbox_mode: c.sandboxMode,
116
+ approval_policy: c.approvalPolicy,
117
+ model: c.model,
118
+ model_reasoning_effort: c.reasoningEffort === 'default' ? '' : c.reasoningEffort,
119
+ };
120
+ const lines = CODEX_OWNED_KEYS.filter((k) => values[k] && !hasKey(top, k)).map((k) => k + ' = ' + JSON.stringify(values[k]));
121
+ const marked = lines.length ? [CODEX_MARKER_LINE, ...lines].join(eol) + eol + (top || tables ? eol : '') : '';
122
+ return marked + top + codexWindowsSandbox(tables, c.windowsSandbox, eol);
123
+ }
124
+ /** True when the top of config.toml sets the key on a line of its own. */
125
+ function hasKey(top, key) {
126
+ return new RegExp('^[ \t]*' + key + '[ \t]*=', 'm').test(top);
105
127
  }
106
- /** The lines an older version wrote above the first table: its marker comment, then sandbox_mode / approval_policy. */
107
- const MARKED_LINES = new RegExp('^' + CODEX_MARKER + '[^\r\n]*\r?\n(?:(?:sandbox_mode|approval_policy)[ \t]*=[^\r\n]*\r?\n)*(?:[ \t]*\r?\n)?', 'm');
128
+ /** The lines the sandbox wrote above the first table: a marker comment, the keys it owns, then one blank line. */
129
+ const MARKED_LINES = new RegExp('^(?:' +
130
+ CODEX_MARKERS.join('|') +
131
+ ')[^\r\n]*\r?\n(?:(?:' +
132
+ CODEX_OWNED_KEYS.join('|') +
133
+ ')[ \t]*=[^\r\n]*(?:\r?\n|$))*(?:[ \t]*\r?\n)?', 'm');
108
134
  /** The tables of config.toml (from the first one on) with `[windows] sandbox` set to the mode, or removed for "off". */
109
135
  function codexWindowsSandbox(tables, mode, eol) {
110
136
  const setting = 'sandbox = "' + mode + '"';
@@ -7,7 +7,8 @@ export declare function localStateVars(): Record<string, string>;
7
7
  export declare function resetMachineBound(): void;
8
8
  /**
9
9
  * Boot: Codex's config.toml follows the codex settings (codexConfigOwned): no Windows sandbox setup of its own, its MCP
10
- * logins in a file. The sandbox mode itself travels as launch flags (codexArguments), which beat this file.
10
+ * logins in a file, the sandbox mode and approval policy for a start without the launch flags (codexArguments, which
11
+ * beat this file and still go on every start through the sandbox).
11
12
  */
12
13
  export declare function codexBootConfig(): void;
13
14
  /** Boot: the saved databases of every configured agent are copied to the sandbox's disk, where the agent runs them. */
@@ -104,7 +104,8 @@ export function resetMachineBound() {
104
104
  }
105
105
  /**
106
106
  * Boot: Codex's config.toml follows the codex settings (codexConfigOwned): no Windows sandbox setup of its own, its MCP
107
- * logins in a file. The sandbox mode itself travels as launch flags (codexArguments), which beat this file.
107
+ * logins in a file, the sandbox mode and approval policy for a start without the launch flags (codexArguments, which
108
+ * beat this file and still go on every start through the sandbox).
108
109
  */
109
110
  export function codexBootConfig() {
110
111
  if (dryRun || !session.agents?.codex || !session.codex)
@@ -1,3 +1,3 @@
1
- import type { AppEntry } from '../../shared/apps/index.js';
1
+ import { type AppEntry } from '../../shared/apps/index.js';
2
2
  /** Settings an app needs on every boot (the sandbox's registry is new each time), and files next to its program. */
3
3
  export declare function afterInstall(entry: AppEntry, exe: string): Promise<void>;
@@ -1,11 +1,12 @@
1
1
  import fs from 'node:fs';
2
2
  import path from 'node:path';
3
+ import { associationPlan } from '../../shared/apps/index.js';
3
4
  import { SYSTEM } from '../../shared/system.js';
4
5
  import { errMsg } from '../../core/util/errors.js';
5
6
  import { takePendingComet } from '../accounts/kinds/comet.js';
6
7
  import { restoreCometSignIn } from '../browser/comet.js';
7
8
  import { log, warn } from '../runtime/log.js';
8
- import { execAsync } from '../runtime/process.js';
9
+ import { execAsync, runScriptWithJsonAsync } from '../runtime/process.js';
9
10
  import { exists, mkdirp } from '../runtime/fs.js';
10
11
  import { arch, tool } from '../toolchains/tool-env.js';
11
12
  /** The Sysinternals tools whose EULA is accepted up front (each asks once otherwise). */
@@ -58,6 +59,10 @@ export async function afterInstall(entry, exe) {
58
59
  mkdirp(path.join(dir, 'Data'));
59
60
  if (entry.id === 'comet')
60
61
  await restoreComet(exe);
62
+ const plan = associationPlan(entry.id, exe);
63
+ if (plan.length) {
64
+ await runScriptWithJsonAsync('set-registry-values.ps1', 'PlanFile', { values: plan }).catch((e) => warn(entry.name + ': double-click file types were not set (' + errMsg(e) + ')'));
65
+ }
61
66
  if (entry.id === 'sysinternals') {
62
67
  for (const name of SYSINTERNALS_TOOLS) {
63
68
  await execAsync(SYSTEM.reg(), ['add', 'HKCU\\Software\\Sysinternals\\' + name, '/v', 'EulaAccepted', '/t', 'REG_DWORD', '/d', '1', '/f'], { stdio: 'ignore' }).catch(() => undefined);
@@ -13,6 +13,7 @@ export declare class AppInstaller {
13
13
  private readonly parallel;
14
14
  private readonly gui;
15
15
  private readonly installed;
16
+ private readonly installedIds;
16
17
  constructor(env: ToolEnv, options: AppsOptions, parallel?: number);
17
18
  /**
18
19
  * Downloads the plain downloads (not pinned, not npm, not remote) of the apps that are not installed yet, into
@@ -2,7 +2,7 @@
2
2
  // failing is reported and never stops the others.
3
3
  import fs from 'node:fs';
4
4
  import path from 'node:path';
5
- import { appsToInstall } from '../../shared/apps/index.js';
5
+ import { appsToInstall, previewGuideLine } from '../../shared/apps/index.js';
6
6
  import { downloader, moveIntoPlace, stagingFolder } from '../../shared/download/index.js';
7
7
  import { safeRemove } from '../../shared/io/safe-remove.js';
8
8
  import { dryRun, launchDir } from '../runtime/paths.js';
@@ -33,6 +33,7 @@ export class AppInstaller {
33
33
  parallel;
34
34
  gui = [];
35
35
  installed = [];
36
+ installedIds = [];
36
37
  constructor(env, options, parallel = PARALLEL) {
37
38
  this.env = env;
38
39
  this.options = options;
@@ -82,6 +83,9 @@ export class AppInstaller {
82
83
  }
83
84
  if (this.installed.length)
84
85
  this.env.summary.push('apps: ' + this.installed.join(', ') + ' (shortcuts on the desktop and in Start > Sandbox apps)');
86
+ const preview = previewGuideLine(this.installedIds);
87
+ if (preview)
88
+ this.env.summary.push(preview);
85
89
  }
86
90
  async installOne(entry) {
87
91
  try {
@@ -102,11 +106,17 @@ export class AppInstaller {
102
106
  throw new Error(entry.exe + ' not found in the download');
103
107
  await afterInstall(entry, exe);
104
108
  // A proxy or MCP server (agent add-ons) is started by the sandbox or the agent: no PATH entry, no shortcut.
105
- if (!entry.service && (entry.cli || entry.id === 'sysinternals'))
106
- this.env.prepend.push(path.dirname(exe));
109
+ if (!entry.service && (entry.cli || entry.id === 'sysinternals')) {
110
+ // rustup's rust-analyzer proxy (in cargo's bin folder) only errors until its component is added.
111
+ if (entry.id === 'rust-analyzer')
112
+ this.env.prepend.unshift(path.dirname(exe));
113
+ else
114
+ this.env.prepend.push(path.dirname(exe));
115
+ }
107
116
  if (!entry.cli && !entry.service)
108
117
  this.gui.push({ name: entry.name, exe, ...this.launchArgs(entry) });
109
118
  this.installed.push(entry.name);
119
+ this.installedIds.push(entry.id);
110
120
  }
111
121
  catch (e) {
112
122
  warn(entry.name + ' could not be installed and is skipped; the other apps are not affected. Reason: ' + errMsg(e));
@@ -15,10 +15,11 @@ const INNER = [
15
15
  /^chrome\.7z$/i,
16
16
  /^chrome\.packed\.7z$/i,
17
17
  /^mini_installer\.exe$/i,
18
- /^updater\.packed\.7z$/i,
18
+ /^updater\.(packed\.)?7z$/i,
19
19
  ];
20
20
  const MAX_LEVELS = 6;
21
- const MAX_DEPTH = 4;
21
+ /** Comet's installer nests mini_installer.exe five folders down (bin\Offline\{id}\{id}). */
22
+ const MAX_DEPTH = 7;
22
23
  const MAX_ENTRIES = 20000;
23
24
  /** Where the installer is kept when it could not be unpacked (below the app's folder). */
24
25
  export const KEPT_INSTALLER_DIR = 'installer';
@@ -1 +1,55 @@
1
- export const GAMEDEV_RESOLVERS = {};
1
+ // Where the latest version of the game-development apps (catalogue-gamedev.ts) is, when it is not a plain GitHub
2
+ // release: Krita (download.kde.org, with the SHA-256 its mirror publishes beside each file), Inkscape (inkscape.org's
3
+ // portable 7z and its .sha256) and RenderDoc (renderdoc.org's portable zip; it publishes no checksum, the download is
4
+ // HTTPS from renderdoc.org).
5
+ import { fetchText } from '../../shared/download/index.js';
6
+ const firstHash = (text) => /\b([0-9a-f]{64})\b/i.exec(text)?.[1].toLowerCase() ?? null;
7
+ const byVersion = (a, b) => {
8
+ const x = a.split('.').map(Number);
9
+ const y = b.split('.').map(Number);
10
+ for (let i = 0; i < Math.max(x.length, y.length); i++)
11
+ if ((x[i] ?? 0) !== (y[i] ?? 0))
12
+ return (x[i] ?? 0) - (y[i] ?? 0);
13
+ return 0;
14
+ };
15
+ /** The newest Krita release folder that has the portable Windows zip (a hotfix folder may have only some files). */
16
+ async function krita() {
17
+ const base = 'https://download.kde.org/stable/krita/';
18
+ const versions = [...(await fetchText(base)).matchAll(/href="(\d+\.\d+\.\d+(?:\.\d+)?)\/"/g)]
19
+ .map((m) => m[1])
20
+ .sort(byVersion)
21
+ .reverse()
22
+ .slice(0, 4);
23
+ for (const v of versions) {
24
+ const file = 'krita-x64-' + v + '.zip';
25
+ if (!(await fetchText(base + v + '/')).includes('href="' + file + '"'))
26
+ continue;
27
+ const url = base + v + '/' + file;
28
+ return { url, fileName: file, sha256: firstHash(await fetchText(url + '.sha256').catch(() => '')) };
29
+ }
30
+ throw new Error('no portable Krita zip on download.kde.org');
31
+ }
32
+ async function inkscape() {
33
+ const release = await fetchText('https://inkscape.org/release/');
34
+ const v = /\/release\/inkscape-(\d+\.\d+(?:\.\d+)?)\//.exec(release)?.[1];
35
+ if (!v)
36
+ throw new Error('could not find the latest Inkscape release');
37
+ const page = await fetchText('https://inkscape.org/release/inkscape-' + v + '/windows/64-bit/compressed-7z/dl/');
38
+ const item = /\/gallery\/item\/\d+\/inkscape-[\w.-]+-x64[\w-]*\.7z/.exec(page);
39
+ const sums = /https:\/\/media\.inkscape\.org\/media\/resources\/sigs\/[\w.-]+\.sha256/.exec(page)?.[0];
40
+ if (!item)
41
+ throw new Error('no portable Inkscape 7z for ' + v);
42
+ return {
43
+ url: 'https://inkscape.org' + item[0],
44
+ fileName: 'inkscape-' + v + '-x64.7z',
45
+ sha256: sums ? firstHash(await fetchText(sums).catch(() => '')) : null,
46
+ };
47
+ }
48
+ async function renderdoc() {
49
+ const page = await fetchText('https://renderdoc.org/builds');
50
+ const hit = /https:\/\/renderdoc\.org\/stable\/([\d.]+)\/RenderDoc_[\d.]+_64\.zip/.exec(page);
51
+ if (!hit)
52
+ throw new Error('could not find the RenderDoc download on renderdoc.org');
53
+ return { url: hit[0], fileName: 'RenderDoc_' + hit[1] + '_64.zip' };
54
+ }
55
+ export const GAMEDEV_RESOLVERS = { krita, inkscape, renderdoc };
@@ -0,0 +1,28 @@
1
+ import { type ReadyInfo, type ViewStage, type ViewTask } from './progress-view.js';
2
+ import type { BootTaskDef, RunnerObserver, TaskOutcome } from './runner.js';
3
+ export declare class BootConsole implements RunnerObserver {
4
+ private readonly view;
5
+ private readonly live;
6
+ private timer;
7
+ private open;
8
+ private constructor();
9
+ /** `plain`: ordered lines even on a terminal (a dry run). */
10
+ static create(stages: readonly ViewStage[], tasks: readonly ViewTask[], o: {
11
+ plain: boolean;
12
+ }): BootConsole;
13
+ /** Prints the header and takes the console. */
14
+ start(header: string): void;
15
+ started(task: BootTaskDef): void;
16
+ finished(task: BootTaskDef, outcome: TaskOutcome): void;
17
+ /** What a running task's place on its row says (null: its title). */
18
+ detail(taskId: string, text: string | null): void;
19
+ /** The tool summary: to the file now, to the console once the boot is ready. */
20
+ summary(lines: readonly string[]): void;
21
+ /** Runs `work` that writes to the terminal itself (the hooks). */
22
+ aside<T>(work: () => T): T;
23
+ /**
24
+ * The final summary (`ready`, or null when the boot failed); the console is the log's own again. Programs' output
25
+ * keeps going to the log (the background runs quiet: file only).
26
+ */
27
+ release(ready: ReadyInfo | null): void;
28
+ }
@@ -0,0 +1,98 @@
1
+ // The boot's console on the sandbox's terminal: the live progress block when it is a terminal, ordered plain lines
2
+ // otherwise (and in a dry run). While it is open it takes the log's console lines and the output of the programs the
3
+ // boot starts; it is released, with the final summary, right before the first session takes the window.
4
+ import { log, record, routeConsole } from '../runtime/log.js';
5
+ import { routeOutput } from '../runtime/process.js';
6
+ import { CONSOLE_GLYPHS, ProgressView, readyLines, RICH_GLYPHS, } from './progress-view.js';
7
+ /** The spinner's pace (the elapsed times change every tenth of a second too). */
8
+ const FRAME_MS = 100;
9
+ const COLUMNS = 80;
10
+ const SHOW_CURSOR = '\x1b[?25h';
11
+ export class BootConsole {
12
+ view;
13
+ live;
14
+ timer = null;
15
+ open = false;
16
+ constructor(view, live) {
17
+ this.view = view;
18
+ this.live = live;
19
+ }
20
+ /** `plain`: ordered lines even on a terminal (a dry run). */
21
+ static create(stages, tasks, o) {
22
+ const live = !o.plain && process.stdout.isTTY === true;
23
+ // Windows Terminal sets WT_SESSION; the classic console's font has no braille or check marks.
24
+ const rich = live && Boolean(process.env.WT_SESSION);
25
+ const view = new ProgressView(stages, tasks, {
26
+ live,
27
+ glyphs: rich ? RICH_GLYPHS : CONSOLE_GLYPHS,
28
+ now: () => Date.now(),
29
+ screen: {
30
+ write: (text) => void process.stdout.write(text),
31
+ columns: () => process.stdout.columns ?? COLUMNS,
32
+ },
33
+ });
34
+ return new BootConsole(view, live);
35
+ }
36
+ /** Prints the header and takes the console. */
37
+ start(header) {
38
+ record(header);
39
+ this.open = true;
40
+ this.view.start(header);
41
+ routeConsole({
42
+ line: (message, stage) => this.view.line(message, stage),
43
+ warning: (message) => this.view.warning(message),
44
+ notice: (message) => this.view.notice(message),
45
+ });
46
+ // Programs the boot starts would draw over the block (or between the plain stages): their output goes to the log;
47
+ // the hooks keep the terminal. After the release too: the background steps must not write into the session.
48
+ routeOutput({ capturing: () => !this.open || this.view.capturing, line: (text) => log(text) });
49
+ if (!this.live)
50
+ return;
51
+ this.timer = setInterval(() => this.view.tick(), FRAME_MS);
52
+ this.timer.unref();
53
+ // A crash must not leave the terminal without its cursor.
54
+ process.once('exit', () => {
55
+ if (this.open)
56
+ process.stdout.write(SHOW_CURSOR);
57
+ });
58
+ }
59
+ started(task) {
60
+ this.view.started(task);
61
+ this.view.tick();
62
+ }
63
+ finished(task, outcome) {
64
+ this.view.finished(task, outcome);
65
+ this.view.tick();
66
+ }
67
+ /** What a running task's place on its row says (null: its title). */
68
+ detail(taskId, text) {
69
+ this.view.detail(taskId, text);
70
+ }
71
+ /** The tool summary: to the file now, to the console once the boot is ready. */
72
+ summary(lines) {
73
+ for (const line of lines)
74
+ record(' ' + line);
75
+ this.view.summary(lines);
76
+ }
77
+ /** Runs `work` that writes to the terminal itself (the hooks). */
78
+ aside(work) {
79
+ return this.view.aside(work);
80
+ }
81
+ /**
82
+ * The final summary (`ready`, or null when the boot failed); the console is the log's own again. Programs' output
83
+ * keeps going to the log (the background runs quiet: file only).
84
+ */
85
+ release(ready) {
86
+ if (!this.open)
87
+ return;
88
+ this.open = false;
89
+ if (this.timer)
90
+ clearInterval(this.timer);
91
+ this.timer = null;
92
+ routeConsole(null);
93
+ if (ready)
94
+ for (const line of readyLines(ready))
95
+ record(line);
96
+ this.view.release(ready);
97
+ }
98
+ }
@@ -15,6 +15,9 @@ export declare class Boot {
15
15
  private secrets;
16
16
  private basePath;
17
17
  private readonly startedAt;
18
+ private readonly who;
19
+ /** The console while the boot runs: the stage rows, warnings and summary (released before the first session). */
20
+ private readonly progress;
18
21
  private openGate;
19
22
  private readonly gate;
20
23
  /** The boot is ready: the runner no longer writes the status (the session owns it). */
@@ -24,9 +27,13 @@ export declare class Boot {
24
27
  private stepsDone;
25
28
  private stepsTotal;
26
29
  run(): Promise<void>;
30
+ /** The prelude, then every foreground task (the console shows them). */
31
+ private foreground;
27
32
  private stageTitle;
28
- /** The names shown for work that goes on after the boot is ready. */
33
+ /** The toolchain steps that go on after the boot is ready (Visual Studio and what needs it), in plan order. */
29
34
  private backgroundNames;
35
+ /** Runs a stage's hooks; they write to the terminal themselves, so the progress block steps aside for them. */
36
+ private hooks;
30
37
  /**
31
38
  * The host's one-shot file, read once. Your saved service sign-ins (accounts.<kind>.share) go to their tools before
32
39
  * anything opens them, and leave memory at once; what they give the agent (EXPO_TOKEN ...) joins its environment
@@ -15,7 +15,7 @@ import { linkProfileDirsAsync } from '../agents/profile-links.js';
15
15
  import { restoreClaudeMem } from '../setup/claude-mem.js';
16
16
  import { syncClaudeProfile } from '../setup/claude-profile.js';
17
17
  import { agentEnv, persistEnvironment } from '../setup/environment.js';
18
- import { runHooks } from '../setup/hooks.js';
18
+ import { hasHooks, runHooks } from '../setup/hooks.js';
19
19
  import { applyAllowOnly, proxyEnv } from '../network/allow-only.js';
20
20
  import { applyNetworkPolicy, networkProblems } from '../network/firewall.js';
21
21
  import { writeManagedSettings } from '../setup/managed-policy.js';
@@ -37,12 +37,14 @@ import { now } from '../../shared/channel/run-dir.js';
37
37
  import { writeJsonAtomic } from '../../shared/io/json.js';
38
38
  import { viewSession } from '../sessions/viewer.js';
39
39
  import { saveBootEnv } from './boot-env.js';
40
- import { BOOT_DEFS, STAGES, stageTitle } from './graph.js';
41
- import { BootRunner, bootSummary, seconds } from './runner.js';
40
+ import { BOOT_DEFS, STAGES, stageName, stageTitle } from './graph.js';
41
+ import { BootRunner, duration } from './runner.js';
42
+ import { BootConsole } from './boot-console.js';
43
+ import { stepsDetail } from './progress-view.js';
42
44
  import { installPools } from '../runtime/pools.js';
43
45
  import { primaryProject, saveProjectStatus } from './project-status.js';
44
46
  import { dryRun, isWin, localDir, logsDir, run, toolsDir } from '../runtime/paths.js';
45
- import { log, logPath, runQuiet, warn, withLogContext } from '../runtime/log.js';
47
+ import { log, logPath, notice, record, runQuiet, warn, withLogContext } from '../runtime/log.js';
46
48
  import { mkdirp } from '../runtime/fs.js';
47
49
  import { session, workspace } from '../runtime/session.js';
48
50
  import { applyBootEnvironment, BOOT_SECRETS_VARIABLE, takeSecrets } from '../runtime/secrets.js';
@@ -50,7 +52,20 @@ import { setBackgroundWork, setSandboxStatus } from '../runtime/status.js';
50
52
  import { errMsg } from '../../core/util/errors.js';
51
53
  const PRIMARY_POLLS = 600;
52
54
  const POLL_MS = 500;
53
- const VSCODE_LANGUAGES = ['python', 'java', 'go', 'rust', 'cpp', 'msvc', 'dotnet', 'ruby', 'php', 'deno', 'pwsh'];
55
+ const VSCODE_LANGUAGES = [
56
+ 'python',
57
+ 'java',
58
+ 'go',
59
+ 'rust',
60
+ 'cpp',
61
+ 'msvc',
62
+ 'unreal',
63
+ 'dotnet',
64
+ 'ruby',
65
+ 'php',
66
+ 'deno',
67
+ 'pwsh',
68
+ ];
54
69
  function startDaemon(env, secrets) {
55
70
  spawn(process.execPath, [path.join(path.dirname(process.argv[1]), 'daemon.js')], {
56
71
  detached: true,
@@ -159,6 +174,12 @@ export class Boot {
159
174
  secrets = Object.freeze({});
160
175
  basePath = '';
161
176
  startedAt = Date.now();
177
+ who = {
178
+ ...(session.vpn ? { vpnServer: session.vpn.server } : {}),
179
+ agent: session.harnessName,
180
+ };
181
+ /** The console while the boot runs: the stage rows, warnings and summary (released before the first session). */
182
+ progress = BootConsole.create(Array.from({ length: STAGES }, (_, i) => ({ stage: i + 1, name: stageName(i + 1, this.who) })), BOOT_DEFS.filter((d) => !d.background), { plain: dryRun });
162
183
  openGate = () => undefined;
163
184
  gate = new Promise((resolve) => {
164
185
  this.openGate = resolve;
@@ -170,14 +191,39 @@ export class Boot {
170
191
  stepsDone = 0;
171
192
  stepsTotal = 0;
172
193
  async run() {
173
- log('Agent sandbox: ' +
174
- session.project +
175
- ' | workspace ' +
176
- workspace +
177
- ' | agent ' +
178
- session.harnessName +
179
- ' | log ' +
180
- logPath);
194
+ const agent = session.harness ? session.harnessName : 'no agent';
195
+ this.progress.start('Agent sandbox · ' + session.project + ' · ' + agent + ' · ' + workspace);
196
+ let runner;
197
+ try {
198
+ runner = await this.foreground();
199
+ }
200
+ catch (e) {
201
+ this.progress.release(null);
202
+ throw e;
203
+ }
204
+ this.progress.release({
205
+ ms: Date.now() - this.startedAt,
206
+ outcomes: runner.results(),
207
+ background: this.backgroundNames(),
208
+ logPath,
209
+ });
210
+ if (dryRun) {
211
+ // Same graph, one task at a time: the background tasks run in place, right after the session.
212
+ this.openGate();
213
+ await runner.run('background');
214
+ return;
215
+ }
216
+ this.openGate();
217
+ const later = runner.run('background');
218
+ // The viewer owns the console now: the background tasks write to the log file only.
219
+ if (session.harness && session.autoSession !== false && process.stdin.isTTY) {
220
+ await viewSession(session.launchId);
221
+ log('\nThis window stays open in ' + workspace + '. Other sessions keep running in their own tabs.');
222
+ }
223
+ await later.catch((e) => warn('Background work failed: ' + errMsg(e)));
224
+ }
225
+ /** The prelude, then every foreground task (the console shows them). */
226
+ async foreground() {
181
227
  if (!fs.statSync(workspace, { throwIfNoEntry: false })?.isDirectory())
182
228
  throw new Error('Workspace folder is missing in the sandbox: ' + workspace);
183
229
  this.basePath = process.env.PATH ?? '';
@@ -189,7 +235,7 @@ export class Boot {
189
235
  prepareGithubFolder(this.secrets.githubToken);
190
236
  installPools();
191
237
  const runner = new BootRunner(BOOT_DEFS, this.tasks(), {
192
- say: (line, quiet) => (quiet ? runQuiet(() => log(line)) : log(line)),
238
+ trace: (line) => record(line),
193
239
  warn: (line, quiet) => (quiet ? runQuiet(() => warn(line)) : warn(line)),
194
240
  now: () => Date.now(),
195
241
  dryRun,
@@ -199,36 +245,26 @@ export class Boot {
199
245
  this.lastStatus = s;
200
246
  this.publish();
201
247
  },
202
- inContext: (ctx, work) => withLogContext({ prefix: ctx.prefix, quiet: ctx.quiet }, work),
248
+ observer: this.progress,
249
+ inContext: (ctx, work) => withLogContext({ stage: ctx.stage, task: ctx.title, quiet: ctx.quiet }, work),
203
250
  });
204
251
  await runner.run('foreground');
205
- const background = this.backgroundNames();
206
- log(bootSummary(Date.now() - this.startedAt, runner.results(), background, logPath));
207
- if (dryRun) {
208
- // Same graph, one task at a time: the background tasks run in place, right after the session.
209
- this.openGate();
210
- await runner.run('background');
211
- return;
212
- }
213
- this.openGate();
214
- const later = runner.run('background');
215
- // The viewer owns the console now: the background tasks write to the log file only.
216
- if (session.harness && session.autoSession !== false && process.stdin.isTTY) {
217
- await viewSession(session.launchId);
218
- log('\nThis window stays open in ' + workspace + '. Other sessions keep running in their own tabs.');
219
- }
220
- await later.catch((e) => warn('Background work failed: ' + errMsg(e)));
252
+ return runner;
221
253
  }
222
254
  stageTitle(n) {
223
- return stageTitle(n, { ...(session.vpn ? { vpnServer: session.vpn.server } : {}), agent: session.harnessName });
255
+ return stageTitle(n, this.who);
224
256
  }
225
- /** The names shown for work that goes on after the boot is ready. */
257
+ /** The toolchain steps that go on after the boot is ready (Visual Studio and what needs it), in plan order. */
226
258
  backgroundNames() {
227
259
  const t = this.tools;
228
260
  if (!t?.later)
229
261
  return [];
230
- const later = t.names.filter((n) => !(n in t.steps));
231
- return later.length ? [later.slice(0, 3).join(', ') + (later.length > 3 ? ' ...' : '')] : [];
262
+ return t.names.filter((n) => !(n in t.steps));
263
+ }
264
+ /** Runs a stage's hooks; they write to the terminal themselves, so the progress block steps aside for them. */
265
+ hooks(stage, env) {
266
+ if (hasHooks(stage))
267
+ this.progress.aside(() => runHooks(stage, env));
232
268
  }
233
269
  /**
234
270
  * The host's one-shot file, read once. Your saved service sign-ins (accounts.<kind>.share) go to their tools before
@@ -262,7 +298,7 @@ export class Boot {
262
298
  setSandboxStatus(s.phase, s.message, {
263
299
  step: s.step,
264
300
  steps: STAGES,
265
- title: this.stageTitle(s.step).replace(/ \(.*\)$/, ''),
301
+ title: stageName(s.step, this.who),
266
302
  active: [...s.running.map((name) => ({ name })), ...this.activeSteps],
267
303
  done: this.stepsDone,
268
304
  total: this.stepsTotal,
@@ -281,7 +317,7 @@ export class Boot {
281
317
  },
282
318
  audit: () => enableProcessAudit(),
283
319
  // 2 - pre-provision hooks and desktop
284
- hooksPreProvision: () => runHooks('pre-provision', process.env),
320
+ hooksPreProvision: () => this.hooks('pre-provision', process.env),
285
321
  desktop: () => applyPersonalization(),
286
322
  browserGpu: () => keepBrowsersOffGpu(),
287
323
  // 3 - developer tools
@@ -343,7 +379,7 @@ export class Boot {
343
379
  await agents.provision(session.harnessName, session.harness, this.env);
344
380
  },
345
381
  // 8 - pre-agent hooks
346
- hooksPreAgent: () => runHooks('pre-agent', this.env),
382
+ hooksPreAgent: () => this.hooks('pre-agent', this.env),
347
383
  // 9 - start
348
384
  session: () => this.start(),
349
385
  // After the first session runs
@@ -371,6 +407,7 @@ export class Boot {
371
407
  ...(session.pcInstalls ? { pcInstalls: session.pcInstalls } : {}),
372
408
  }, {
373
409
  progress: (p) => {
410
+ this.progress.detail('toolchains', stepsDetail(p.active.map((a) => a.name + (a.percent === undefined ? '' : ' ' + Math.round(a.percent) + '%')), p.done, p.total));
374
411
  this.activeSteps = p.active;
375
412
  this.stepsDone = p.done;
376
413
  this.stepsTotal = p.total;
@@ -424,8 +461,7 @@ export class Boot {
424
461
  lan: this.lan,
425
462
  });
426
463
  saveProjectStatus(primaryProject(), workspace, 'ready', 'Ready', session.databases ?? [], this.summary);
427
- for (const line of this.summary)
428
- log(' ' + line);
464
+ this.progress.summary(this.summary);
429
465
  }
430
466
  /** The toolchains' lines, then the missing protections, allowOnly and VS Code (in this order). */
431
467
  summaryLines(toolchainLines) {
@@ -441,7 +477,8 @@ export class Boot {
441
477
  const tools = this.tools;
442
478
  if (!tools?.later)
443
479
  return;
444
- const name = this.backgroundNames()[0] ?? 'Visual Studio';
480
+ const names = this.backgroundNames();
481
+ const name = names.length ? names.slice(0, 3).join(', ') + (names.length > 3 ? ' ...' : '') : 'Visual Studio';
445
482
  const began = Date.now();
446
483
  setBackgroundWork([{ name, state: 'running' }]);
447
484
  log('Background: ' + name + ' (installing)');
@@ -468,7 +505,7 @@ export class Boot {
468
505
  saveProjectStatus(primaryProject(), workspace, 'ready', 'Ready', session.databases ?? [], this.summary);
469
506
  await soft('Sandbox guide for the agent', () => writeGuides(this.summary, this.lan));
470
507
  setBackgroundWork([{ name, state: 'done' }]);
471
- log('Background done in ' + seconds(Date.now() - began) + ': ' + name);
508
+ log('Background done in ' + duration(Date.now() - began) + ': ' + name);
472
509
  }
473
510
  catch (e) {
474
511
  setBackgroundWork([{ name, state: 'failed' }]);
@@ -512,7 +549,7 @@ export class Boot {
512
549
  });
513
550
  if (!(await waitForPrimary()))
514
551
  throw new Error('The agent session did not start. Logs: ' + logsDir);
515
- log('If Claude asks you to sign in, do it once; the login is saved in persistent storage.');
552
+ notice('If Claude asks you to sign in, do it once; the login is saved in persistent storage.');
516
553
  }
517
554
  }
518
555
  export function runBoot() {