windows-development-sandbox 1.1.0 → 1.4.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 (222) hide show
  1. package/.sandbox-config.example.json +2 -1
  2. package/README.md +3 -2
  3. package/config.example.json +39 -5
  4. package/dist/cli/args.d.ts +3 -0
  5. package/dist/cli/args.js +3 -1
  6. package/dist/cli/boot-progress.d.ts +22 -0
  7. package/dist/cli/boot-progress.js +59 -0
  8. package/dist/cli/commands/accounts.js +2 -2
  9. package/dist/cli/commands/agent-commands.js +1 -1
  10. package/dist/cli/commands/context.js +2 -1
  11. package/dist/cli/commands/data/clean.js +6 -1
  12. package/dist/cli/commands/data/collect.js +3 -3
  13. package/dist/cli/commands/index.d.ts +1 -1
  14. package/dist/cli/commands/index.js +1 -1
  15. package/dist/cli/commands/launch.d.ts +5 -2
  16. package/dist/cli/commands/launch.js +113 -25
  17. package/dist/cli/commands/maintenance.d.ts +2 -0
  18. package/dist/cli/commands/maintenance.js +70 -4
  19. package/dist/cli/commands/project.d.ts +10 -6
  20. package/dist/cli/commands/project.js +42 -3
  21. package/dist/cli/commands/restart.js +17 -7
  22. package/dist/cli/commands/sessions.js +5 -4
  23. package/dist/cli/commands/user-config.js +1 -1
  24. package/dist/cli/index.js +6 -4
  25. package/dist/cli/manifest.js +6 -1
  26. package/dist/cli/menu/apps.d.ts +21 -0
  27. package/dist/cli/menu/apps.js +122 -0
  28. package/dist/cli/menu/command-form.d.ts +28 -0
  29. package/dist/cli/menu/command-form.js +219 -0
  30. package/dist/cli/menu/index.d.ts +26 -0
  31. package/dist/cli/menu/index.js +180 -0
  32. package/dist/cli/menu/settings/categories.d.ts +9 -0
  33. package/dist/cli/menu/settings/categories.js +28 -0
  34. package/dist/cli/menu/settings/config-file.d.ts +40 -0
  35. package/dist/cli/menu/settings/config-file.js +71 -0
  36. package/dist/cli/menu/settings/editors.d.ts +30 -0
  37. package/dist/cli/menu/settings/editors.js +305 -0
  38. package/dist/cli/menu/settings/index.d.ts +32 -0
  39. package/dist/cli/menu/settings/index.js +136 -0
  40. package/dist/cli/menu/settings/schema-node.d.ts +62 -0
  41. package/dist/cli/menu/settings/schema-node.js +104 -0
  42. package/dist/cli/{menu-multi.d.ts → menu/several.d.ts} +10 -2
  43. package/dist/cli/menu/several.js +102 -0
  44. package/dist/cli/progress.d.ts +15 -0
  45. package/dist/cli/progress.js +52 -0
  46. package/dist/cli/tui/app.d.ts +39 -0
  47. package/dist/cli/tui/app.js +52 -0
  48. package/dist/cli/tui/dialogs.d.ts +57 -0
  49. package/dist/cli/tui/dialogs.js +162 -0
  50. package/dist/cli/tui/frame.d.ts +18 -0
  51. package/dist/cli/tui/frame.js +27 -0
  52. package/dist/cli/tui/index.d.ts +7 -0
  53. package/dist/cli/tui/index.js +8 -0
  54. package/dist/cli/tui/list-screen.d.ts +101 -0
  55. package/dist/cli/tui/list-screen.js +299 -0
  56. package/dist/cli/tui/terminal.d.ts +37 -0
  57. package/dist/cli/tui/terminal.js +89 -0
  58. package/dist/cli/tui/text.d.ts +20 -0
  59. package/dist/cli/tui/text.js +105 -0
  60. package/dist/cli/tui/theme.d.ts +45 -0
  61. package/dist/cli/tui/theme.js +30 -0
  62. package/dist/core/config.js +10 -7
  63. package/dist/core/local-config.d.ts +31 -1
  64. package/dist/core/local-config.js +72 -7
  65. package/dist/core/project-policy.d.ts +50 -3
  66. package/dist/core/project-policy.js +90 -4
  67. package/dist/core/schema/apps.d.ts +40 -0
  68. package/dist/core/schema/apps.js +27 -0
  69. package/dist/core/schema/config.d.ts +69 -3
  70. package/dist/core/schema/config.js +18 -6
  71. package/dist/core/schema/profile.d.ts +5 -0
  72. package/dist/core/schema/profile.js +4 -2
  73. package/dist/core/schema/projects.d.ts +1 -0
  74. package/dist/core/schema/projects.js +1 -0
  75. package/dist/core/schema/sandbox.d.ts +29 -1
  76. package/dist/core/schema/sandbox.js +29 -1
  77. package/dist/core/schema/toolchains.d.ts +3 -0
  78. package/dist/core/schema/toolchains.js +6 -1
  79. package/dist/core/types.d.ts +24 -1
  80. package/dist/core/util.d.ts +2 -0
  81. package/dist/core/util.js +2 -0
  82. package/dist/core/workspace.d.ts +21 -4
  83. package/dist/core/workspace.js +99 -21
  84. package/dist/guest/apps/index.d.ts +10 -0
  85. package/dist/guest/apps/index.js +206 -0
  86. package/dist/guest/apps/resolvers.d.ts +8 -0
  87. package/dist/guest/apps/resolvers.js +90 -0
  88. package/dist/guest/audit-parse.d.ts +7 -0
  89. package/dist/guest/audit-parse.js +24 -0
  90. package/dist/guest/audit.d.ts +4 -0
  91. package/dist/guest/audit.js +47 -0
  92. package/dist/guest/daemon/relays.d.ts +1 -0
  93. package/dist/guest/daemon/relays.js +18 -0
  94. package/dist/guest/daemon.js +4 -0
  95. package/dist/guest/env.d.ts +7 -2
  96. package/dist/guest/env.js +17 -3
  97. package/dist/guest/helpers.d.ts +1 -1
  98. package/dist/guest/main.js +20 -3
  99. package/dist/guest/netproxy.d.ts +1 -0
  100. package/dist/guest/netproxy.js +100 -0
  101. package/dist/guest/ptyhost.js +14 -4
  102. package/dist/guest/setup/claude.d.ts +0 -4
  103. package/dist/guest/setup/claude.js +48 -2
  104. package/dist/guest/setup/network.d.ts +9 -0
  105. package/dist/guest/setup/network.js +97 -4
  106. package/dist/guest/setup/user.js +4 -0
  107. package/dist/guest/toolchains/index.js +29 -3
  108. package/dist/guest/toolchains/native/mesa.js +13 -3
  109. package/dist/guest/toolchains/plan.d.ts +3 -1
  110. package/dist/guest/toolchains/plan.js +53 -9
  111. package/dist/guest/toolchains/python.js +26 -2
  112. package/dist/guest/toolchains/vscode-profile.d.ts +30 -0
  113. package/dist/guest/toolchains/vscode-profile.js +69 -0
  114. package/dist/guest/toolchains/vscode.d.ts +17 -0
  115. package/dist/guest/toolchains/vscode.js +87 -1
  116. package/dist/host/attach.js +2 -1
  117. package/dist/host/audit/collect.d.ts +34 -0
  118. package/dist/host/audit/collect.js +126 -0
  119. package/dist/host/audit/log.d.ts +46 -0
  120. package/dist/host/audit/log.js +143 -0
  121. package/dist/host/channel.d.ts +2 -2
  122. package/dist/host/channel.js +3 -2
  123. package/dist/host/generate.d.ts +7 -1
  124. package/dist/host/generate.js +23 -6
  125. package/dist/host/join.d.ts +7 -0
  126. package/dist/host/join.js +23 -9
  127. package/dist/host/launch/mappings.d.ts +7 -2
  128. package/dist/host/launch/mappings.js +68 -18
  129. package/dist/host/launch/projects.d.ts +2 -0
  130. package/dist/host/launch/projects.js +11 -5
  131. package/dist/host/launch/stage.js +1 -1
  132. package/dist/host/motw.d.ts +17 -0
  133. package/dist/host/motw.js +74 -0
  134. package/dist/host/platform.js +3 -0
  135. package/dist/host/port-forward.d.ts +8 -0
  136. package/dist/host/port-forward.js +37 -0
  137. package/dist/host/projects.d.ts +25 -2
  138. package/dist/host/projects.js +54 -5
  139. package/dist/host/repo-guard.d.ts +10 -3
  140. package/dist/host/repo-guard.js +142 -14
  141. package/dist/host/stops.d.ts +67 -0
  142. package/dist/host/stops.js +126 -0
  143. package/dist/host/terminal.d.ts +8 -0
  144. package/dist/host/terminal.js +45 -1
  145. package/dist/host/workspace-view/index.d.ts +20 -0
  146. package/dist/host/workspace-view/index.js +53 -0
  147. package/dist/host/workspace-view/rules.d.ts +26 -0
  148. package/dist/host/workspace-view/rules.js +93 -0
  149. package/dist/host/workspace-view/view.d.ts +50 -0
  150. package/dist/host/workspace-view/view.js +281 -0
  151. package/dist/runtime/daemon.js +1387 -558
  152. package/dist/runtime/db.js +199 -14
  153. package/dist/runtime/main.js +3082 -631
  154. package/dist/runtime/netproxy.js +25948 -0
  155. package/dist/runtime/view.js +531 -148
  156. package/dist/selftest/audit.test.d.ts +2 -0
  157. package/dist/selftest/audit.test.js +57 -0
  158. package/dist/selftest/channel.test.js +45 -2
  159. package/dist/selftest/cli.test.js +47 -17
  160. package/dist/selftest/fixtures.d.ts +47 -1
  161. package/dist/selftest/fixtures.js +3 -1
  162. package/dist/selftest/generate.test.js +7 -2
  163. package/dist/selftest/index.js +6 -2
  164. package/dist/selftest/live-network.test.d.ts +2 -0
  165. package/dist/selftest/live-network.test.js +63 -0
  166. package/dist/selftest/live-terminal.test.d.ts +2 -0
  167. package/dist/selftest/live-terminal.test.js +169 -0
  168. package/dist/selftest/local-config.test.js +42 -4
  169. package/dist/selftest/mirror.test.js +36 -7
  170. package/dist/selftest/network.test.js +12 -0
  171. package/dist/selftest/projects.test.js +77 -8
  172. package/dist/selftest/repo-guard.test.js +59 -1
  173. package/dist/selftest/sync.test.js +50 -10
  174. package/dist/selftest/toolchains.test.js +30 -0
  175. package/dist/selftest/workspace-view.test.d.ts +2 -0
  176. package/dist/selftest/workspace-view.test.js +89 -0
  177. package/dist/shared/apps.d.ts +49 -0
  178. package/dist/shared/apps.js +114 -0
  179. package/dist/shared/console-input.d.ts +11 -0
  180. package/dist/shared/console-input.js +61 -0
  181. package/dist/shared/download.d.ts +3 -0
  182. package/dist/shared/download.js +25 -2
  183. package/dist/shared/guest-paths.d.ts +27 -0
  184. package/dist/shared/guest-paths.js +68 -0
  185. package/dist/shared/netpolicy.d.ts +27 -0
  186. package/dist/shared/netpolicy.js +51 -0
  187. package/dist/shared/projects.d.ts +27 -4
  188. package/dist/shared/projects.js +72 -13
  189. package/dist/shared/pty-client.d.ts +26 -4
  190. package/dist/shared/pty-client.js +204 -25
  191. package/dist/shared/rundir.d.ts +2 -0
  192. package/dist/shared/rundir.js +2 -0
  193. package/dist/shared/schema.d.ts +34 -0
  194. package/dist/shared/schema.js +14 -0
  195. package/dist/shared/system.d.ts +1 -0
  196. package/dist/shared/system.js +1 -0
  197. package/dist/sync/claude-mem.js +3 -2
  198. package/dist/sync/history.d.ts +6 -0
  199. package/dist/sync/history.js +26 -11
  200. package/dist/sync/watcher.js +78 -6
  201. package/package.json +2 -1
  202. package/sandbox/cargoshim.cs +56 -0
  203. package/sandbox/cpucap.cs +33 -0
  204. package/sandbox/cpucap.ps1 +4 -35
  205. package/sandbox/logon.cmd +6 -0
  206. package/sandbox/memlog.ps1 +34 -0
  207. package/sandbox/pagefile.cs +31 -0
  208. package/sandbox/pagefile.ps1 +21 -0
  209. package/sandbox/ptyhost.cs +162 -7
  210. package/schemas/config.schema.json +318 -3
  211. package/schemas/sandbox-config.schema.json +129 -14
  212. package/dist/cli/menu-multi.js +0 -82
  213. package/dist/cli/menu.d.ts +0 -14
  214. package/dist/cli/menu.js +0 -290
  215. package/dist/cli/settings/edit.d.ts +0 -13
  216. package/dist/cli/settings/edit.js +0 -19
  217. package/dist/cli/settings/index.d.ts +0 -8
  218. package/dist/cli/settings/index.js +0 -131
  219. package/dist/cli/settings/prompts.d.ts +0 -7
  220. package/dist/cli/settings/prompts.js +0 -48
  221. package/dist/cli/settings/schema.d.ts +0 -33
  222. package/dist/cli/settings/schema.js +0 -29
@@ -8,7 +8,8 @@
8
8
  "rust": { "enabled": false }
9
9
  },
10
10
  "mappings": [
11
- { "host": "D:\\Assets\\textures", "guest": "C:\\Assets", "readOnly": true, "optional": true }
11
+ { "host": "./data", "guest": "C:\\Data", "readOnly": true, "optional": true },
12
+ { "host": "D:\\Assets\\textures", "readOnly": true, "optional": true }
12
13
  ],
13
14
  "scripts": []
14
15
  }
package/README.md CHANGED
@@ -31,7 +31,7 @@ works too. [Other ways to install](docs/install.md)
31
31
  ```powershell
32
32
  cd C:\code\my-app
33
33
 
34
- sandbox # menu: start an agent, a plain sandbox, diagnostics, cleanup ...
34
+ sandbox # the menu: start agents, sessions, apps, every setting, diagnostics ...
35
35
  sandbox claude # Claude Code in a sandbox for this folder (or: claude-sandbox)
36
36
  sandbox claude -t # ... in this terminal (Ctrl+] detaches, the session keeps running)
37
37
  sandbox start # just the sandbox, no agent
@@ -49,6 +49,7 @@ The first start downloads what the project needs (a few minutes, once). After th
49
49
  |---|---|
50
50
  | **Safe by default** | The agent sees only your project folder. Credentials are stripped before anything is copied in, your LAN is blocked, and nothing the sandbox writes can run on your PC. |
51
51
  | **Languages** | Node, Python, Rust, C/C++ (GCC or MSVC), .NET (incl. .NET Framework targeting), Go, Java, Ruby, PHP, Deno, Bun, PowerShell 7, with pnpm, yarn, Volta, uv, pyenv, Maven, Gradle, Composer, gh ... |
52
+ | **Apps and tools** | VS Code (set up for your languages, with their extensions) and Windows Terminal, plus 60 optional apps and tools by category: DBeaver, pgAdmin, Bruno, Godot, Blender, kubectl, jq, ripgrep ... and programs of your own |
52
53
  | **Databases** | PostgreSQL, MySQL, MongoDB, Redis and DynamoDB Local with a web UI, started when the project uses them, or any time with `--database` / `sandbox db` |
53
54
  | **Agents** | Claude Code, Codex, Gemini, opencode, Qwen, Crush, Copilot, Kilo, Amp, Cline, Continue, Kimi, Aider, DeepSeek agents |
54
55
  | **Smart** | Detects what the project needs from its files, respects version files, installs in parallel and reuses everything |
@@ -60,7 +61,7 @@ The first start downloads what the project needs (a few minutes, once). After th
60
61
 
61
62
  | Command | |
62
63
  |---|---|
63
- | `sandbox` | the interactive menu: repeat your last command, start agents, every option, and all settings |
64
+ | `sandbox` | the menu, a full-screen app: start agents, attach to sessions, pick apps, edit every setting (yours and the project's), all commands, with search |
64
65
  | `sandbox <agent>`, `claude-sandbox` | start an agent; `-t` for this terminal, `--headless` for no window |
65
66
  | `sandbox start` | the sandbox without an agent |
66
67
  | `sandbox attach` / `sessions` / `status` | work with a running sandbox (this folder's project first) |
@@ -3,12 +3,25 @@
3
3
  "defaultHarness": "claude",
4
4
  "guestWorkspace": "auto",
5
5
  "readOnly": false,
6
+ "workspaceFilter": {
7
+ "enabled": false,
8
+ "exclude": [],
9
+ "include": []
10
+ },
11
+ "audit": {
12
+ "enabled": false,
13
+ "days": 14,
14
+ "maxMb": 100,
15
+ "processes": true,
16
+ "files": true
17
+ },
6
18
  "mappings": [],
7
19
  "sandbox": {
8
20
  "memoryMb": "auto",
9
21
  "maxMemoryMb": 32768,
10
22
  "maxMemoryPercent": 80,
11
23
  "cpuLimit": 80,
24
+ "pageFileMb": 16384,
12
25
  "reserveHostMb": 8192,
13
26
  "vGpu": false,
14
27
  "networking": true,
@@ -17,15 +30,23 @@
17
30
  "videoInput": false,
18
31
  "printerRedirection": false,
19
32
  "protectedClient": true,
20
- "headless": false
33
+ "headless": false,
34
+ "keepAwake": true
21
35
  },
22
36
  "storage": {
23
37
  "root": "%LOCALAPPDATA%\\AgentSandbox",
24
38
  "persistAgentState": true,
25
- "dataName": "default"
39
+ "dataName": "default",
40
+ "markSandboxFiles": true,
41
+ "warnDataGb": 50
26
42
  },
27
43
  "network": {
28
- "blockPrivateRanges": true
44
+ "blockPrivateRanges": true,
45
+ "allowOnly": [],
46
+ "blockHosts": [],
47
+ "forwardPorts": [],
48
+ "dns": [],
49
+ "proxy": ""
29
50
  },
30
51
  "provisioning": {
31
52
  "copyFromHost": true,
@@ -37,6 +58,9 @@
37
58
  },
38
59
  "terminal": {
39
60
  "enabled": true
61
+ },
62
+ "conpty": {
63
+ "enabled": true
40
64
  }
41
65
  },
42
66
  "downloads": {
@@ -135,7 +159,10 @@
135
159
  "vulkan": false
136
160
  },
137
161
  "vscode": {
138
- "enabled": true
162
+ "enabled": true,
163
+ "setup": true,
164
+ "recommended": true,
165
+ "extensions": []
139
166
  },
140
167
  "environment": {}
141
168
  },
@@ -188,6 +215,10 @@
188
215
  "personalization": {
189
216
  "enabled": true
190
217
  },
218
+ "apps": {
219
+ "install": [],
220
+ "custom": {}
221
+ },
191
222
  "claudeProfile": {
192
223
  "enabled": true,
193
224
  "hostDir": "auto",
@@ -203,7 +234,9 @@
203
234
  "copyCredentials": false,
204
235
  "overwrite": true,
205
236
  "syncHistory": true,
206
- "syncClaudeMem": true
237
+ "historyScope": "project",
238
+ "syncClaudeMem": true,
239
+ "syncClaudeMemBack": false
207
240
  },
208
241
  "harnesses": {
209
242
  "claude": {
@@ -344,6 +377,7 @@
344
377
  "roots": [],
345
378
  "askToAddRoot": true,
346
379
  "declinedRoots": [],
380
+ "allowOutsideMappings": "ask",
347
381
  "wsbShare": true
348
382
  }
349
383
  }
@@ -29,6 +29,9 @@ export interface CliOptions {
29
29
  all: boolean;
30
30
  /** launch --session <agent>@<folder> (repeatable): several sessions at once, folders made absolute. */
31
31
  sessions: SessionWant[];
32
+ /** audit: only events of this kind, and containing this text. */
33
+ kind?: string;
34
+ grep?: string;
32
35
  }
33
36
  /** One session to start: an agent in a folder, with arguments of its own (after the ones given after --). */
34
37
  export interface SessionWant {
package/dist/cli/args.js CHANGED
@@ -134,7 +134,9 @@ export const parseCli = (argv, wrapper = false) => {
134
134
  all: Boolean(o.all),
135
135
  sessions: (o.session ?? []).map((s) => parseSessionWant(s)),
136
136
  timeout: o.timeout ?? 600,
137
- days: o.days ?? 7,
137
+ days: o.days ?? (name === 'audit' ? 1 : 7),
138
+ kind: o.kind,
139
+ grep: o.grep,
138
140
  };
139
141
  };
140
142
  const defaults = () => ({
@@ -0,0 +1,22 @@
1
+ import type { SandboxStatus } from '../core/types.js';
2
+ /** One progress line: `line` is shown, `key` changes only when the step or what runs changes, `what` names it. */
3
+ export interface Shown {
4
+ line: string;
5
+ key: string;
6
+ what: string;
7
+ }
8
+ /** "[3/8] Developer tools: downloading rust 40%, setting up go (2 of 6 done)". */
9
+ export declare const bootLine: (status: SandboxStatus | null, fallback?: string) => Shown;
10
+ /** Nothing new for a while: say so, and where the details are. */
11
+ export declare const stillWorking: (what: string, ms: number) => string;
12
+ export declare const readyIn: (ms: number) => string;
13
+ /** The wanted tools that are not downloaded yet (`has` says whether a folder in <data>\tools exists), with their rough size. */
14
+ export declare const pendingDownloads: (wanted: string[], has: (dir: string) => boolean) => {
15
+ names: string[];
16
+ mb: number;
17
+ };
18
+ /** The heads-up before the wait, or null when everything is already downloaded. first: nothing was ever installed. */
19
+ export declare const firstStartNotice: (first: boolean, pending: {
20
+ names: string[];
21
+ mb: number;
22
+ }) => string | null;
@@ -0,0 +1,59 @@
1
+ // What the terminal says while a sandbox starts: the first-start heads-up (what is downloaded, roughly how much) and
2
+ // one line of progress from the guest's status. Pure: the status is guest-written, so every bit of it is made safe
3
+ // and short here.
4
+ import { plainText } from '../core/util.js';
5
+ import { elapsed } from './progress.js';
6
+ const clean = (text, max) => plainText(text).replace(/\s+/g, ' ').trim().slice(0, max);
7
+ /** "[3/8] Developer tools: downloading rust 40%, setting up go (2 of 6 done)". */
8
+ export const bootLine = (status, fallback = 'Starting Windows Sandbox') => {
9
+ if (!status)
10
+ return { line: fallback, key: fallback, what: fallback.toLowerCase() };
11
+ const p = status.progress;
12
+ if (!p) {
13
+ const line = clean(status.message, 200) || fallback;
14
+ return { line, key: line, what: line };
15
+ }
16
+ const title = clean(p.title, 60);
17
+ const items = p.active.slice(0, 8).map((a) => ({ name: clean(a.name, 40), percent: a.percent }));
18
+ const loading = items.filter((a) => a.percent !== undefined && a.percent < 100);
19
+ const others = items.filter((a) => !loading.includes(a));
20
+ const parts = [
21
+ ...(loading.length ? ['downloading ' + loading.map((a) => a.name + ' ' + a.percent + '%').join(', ')] : []),
22
+ ...(others.length ? ['setting up ' + others.map((a) => a.name).join(', ')] : []),
23
+ ];
24
+ const count = p.total > 0 && items.length ? ' (' + Math.min(p.done, p.total) + ' of ' + p.total + ' done)' : '';
25
+ const head = '[' + p.step + '/' + p.steps + '] ' + title;
26
+ return {
27
+ line: (head + (parts.length ? ': ' + parts.join('; ') : '') + count).slice(0, 240),
28
+ key: head + '|' + items.map((a) => a.name).sort().join(','),
29
+ what: items.length ? items.map((a) => a.name).join(', ') : title,
30
+ };
31
+ };
32
+ /** Nothing new for a while: say so, and where the details are. */
33
+ export const stillWorking = (what, ms) => 'Still working on ' + clean(what, 80) + ' (' + Math.max(1, Math.round(ms / 60000)) + ' min). Full log: sandbox logs -f';
34
+ export const readyIn = (ms) => 'Ready in ' + elapsed(ms) + '.';
35
+ // ---- the first-start heads-up ----------------------------------------------------------------------------------------
36
+ /** What a first start downloads, by setting name: its folder in <data>\tools, rough size and a readable name. */
37
+ const DOWNLOADS = {
38
+ node: { dir: 'nvm', mb: 30 }, python: { dir: 'python', mb: 30 }, rust: { dir: 'rust', mb: 300 }, msvc: { dir: 'msvc', mb: 1500, label: 'MSVC' },
39
+ cpp: { dir: 'mingw', mb: 270, label: 'C/C++' }, dotnet: { dir: 'dotnet', mb: 250, label: '.NET' }, go: { dir: 'go', mb: 80 }, java: { dir: 'java', mb: 200 },
40
+ ruby: { dir: 'ruby', mb: 100 }, php: { dir: 'php', mb: 30 }, deno: { dir: 'deno', mb: 40 }, pwsh: { dir: 'pwsh', mb: 100, label: 'PowerShell 7' },
41
+ vscode: { dir: 'vscode', mb: 150, label: 'VS Code' },
42
+ postgres: { dir: 'postgres', mb: 330, label: 'PostgreSQL' }, mysql: { dir: 'mariadb', mb: 90, label: 'MariaDB' }, mongodb: { dir: 'mongodb', mb: 450, label: 'MongoDB' },
43
+ redis: { dir: 'redis', mb: 10 }, dynamodb: { dir: 'dynamodb-local', mb: 80, label: 'DynamoDB Local' },
44
+ };
45
+ /** The wanted tools that are not downloaded yet (`has` says whether a folder in <data>\tools exists), with their rough size. */
46
+ export const pendingDownloads = (wanted, has) => {
47
+ const missing = [...new Set(wanted)].filter((w) => Object.hasOwn(DOWNLOADS, w) && !has(DOWNLOADS[w].dir));
48
+ return { names: missing.map((w) => DOWNLOADS[w].label ?? w), mb: missing.reduce((sum, w) => sum + DOWNLOADS[w].mb, 0) };
49
+ };
50
+ const roughSize = (mb) => (mb >= 950 ? (Math.round(mb / 100) / 10) + ' GB' : Math.max(10, Math.round(mb / 10) * 10) + ' MB');
51
+ /** The heads-up before the wait, or null when everything is already downloaded. first: nothing was ever installed. */
52
+ export const firstStartNotice = (first, pending) => {
53
+ if (!first && !pending.names.length)
54
+ return null;
55
+ const what = pending.names.length ? ' (about ' + roughSize(pending.mb) + ': ' + pending.names.slice(0, 8).join(', ') + (pending.names.length > 8 ? ', ...' : '') + ')' : '';
56
+ return first
57
+ ? 'First start: the sandbox downloads the tools this project needs once' + what + '. This can take 5-20 minutes depending on your connection; later starts take about a minute.'
58
+ : 'New tools for this project: the sandbox downloads them once' + what + '. This can take a few minutes; later starts skip it.';
59
+ };
@@ -3,7 +3,7 @@ import { activateAccount, listAccounts, removeAccount, saveAccount, setDefaultAc
3
3
  import { restartSessions, runningSessions } from '../../host/channel.js';
4
4
  import { hostClaudeDir, hostGlobalStateFile } from '../../host/claude-profile.js';
5
5
  import { isSandboxRunning } from '../../host/platform.js';
6
- import { fail } from '../../core/util.js';
6
+ import { fail, safeText } from '../../core/util.js';
7
7
  import { printJson, sandboxAlive, table } from './context.js';
8
8
  const label = (a) => '"' + a.name + '"' + (a.email ? ' (' + a.email + ')' : '');
9
9
  /** This PC's Claude login location (config folder + global state file). */
@@ -27,7 +27,7 @@ const use = async (ctx, name) => {
27
27
  console.log('Sandbox now uses ' + label(activateAccount(ctx.storageRoot, ctx.dataPath, name)) + '.');
28
28
  const claudeRunning = isSandboxRunning() && runningSessions(ctx.dataPath).some((s) => s.harness === 'claude');
29
29
  if (claudeRunning && !ctx.options.noRestart)
30
- console.log((await restartSessions(ctx.dataPath, 'claude', sandboxAlive)).message + ' with --continue; the conversation carries on under the new account.');
30
+ console.log(safeText((await restartSessions(ctx.dataPath, 'claude', sandboxAlive)).message) + ' with --continue; the conversation carries on under the new account.');
31
31
  return 0;
32
32
  };
33
33
  export const account = async (ctx) => {
@@ -7,7 +7,7 @@ import { agentCommands, commandsDir, reconcileAgentCommands } from '../../host/a
7
7
  import { insideSandbox } from '../../host/platform.js';
8
8
  import { writeFileAtomic } from '../../shared/rundir.js';
9
9
  import { c } from '../colors.js';
10
- import { editConfigText } from '../settings/edit.js';
10
+ import { editConfigText } from '../menu/settings/config-file.js';
11
11
  import { table } from './context.js';
12
12
  const entryOf = (ctx) => path.join(ctx.packageRoot, 'dist', 'bin', 'sandbox.js');
13
13
  /** Brings this PC in line with agentCommands and says what changed (used after on/off, settings and launch). */
@@ -36,8 +36,9 @@ export const table = (rows) => {
36
36
  };
37
37
  /** Prints `value` as JSON when --json was given; returns whether it did. */
38
38
  export const printJson = (ctx, value) => {
39
+ // JSON.stringify leaves C1 controls (U+0080-U+009F) as they are; text from the sandbox must not reach a terminal raw.
39
40
  if (ctx.options.json)
40
- console.log(JSON.stringify(value, null, 2));
41
+ console.log(JSON.stringify(value, null, 2).replace(/[\u0080-\u009f]/g, (ch) => '\\u' + ch.charCodeAt(0).toString(16).padStart(4, '0')));
41
42
  return ctx.options.json;
42
43
  };
43
44
  export const sandboxAlive = () => isSandboxRunning();
@@ -17,7 +17,7 @@ const ALL_WARNING = 'removes EVERYTHING the sandbox keeps: toolchains, databases
17
17
  * not a plain relative target inside the data folder: absolute paths, drive letters, `..`, wildcards, unknown areas.
18
18
  */
19
19
  export const resolveCleanTargets = (inputs, dataPath, storageRoot) => {
20
- const usage = 'Targets: all | tools | tools/<name> | tools/agents/<name> | databases | databases/<name> | state | state/<agent> | logs | launches. See them with: sandbox data';
20
+ const usage = 'Targets: all | tools | tools/<name> | tools/agents/<name> | databases | databases/<name> | state | state/<agent> | logs | launches | views. See them with: sandbox data';
21
21
  if (!inputs.length)
22
22
  fail('Say what to remove.', usage);
23
23
  const resolved = inputs.map((raw) => {
@@ -39,6 +39,11 @@ export const resolveCleanTargets = (inputs, dataPath, storageRoot) => {
39
39
  return { target: 'all', path: dataPath, root: path.join(storageRoot, 'data'), warning: ALL_WARNING };
40
40
  if (area === 'launches')
41
41
  return { target, path: path.join(storageRoot, 'launches', ...rest), root: storageRoot };
42
+ if (area === 'views') {
43
+ if (rest.length)
44
+ fail('Remove all views ("views"), not single ones.', usage);
45
+ return { target, path: path.join(storageRoot, 'views'), root: storageRoot, warning: 'Views are the copies of your projects without their git-ignored files (workspaceFilter). Your projects are not touched; what the sandbox built or installed in them (node_modules, build output) goes, and the next start copies the projects again.' };
46
+ }
42
47
  if (area === 'accounts' || area === 'sync')
43
48
  fail('"' + area + '" is kept on this PC on purpose and is not removed here.', area === 'accounts' ? 'Remove a saved login with: sandbox account remove <name>' : usage);
44
49
  if (!DATA_AREAS.includes(area))
@@ -35,7 +35,7 @@ export const collectDataEntries = (dataPath, storageRoot) => {
35
35
  });
36
36
  const otherData = entriesOf(path.join(storageRoot, 'data')).filter((d) => d.isDirectory() && path.join(storageRoot, 'data', d.name).toLowerCase() !== path.resolve(dataPath).toLowerCase());
37
37
  const host = [
38
- ...['launches', 'accounts', 'sync', 'logs', 'run', 'host'].map((name) => ({ area: 'host', name, path: path.join(storageRoot, name), bytes: sizeOf(path.join(storageRoot, name)) })),
38
+ ...['launches', 'views', 'accounts', 'sync', 'logs', 'run', 'host'].map((name) => ({ area: 'host', name, path: path.join(storageRoot, name), bytes: sizeOf(path.join(storageRoot, name)) })),
39
39
  ...otherData.map((d) => ({ area: 'host', name: 'other data folder: ' + d.name, path: path.join(storageRoot, 'data', d.name), bytes: sizeOf(path.join(storageRoot, 'data', d.name)) })),
40
40
  ].filter((r) => fs.existsSync(r.path));
41
41
  return _.sortBy([...rows, ...host], (r) => AREA_ORDER.indexOf(r.area), (r) => -r.bytes, (r) => r.name.toLowerCase());
@@ -48,7 +48,7 @@ export const cleanTargetOf = (entry) => {
48
48
  return entry.name;
49
49
  if (entry.area === 'logs')
50
50
  return 'logs';
51
- if (entry.area === 'host' && entry.name === 'launches')
52
- return 'launches';
51
+ if (entry.area === 'host' && (entry.name === 'launches' || entry.name === 'views'))
52
+ return entry.name;
53
53
  return null;
54
54
  };
@@ -6,5 +6,5 @@ export { repairCommand } from './repair/index.js';
6
6
  export { attach, db, exec, hide, restart, sessions, show, status, stop } from './sessions.js';
7
7
  export { account } from './accounts.js';
8
8
  export { projects } from './projects.js';
9
- export { claudeHost, clean, config, doctor, logs, sync, watcher } from './maintenance.js';
9
+ export { audit, claudeHost, clean, config, doctor, logs, sync, watcher } from './maintenance.js';
10
10
  export { agentCommandsCommand, syncAgentCommands } from './agent-commands.js';
@@ -6,5 +6,5 @@ export { repairCommand } from './repair/index.js';
6
6
  export { attach, db, exec, hide, restart, sessions, show, status, stop } from './sessions.js';
7
7
  export { account } from './accounts.js';
8
8
  export { projects } from './projects.js';
9
- export { claudeHost, clean, config, doctor, logs, sync, watcher } from './maintenance.js';
9
+ export { audit, claudeHost, clean, config, doctor, logs, sync, watcher } from './maintenance.js';
10
10
  export { agentCommandsCommand, syncAgentCommands } from './agent-commands.js';
@@ -6,8 +6,11 @@ import { type Joined } from './project.js';
6
6
  export declare const attachHere: (ctx: Context, session: Parameters<typeof attachTerminal>[0]) => Promise<number>;
7
7
  /** The sandbox writes sandbox.json a few seconds into its boot; a second launch right after the first waits for it. */
8
8
  export declare const runningSandboxStatus: (dataPath: string) => Promise<SandboxStatus>;
9
- /** Waits for the boot with its current step on a spinner (plain lines without a terminal). */
10
- export declare const waitForBoot: (dataPath: string, launchId: string | null) => Promise<SandboxStatus>;
9
+ /**
10
+ * Waits for the boot with its current step and what it sets up on one line (plain lines without a terminal).
11
+ * timed: say how long it took (a first start, where that is worth knowing).
12
+ */
13
+ export declare const waitForBoot: (dataPath: string, launchId: string | null, timed?: boolean) => Promise<SandboxStatus>;
11
14
  /** The running sandbox, once it is ready. */
12
15
  export declare const readySandbox: (dataPath: string) => Promise<SandboxStatus>;
13
16
  /**
@@ -12,25 +12,35 @@ import { generateSandbox } from '../../host/generate.js';
12
12
  import { findSecretFiles } from '../../host/secrets.js';
13
13
  import { checkRepo, saveRepoSnapshot } from '../../host/repo-guard.js';
14
14
  import { assertCanLaunch, insideSandbox, isSandboxRunning, probeHost, spawnDetached, startSandbox } from '../../host/platform.js';
15
- import { NeedsRestart, rememberProject, startRegistry } from '../../host/projects.js';
16
- import { ensureTerminal } from '../../host/terminal.js';
15
+ import { NeedsRestart, readKnown, rememberProject, startRegistry } from '../../host/projects.js';
16
+ import { ensureConpty, ensureTerminal } from '../../host/terminal.js';
17
+ import { planView, saveActiveViews, viewSkipped, WorkspaceView } from '../../host/workspace-view/index.js';
18
+ import { saveForwards } from '../../host/port-forward.js';
19
+ import { saveAuditSettings } from '../../host/audit/collect.js';
20
+ import { readDataSize, saveMarkedProjects } from '../../host/motw.js';
21
+ import { humanBytes } from '../../core/disk.js';
17
22
  import { hideSandboxWindow, showSandboxWindow } from '../../host/window.js';
18
23
  import { readSandboxStatus } from '../../shared/rundir.js';
19
24
  import { isWithin } from '../../shared/projects.js';
20
25
  import { syncBeforeLaunch, watcherRunning } from '../../sync/watcher.js';
21
- import { protectionsOf } from '../../core/project-policy.js';
26
+ import { BOOT_SETTINGS, protectionsOf } from '../../core/project-policy.js';
27
+ import { LOCAL_CONFIG_NAME } from '../../core/local-config.js';
22
28
  import { assertSafeMapping, assertSafeWorkspace } from '../../core/safe-paths.js';
23
- import { errMsg, fail, isRootPath, sleep } from '../../core/util.js';
29
+ import { dirExists, errMsg, fail, fileExists, isRootPath, sleep } from '../../core/util.js';
30
+ import { planBootProjects } from '../../host/launch/projects.js';
24
31
  import { syncAgentCommands } from './agent-commands.js';
25
32
  import { askYesNo } from '../../core/prompt.js';
26
- import { resolveRun } from '../../core/workspace.js';
33
+ import { autoGuestWorkspace, placeInRoot, resolveRun } from '../../core/workspace.js';
34
+ import { planMirrorWorkspace } from '../../shared/guest-paths.js';
27
35
  import { c } from '../colors.js';
28
- import { working } from '../progress.js';
36
+ import { follow, working } from '../progress.js';
37
+ import { bootLine, firstStartNotice, pendingDownloads, readyIn, stillWorking } from '../boot-progress.js';
29
38
  import { requireOnPc, sandboxAlive, table } from './context.js';
30
39
  import _ from 'lodash';
31
40
  import { hereDir, joinHere, readyHere } from './project.js';
32
41
  import { offerRestart } from './restart.js';
33
42
  import { addToList, setSetting } from './user-config.js';
43
+ import { lastStopWarning, startLife, UPDATE_RESTART_WARNING, updateRestartPending } from '../../host/stops.js';
34
44
  /** Attaches this terminal; with attach.autoHide the sandbox window hides meanwhile (best effort, never fails the attach). */
35
45
  export const attachHere = (ctx, session) => {
36
46
  if (ctx.loaded.config.attach.autoHide && !insideSandbox())
@@ -39,8 +49,11 @@ export const attachHere = (ctx, session) => {
39
49
  };
40
50
  /** Nothing has been installed into this data set yet: the first start downloads every toolchain once. */
41
51
  const firstBoot = (dataPath) => !fs.existsSync(path.join(dataPath, 'tools', 'resolved-versions.json'));
42
- const FIRST_BOOT = 'First start for this storage: the sandbox downloads and installs the toolchains, databases and agent this project needs.\n' +
43
- 'That takes a few minutes, once. Everything is kept, so every later start reuses it and takes seconds.';
52
+ /** The first-start heads-up (also when this project needs tools not downloaded yet), or null. */
53
+ const FIRST_BOOT = (dataPath, generated, config) => {
54
+ const wanted = ['node', ...Object.entries(generated.toolchains).filter(([, d]) => d.on).map(([name]) => name), ...(config.toolchains.vscode.enabled ? ['vscode'] : []), ...generated.databases.map((d) => d.database)];
55
+ return firstStartNotice(firstBoot(dataPath), pendingDownloads(wanted, (dir) => fs.existsSync(path.join(dataPath, 'tools', dir))));
56
+ };
44
57
  /** The sandbox writes sandbox.json a few seconds into its boot; a second launch right after the first waits for it. */
45
58
  export const runningSandboxStatus = async (dataPath) => {
46
59
  for (let i = 0; i < 60; i++) {
@@ -51,10 +64,18 @@ export const runningSandboxStatus = async (dataPath) => {
51
64
  }
52
65
  return fail('A Windows Sandbox is running that this tool did not start (or that uses another storage.dataName).', 'Close that sandbox window first, then run sandbox claude again.');
53
66
  };
54
- /** Waits for the boot with its current step on a spinner (plain lines without a terminal). */
55
- export const waitForBoot = (dataPath, launchId) => {
56
- let step = 'starting Windows Sandbox';
57
- return working(() => 'Waiting for the sandbox: ' + step, () => waitForReady(dataPath, launchId, sandboxAlive, (m) => { step = m; }));
67
+ /**
68
+ * Waits for the boot with its current step and what it sets up on one line (plain lines without a terminal).
69
+ * timed: say how long it took (a first start, where that is worth knowing).
70
+ */
71
+ export const waitForBoot = async (dataPath, launchId, timed = false) => {
72
+ let status = null;
73
+ const start = Date.now();
74
+ const ready = await follow(() => bootLine(status), () => waitForReady(dataPath, launchId, sandboxAlive, () => undefined, (s) => { if (s.phase !== 'failed')
75
+ status = s; }), { stall: stillWorking });
76
+ if (timed)
77
+ console.log(readyIn(Date.now() - start));
78
+ return ready;
58
79
  };
59
80
  /** The running sandbox, once it is ready. */
60
81
  export const readySandbox = async (dataPath) => {
@@ -64,7 +85,8 @@ export const readySandbox = async (dataPath) => {
64
85
  /** Waits for a session to start (its project's setup, the agent's install) with its progress on a spinner. */
65
86
  const waitForStart = (ctx, requestId, what) => {
66
87
  let step = 'starting';
67
- return working(() => what + ': ' + step, () => waitForSession(ctx.dataPath, requestId, sandboxAlive, 60 * 60 * 1000, (m) => { step = m; }));
88
+ const shown = () => ({ line: what + ': ' + step, key: step, what: step });
89
+ return follow(shown, () => waitForSession(ctx.dataPath, requestId, sandboxAlive, 60 * 60 * 1000, (m) => { step = m; }), { stall: stillWorking });
68
90
  };
69
91
  /**
70
92
  * Starts (or picks back up) a session in the folder `joined` describes. attach: show it in this terminal.
@@ -210,12 +232,39 @@ const maybeAddRoot = async (ctx, run) => {
210
232
  addToList(ctx, ['projects', 'roots'], parent);
211
233
  run.config.projects.roots.push(parent);
212
234
  console.log('Added ' + parent + ' to projects.roots (sandbox projects roots remove ' + parent + ' undoes it).');
235
+ // From now on this project is under the root's folder inside the sandbox, from this start on (one move, not two).
236
+ const before = run.config.guestWorkspace;
237
+ const derived = [autoGuestWorkspace(run.workspaceHost), planMirrorWorkspace(run.workspaceHost, autoGuestWorkspace(run.workspaceHost)).guest];
238
+ // In place: the caller holds run.config already.
239
+ if (derived.some((g) => g.toLowerCase() === before.toLowerCase()))
240
+ run.config.guestWorkspace = placeInRoot(run.config, run.workspaceHost, Boolean(run.mirror), run.notes).guestWorkspace;
241
+ if (run.config.guestWorkspace !== before)
242
+ console.log('Inside the sandbox this project is now at ' + run.config.guestWorkspace + ' (was ' + before + '): git worktrees made at the old place need `git worktree repair` once.');
213
243
  }
214
244
  if (answer === 'no')
215
245
  addToList(ctx, ['projects', 'declinedRoots'], parent);
216
246
  if (answer === 'never')
217
247
  setSetting(ctx, ['projects', 'askToAddRoot'], false);
218
248
  };
249
+ /**
250
+ * workspaceFilter: brings the project's filtered view in step before the sandbox maps it, and tells the background
251
+ * watcher to keep it in step while the sandbox runs. Returns the line for the launch table.
252
+ */
253
+ const prepareWorkspaceView = async (ctx, run) => {
254
+ const plan = planView(run.config, ctx.storageRoot, run.workspaceHost);
255
+ saveActiveViews(ctx.storageRoot, ctx.dataPath, plan ? [plan.options] : []);
256
+ saveForwards(ctx.storageRoot, ctx.dataPath, run.config.sandbox.networking ? run.config.network.forwardPorts : []);
257
+ saveAuditSettings(ctx.storageRoot, ctx.dataPath, { ...run.config.audit, projects: [run.workspaceHost] });
258
+ saveMarkedProjects(ctx.storageRoot, ctx.dataPath, run.config.storage.markSandboxFiles ? [run.workspaceHost] : []);
259
+ const size = readDataSize(ctx.storageRoot, ctx.dataPath);
260
+ const limit = run.config.storage.warnDataGb;
261
+ if (limit > 0 && size && size.bytes > limit * 1024 ** 3)
262
+ console.warn(c.yellow('The persisted data takes ' + humanBytes(size.bytes) + ' (more than storage.warnDataGb, ' + limit + ' GB). See what: sandbox data; free space: sandbox data clean <what>.'));
263
+ if (!plan)
264
+ return viewSkipped(run.config, run.workspaceHost) ?? 'off (git-ignored files are visible in the sandbox; workspaceFilter.enabled keeps them out)';
265
+ const stats = await working('Preparing the workspace without its git-ignored files', () => new WorkspaceView(plan.options).prepare());
266
+ return 'on: git-ignored files stay on this PC' + (stats.toView || stats.toHost ? ' (' + stats.toView + ' copied in, ' + stats.toHost + ' out' + (stats.conflicts ? ', ' + stats.conflicts + ' conflicts kept next to the file' : '') + ')' : '') + '; ' + plan.options.view;
267
+ };
219
268
  /** Everything a real launch does before the sandbox starts (skipped by `generate`, which only writes files). */
220
269
  const prepare = async (ctx, run) => {
221
270
  const { options, dataPath, storageRoot } = ctx;
@@ -224,12 +273,45 @@ const prepare = async (ctx, run) => {
224
273
  if (account && run.harnessName === 'claude' && account !== activeAccount(dataPath))
225
274
  activateAccount(storageRoot, dataPath, account);
226
275
  const terminal = config.provisioning.terminal.enabled ? await ensureTerminal(dataPath, config.provisioning.refresh, { pwsh: config.toolchains.pwsh.enabled !== false }) : { exe: null, message: 'disabled' };
276
+ const conpty = config.provisioning.conpty.enabled ? await ensureConpty(dataPath, config.provisioning.refresh) : 'off';
227
277
  const history = syncSummary(config, dataPath, run.workspaceHost);
228
278
  const guarded = checkRepo(dataPath);
229
279
  if (guarded.length)
230
280
  console.warn(c.yellow('SECURITY: neutralised ' + guarded.length + ' change(s) the last sandbox made that could run code on this PC (see sandbox status).'));
281
+ const stopped = lastStopWarning(storageRoot, dataPath, watcherRunning(dataPath));
282
+ if (stopped)
283
+ console.warn(c.yellow(stopped));
284
+ if (updateRestartPending())
285
+ console.warn(c.yellow(UPDATE_RESTART_WARNING));
231
286
  resetRunDir(dataPath);
232
- return { account, terminal: terminal.message, history, pruned: describePrune(pruneStorage(storageRoot)) };
287
+ const filter = await prepareWorkspaceView(ctx, run);
288
+ return { account, terminal: terminal.message + '; pseudo console: ' + conpty, history, pruned: describePrune(pruneStorage(storageRoot)), filter };
289
+ };
290
+ /**
291
+ * The other projects mapped at boot (a restart's, --session's, projects.all's, and the known projects in your roots),
292
+ * with their effective configs: only approved .sandbox-config.json files and the looser values you allowed for each
293
+ * (nothing is asked here). The sandbox boots with what they want too (generate.ts).
294
+ */
295
+ const bootWantsFor = async (ctx, run, extraProjects) => {
296
+ const { roots } = run.config.projects;
297
+ const planned = planBootProjects({ run, packageRoot: ctx.packageRoot, extraProjects }).projects.map((p) => p.host);
298
+ const inRoots = readKnown(run.storageRoot).map((k) => k.host).filter((h) => roots.some((r) => isWithin(path.resolve(r), h)));
299
+ const folders = _.uniqBy([...(extraProjects ?? []).map((p) => path.resolve(p)), ...planned, ...inRoots], (f) => f.toLowerCase())
300
+ .filter((f) => !isWithin(run.workspaceHost, f) && dirExists(f)).slice(0, 200);
301
+ const projects = [];
302
+ const notes = [];
303
+ for (const folder of folders) {
304
+ try {
305
+ const other = await resolveRun({ packageRoot: ctx.packageRoot, loaded: ctx.loaded, cwd: folder, workspace: folder, noAgent: true, noLocalConfig: false, trustConfig: false, scripts: [] });
306
+ projects.push({ name: path.basename(folder), config: other.config });
307
+ }
308
+ catch (e) {
309
+ if (fileExists(path.join(folder, LOCAL_CONFIG_NAME)))
310
+ notes.push('What ' + path.basename(folder) + ' asks for at boot is not included: ' + errMsg(e).split('\n')[0]);
311
+ }
312
+ }
313
+ const keepOff = BOOT_SETTINGS.filter((s) => _.get(ctx.loaded.config, s) === true && _.get(run.config, s) === false);
314
+ return { wants: { projects, keepOff }, notes };
233
315
  };
234
316
  export const launch = async (ctx, generateOnly = false, extras = {}) => {
235
317
  const { options, loaded, packageRoot, dataPath, storageRoot } = ctx;
@@ -271,8 +353,11 @@ export const launch = async (ctx, generateOnly = false, extras = {}) => {
271
353
  if (!generateOnly && !extras.extraProjects)
272
354
  await maybeAddRoot(ctx, run);
273
355
  const prepared = generateOnly ? null : await prepare(ctx, run);
274
- const generated = generateSandbox({ packageRoot, run, harnessArgs: options.passthrough, probe: probeHost(config, run.harnessName), extraProjects: extras.extraProjects, autoSession: extras.autoSession });
275
- for (const note of [...run.notes, ...generated.projects.notes])
356
+ const bootWants = await bootWantsFor(ctx, run, extras.extraProjects);
357
+ const generated = generateSandbox({ packageRoot, run, harnessArgs: options.passthrough, probe: probeHost(config, run.harnessName), extraProjects: extras.extraProjects, autoSession: extras.autoSession, bootWants: bootWants.wants });
358
+ // What the sandbox boots with (the launch folder's settings plus what the other projects at boot want).
359
+ const booted = generated.bootConfig;
360
+ for (const note of [...run.notes, ...bootWants.notes, ...generated.projects.notes, ...generated.mappingNotes])
276
361
  console.log(note);
277
362
  const secretFiles = config.secrets.warnFiles ? findSecretFiles(run.workspaceHost) : [];
278
363
  if (secretFiles.length) {
@@ -282,9 +367,9 @@ export const launch = async (ctx, generateOnly = false, extras = {}) => {
282
367
  const { roots, projects } = generated.projects;
283
368
  table([
284
369
  ['Workspace', run.workspaceHost + ' -> ' + config.guestWorkspace + (config.readOnly ? ' (read-only)' : '')],
285
- ['Agent', run.harnessName + (prepared?.account ? ' (account: ' + prepared.account + ')' : '') + ' Memory: ' + generated.memoryMb + ' MB CPU: ' + (config.sandbox.cpuLimit === 'none' ? 'all' : 'max ' + config.sandbox.cpuLimit + '%') + ' Network: ' + config.sandbox.networking + (config.sandbox.headless ? ' HEADLESS' : '')],
370
+ ['Agent', run.harnessName + (prepared?.account ? ' (account: ' + prepared.account + ')' : '') + ' Memory: ' + generated.memoryMb + ' MB CPU: ' + (config.sandbox.cpuLimit === 'none' ? 'all' : 'max ' + config.sandbox.cpuLimit + '%') + ' Network: ' + booted.sandbox.networking + (booted.sandbox.vGpu ? ' vGPU' : '') + (config.sandbox.headless ? ' HEADLESS' : '')],
286
371
  ['Node', run.versions.node.spec + ' (' + run.versions.node.source + ') Python: ' + run.versions.python.spec + ' (' + run.versions.python.source + ')'],
287
- ...(prepared ? [['Terminal', prepared.terminal], ['History', prepared.history], ['Cleanup', prepared.pruned]] : []),
372
+ ...(prepared ? [['Ignored files', prepared.filter], ['Terminal', prepared.terminal], ['History', prepared.history], ['Cleanup', prepared.pruned]] : []),
288
373
  ['Toolchains', Object.entries(generated.toolchains).filter(([, d]) => d.on).map(([name, d]) => name + ' (' + d.reason + ')').join(', ') || 'Node only (nothing else used by this project)'],
289
374
  ...(generated.databases.length
290
375
  ? generated.databases.map((d, i) => [i ? '' : 'Databases', d.database + (d.database === 'sqlserver' ? ' (cannot run in the sandbox; use Postgres or SQLite for local work)' : ' on port ' + d.port) + ' because ' + d.reasons[0] + (d.reasons.length > 1 ? ' (+' + (d.reasons.length - 1) + ' more)' : '')])
@@ -294,7 +379,7 @@ export const launch = async (ctx, generateOnly = false, extras = {}) => {
294
379
  ['Secrets', !config.secrets.strip ? 'NOT stripped (secrets.strip is off)' : generated.profile.secretsRemoved.length ? 'kept on this PC: ' + generated.profile.secretsRemoved.join(', ') + ' (secrets.allow lets one through)' : 'none found in what is copied'],
295
380
  ...roots.map((r, i) => [i ? '' : 'Project roots', r.host + ' -> ' + r.guest + ' (every project in it opens without a restart)']),
296
381
  ...projects.map((p, i) => [i ? '' : 'Projects', p.host + ' -> ' + p.guest]),
297
- ...generated.mappings.slice(3).filter((m) => ![...roots, ...projects].some((p) => p.guest === m.guest)).map((m) => [' map ' + (m.readOnly ? 'ro' : 'rw'), m.host + ' -> ' + m.guest]),
382
+ ...generated.mappings.slice(2).filter((m) => m.guest !== config.guestWorkspace && ![...roots, ...projects].some((p) => p.guest === m.guest)).map((m) => [' map ' + (m.readOnly ? 'ro' : 'rw'), m.host + ' -> ' + m.guest + (m.label ? ' (' + m.label + ')' : '')]),
298
383
  ['Generated', generated.wsbPath],
299
384
  ]);
300
385
  if (generateOnly)
@@ -302,18 +387,21 @@ export const launch = async (ctx, generateOnly = false, extras = {}) => {
302
387
  // Every folder the sandbox can write is snapshotted before it starts (the guard compares against this), and the
303
388
  // projects it starts with are recorded host-side for the commands that run later.
304
389
  saveRepoSnapshot(dataPath, run.workspaceHost, projects.map((p) => p.host), roots.map((r) => r.host));
305
- startRegistry(storageRoot, { launchId: generated.launchId, protections: protectionsOf(config), primary: { host: run.workspaceHost, guest: config.guestWorkspace }, roots, boot: projects });
390
+ startRegistry(storageRoot, { launchId: generated.launchId, protections: protectionsOf(booted), primary: { host: run.workspaceHost, guest: config.guestWorkspace }, roots, boot: projects,
391
+ atBoot: { memoryMb: generated.memoryMb, mappings: generated.mappings, widened: generated.widened } });
306
392
  rememberProject(storageRoot, run.workspaceHost);
393
+ startLife(storageRoot, dataPath, config.sandbox.keepAwake);
307
394
  startSandbox(generated.wsbPath);
308
395
  // Absolute: the watcher runs from another folder, and a relative --config there would make it exit silently.
309
396
  if (!watcherRunning(dataPath))
310
397
  spawnDetached(ctx.entry, ['_watch', ...(options.config ? ['--config', path.resolve(options.config)] : [])]);
311
398
  // Hiding runs alongside; a failure to find the window must never break the launch or an attach.
312
399
  const hidden = config.sandbox.headless ? hideSandboxWindow(storageRoot).catch(() => false) : null;
400
+ const headsUp = FIRST_BOOT(dataPath, generated, config);
313
401
  if (options.terminal || options.noAgent || extras.waitUntilReady) {
314
- if (firstBoot(dataPath))
315
- console.log(FIRST_BOOT);
316
- await waitForBoot(dataPath, generated.launchId);
402
+ if (headsUp)
403
+ console.log(headsUp);
404
+ await waitForBoot(dataPath, generated.launchId, Boolean(headsUp));
317
405
  if (extras.waitUntilReady)
318
406
  return 0;
319
407
  // Without an agent there is no session to attach to: the sandbox itself is the result.
@@ -323,8 +411,8 @@ export const launch = async (ctx, generateOnly = false, extras = {}) => {
323
411
  }
324
412
  return attachHere(ctx, await waitForStart(ctx, generated.launchId, 'Starting ' + run.harnessName));
325
413
  }
326
- if (firstBoot(dataPath))
327
- console.log(FIRST_BOOT);
414
+ if (headsUp)
415
+ console.log(headsUp + '\nFollow along: sandbox logs -f');
328
416
  if (hidden)
329
417
  console.log(await hidden ? 'Sandbox running headless. Open the window: sbx-show Attach here: sandbox claude -t' : 'Could not find the sandbox window to hide (it may still be starting); run sandbox hide in a moment.');
330
418
  else
@@ -6,3 +6,5 @@ export declare const config: (ctx: Context) => number;
6
6
  export declare const doctor: (ctx: Context) => number;
7
7
  export declare const claudeHost: (ctx: Context) => number;
8
8
  export declare const watcher: (ctx: Context) => Promise<void>;
9
+ /** `sandbox audit`: the audit log of the last days (audit.enabled), newest last. */
10
+ export declare const audit: (ctx: Context) => Promise<number>;