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.
- package/.sandbox-config.example.json +2 -1
- package/README.md +3 -2
- package/config.example.json +39 -5
- package/dist/cli/args.d.ts +3 -0
- package/dist/cli/args.js +3 -1
- package/dist/cli/boot-progress.d.ts +22 -0
- package/dist/cli/boot-progress.js +59 -0
- package/dist/cli/commands/accounts.js +2 -2
- package/dist/cli/commands/agent-commands.js +1 -1
- package/dist/cli/commands/context.js +2 -1
- package/dist/cli/commands/data/clean.js +6 -1
- package/dist/cli/commands/data/collect.js +3 -3
- package/dist/cli/commands/index.d.ts +1 -1
- package/dist/cli/commands/index.js +1 -1
- package/dist/cli/commands/launch.d.ts +5 -2
- package/dist/cli/commands/launch.js +113 -25
- package/dist/cli/commands/maintenance.d.ts +2 -0
- package/dist/cli/commands/maintenance.js +70 -4
- package/dist/cli/commands/project.d.ts +10 -6
- package/dist/cli/commands/project.js +42 -3
- package/dist/cli/commands/restart.js +17 -7
- package/dist/cli/commands/sessions.js +5 -4
- package/dist/cli/commands/user-config.js +1 -1
- package/dist/cli/index.js +6 -4
- package/dist/cli/manifest.js +6 -1
- package/dist/cli/menu/apps.d.ts +21 -0
- package/dist/cli/menu/apps.js +122 -0
- package/dist/cli/menu/command-form.d.ts +28 -0
- package/dist/cli/menu/command-form.js +219 -0
- package/dist/cli/menu/index.d.ts +26 -0
- package/dist/cli/menu/index.js +180 -0
- package/dist/cli/menu/settings/categories.d.ts +9 -0
- package/dist/cli/menu/settings/categories.js +28 -0
- package/dist/cli/menu/settings/config-file.d.ts +40 -0
- package/dist/cli/menu/settings/config-file.js +71 -0
- package/dist/cli/menu/settings/editors.d.ts +30 -0
- package/dist/cli/menu/settings/editors.js +305 -0
- package/dist/cli/menu/settings/index.d.ts +32 -0
- package/dist/cli/menu/settings/index.js +136 -0
- package/dist/cli/menu/settings/schema-node.d.ts +62 -0
- package/dist/cli/menu/settings/schema-node.js +104 -0
- package/dist/cli/{menu-multi.d.ts → menu/several.d.ts} +10 -2
- package/dist/cli/menu/several.js +102 -0
- package/dist/cli/progress.d.ts +15 -0
- package/dist/cli/progress.js +52 -0
- package/dist/cli/tui/app.d.ts +39 -0
- package/dist/cli/tui/app.js +52 -0
- package/dist/cli/tui/dialogs.d.ts +57 -0
- package/dist/cli/tui/dialogs.js +162 -0
- package/dist/cli/tui/frame.d.ts +18 -0
- package/dist/cli/tui/frame.js +27 -0
- package/dist/cli/tui/index.d.ts +7 -0
- package/dist/cli/tui/index.js +8 -0
- package/dist/cli/tui/list-screen.d.ts +101 -0
- package/dist/cli/tui/list-screen.js +299 -0
- package/dist/cli/tui/terminal.d.ts +37 -0
- package/dist/cli/tui/terminal.js +89 -0
- package/dist/cli/tui/text.d.ts +20 -0
- package/dist/cli/tui/text.js +105 -0
- package/dist/cli/tui/theme.d.ts +45 -0
- package/dist/cli/tui/theme.js +30 -0
- package/dist/core/config.js +10 -7
- package/dist/core/local-config.d.ts +31 -1
- package/dist/core/local-config.js +72 -7
- package/dist/core/project-policy.d.ts +50 -3
- package/dist/core/project-policy.js +90 -4
- package/dist/core/schema/apps.d.ts +40 -0
- package/dist/core/schema/apps.js +27 -0
- package/dist/core/schema/config.d.ts +69 -3
- package/dist/core/schema/config.js +18 -6
- package/dist/core/schema/profile.d.ts +5 -0
- package/dist/core/schema/profile.js +4 -2
- package/dist/core/schema/projects.d.ts +1 -0
- package/dist/core/schema/projects.js +1 -0
- package/dist/core/schema/sandbox.d.ts +29 -1
- package/dist/core/schema/sandbox.js +29 -1
- package/dist/core/schema/toolchains.d.ts +3 -0
- package/dist/core/schema/toolchains.js +6 -1
- package/dist/core/types.d.ts +24 -1
- package/dist/core/util.d.ts +2 -0
- package/dist/core/util.js +2 -0
- package/dist/core/workspace.d.ts +21 -4
- package/dist/core/workspace.js +99 -21
- package/dist/guest/apps/index.d.ts +10 -0
- package/dist/guest/apps/index.js +206 -0
- package/dist/guest/apps/resolvers.d.ts +8 -0
- package/dist/guest/apps/resolvers.js +90 -0
- package/dist/guest/audit-parse.d.ts +7 -0
- package/dist/guest/audit-parse.js +24 -0
- package/dist/guest/audit.d.ts +4 -0
- package/dist/guest/audit.js +47 -0
- package/dist/guest/daemon/relays.d.ts +1 -0
- package/dist/guest/daemon/relays.js +18 -0
- package/dist/guest/daemon.js +4 -0
- package/dist/guest/env.d.ts +7 -2
- package/dist/guest/env.js +17 -3
- package/dist/guest/helpers.d.ts +1 -1
- package/dist/guest/main.js +20 -3
- package/dist/guest/netproxy.d.ts +1 -0
- package/dist/guest/netproxy.js +100 -0
- package/dist/guest/ptyhost.js +14 -4
- package/dist/guest/setup/claude.d.ts +0 -4
- package/dist/guest/setup/claude.js +48 -2
- package/dist/guest/setup/network.d.ts +9 -0
- package/dist/guest/setup/network.js +97 -4
- package/dist/guest/setup/user.js +4 -0
- package/dist/guest/toolchains/index.js +29 -3
- package/dist/guest/toolchains/native/mesa.js +13 -3
- package/dist/guest/toolchains/plan.d.ts +3 -1
- package/dist/guest/toolchains/plan.js +53 -9
- package/dist/guest/toolchains/python.js +26 -2
- package/dist/guest/toolchains/vscode-profile.d.ts +30 -0
- package/dist/guest/toolchains/vscode-profile.js +69 -0
- package/dist/guest/toolchains/vscode.d.ts +17 -0
- package/dist/guest/toolchains/vscode.js +87 -1
- package/dist/host/attach.js +2 -1
- package/dist/host/audit/collect.d.ts +34 -0
- package/dist/host/audit/collect.js +126 -0
- package/dist/host/audit/log.d.ts +46 -0
- package/dist/host/audit/log.js +143 -0
- package/dist/host/channel.d.ts +2 -2
- package/dist/host/channel.js +3 -2
- package/dist/host/generate.d.ts +7 -1
- package/dist/host/generate.js +23 -6
- package/dist/host/join.d.ts +7 -0
- package/dist/host/join.js +23 -9
- package/dist/host/launch/mappings.d.ts +7 -2
- package/dist/host/launch/mappings.js +68 -18
- package/dist/host/launch/projects.d.ts +2 -0
- package/dist/host/launch/projects.js +11 -5
- package/dist/host/launch/stage.js +1 -1
- package/dist/host/motw.d.ts +17 -0
- package/dist/host/motw.js +74 -0
- package/dist/host/platform.js +3 -0
- package/dist/host/port-forward.d.ts +8 -0
- package/dist/host/port-forward.js +37 -0
- package/dist/host/projects.d.ts +25 -2
- package/dist/host/projects.js +54 -5
- package/dist/host/repo-guard.d.ts +10 -3
- package/dist/host/repo-guard.js +142 -14
- package/dist/host/stops.d.ts +67 -0
- package/dist/host/stops.js +126 -0
- package/dist/host/terminal.d.ts +8 -0
- package/dist/host/terminal.js +45 -1
- package/dist/host/workspace-view/index.d.ts +20 -0
- package/dist/host/workspace-view/index.js +53 -0
- package/dist/host/workspace-view/rules.d.ts +26 -0
- package/dist/host/workspace-view/rules.js +93 -0
- package/dist/host/workspace-view/view.d.ts +50 -0
- package/dist/host/workspace-view/view.js +281 -0
- package/dist/runtime/daemon.js +1387 -558
- package/dist/runtime/db.js +199 -14
- package/dist/runtime/main.js +3082 -631
- package/dist/runtime/netproxy.js +25948 -0
- package/dist/runtime/view.js +531 -148
- package/dist/selftest/audit.test.d.ts +2 -0
- package/dist/selftest/audit.test.js +57 -0
- package/dist/selftest/channel.test.js +45 -2
- package/dist/selftest/cli.test.js +47 -17
- package/dist/selftest/fixtures.d.ts +47 -1
- package/dist/selftest/fixtures.js +3 -1
- package/dist/selftest/generate.test.js +7 -2
- package/dist/selftest/index.js +6 -2
- package/dist/selftest/live-network.test.d.ts +2 -0
- package/dist/selftest/live-network.test.js +63 -0
- package/dist/selftest/live-terminal.test.d.ts +2 -0
- package/dist/selftest/live-terminal.test.js +169 -0
- package/dist/selftest/local-config.test.js +42 -4
- package/dist/selftest/mirror.test.js +36 -7
- package/dist/selftest/network.test.js +12 -0
- package/dist/selftest/projects.test.js +77 -8
- package/dist/selftest/repo-guard.test.js +59 -1
- package/dist/selftest/sync.test.js +50 -10
- package/dist/selftest/toolchains.test.js +30 -0
- package/dist/selftest/workspace-view.test.d.ts +2 -0
- package/dist/selftest/workspace-view.test.js +89 -0
- package/dist/shared/apps.d.ts +49 -0
- package/dist/shared/apps.js +114 -0
- package/dist/shared/console-input.d.ts +11 -0
- package/dist/shared/console-input.js +61 -0
- package/dist/shared/download.d.ts +3 -0
- package/dist/shared/download.js +25 -2
- package/dist/shared/guest-paths.d.ts +27 -0
- package/dist/shared/guest-paths.js +68 -0
- package/dist/shared/netpolicy.d.ts +27 -0
- package/dist/shared/netpolicy.js +51 -0
- package/dist/shared/projects.d.ts +27 -4
- package/dist/shared/projects.js +72 -13
- package/dist/shared/pty-client.d.ts +26 -4
- package/dist/shared/pty-client.js +204 -25
- package/dist/shared/rundir.d.ts +2 -0
- package/dist/shared/rundir.js +2 -0
- package/dist/shared/schema.d.ts +34 -0
- package/dist/shared/schema.js +14 -0
- package/dist/shared/system.d.ts +1 -0
- package/dist/shared/system.js +1 -0
- package/dist/sync/claude-mem.js +3 -2
- package/dist/sync/history.d.ts +6 -0
- package/dist/sync/history.js +26 -11
- package/dist/sync/watcher.js +78 -6
- package/package.json +2 -1
- package/sandbox/cargoshim.cs +56 -0
- package/sandbox/cpucap.cs +33 -0
- package/sandbox/cpucap.ps1 +4 -35
- package/sandbox/logon.cmd +6 -0
- package/sandbox/memlog.ps1 +34 -0
- package/sandbox/pagefile.cs +31 -0
- package/sandbox/pagefile.ps1 +21 -0
- package/sandbox/ptyhost.cs +162 -7
- package/schemas/config.schema.json +318 -3
- package/schemas/sandbox-config.schema.json +129 -14
- package/dist/cli/menu-multi.js +0 -82
- package/dist/cli/menu.d.ts +0 -14
- package/dist/cli/menu.js +0 -290
- package/dist/cli/settings/edit.d.ts +0 -13
- package/dist/cli/settings/edit.js +0 -19
- package/dist/cli/settings/index.d.ts +0 -8
- package/dist/cli/settings/index.js +0 -131
- package/dist/cli/settings/prompts.d.ts +0 -7
- package/dist/cli/settings/prompts.js +0 -48
- package/dist/cli/settings/schema.d.ts +0 -33
- package/dist/cli/settings/schema.js +0 -29
|
@@ -8,7 +8,8 @@
|
|
|
8
8
|
"rust": { "enabled": false }
|
|
9
9
|
},
|
|
10
10
|
"mappings": [
|
|
11
|
-
{ "host": "
|
|
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
|
|
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
|
|
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) |
|
package/config.example.json
CHANGED
|
@@ -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
|
-
"
|
|
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
|
}
|
package/dist/cli/args.d.ts
CHANGED
|
@@ -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/
|
|
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
|
|
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
|
-
/**
|
|
10
|
-
|
|
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
|
-
|
|
43
|
-
|
|
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
|
-
/**
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
275
|
-
|
|
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: ' +
|
|
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(
|
|
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(
|
|
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 (
|
|
315
|
-
console.log(
|
|
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 (
|
|
327
|
-
console.log(
|
|
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>;
|