windows-development-sandbox 0.0.0-stage → 1.0.5

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 (231) hide show
  1. package/.sandbox-config.example.json +14 -0
  2. package/README.md +78 -2
  3. package/config.example.json +332 -0
  4. package/dist/api.d.ts +31 -0
  5. package/dist/api.js +30 -0
  6. package/dist/bin/agent-sandbox.d.ts +2 -0
  7. package/dist/bin/agent-sandbox.js +3 -0
  8. package/dist/bin/claude-host.d.ts +2 -0
  9. package/dist/bin/claude-host.js +5 -0
  10. package/dist/bin/claude-sbx.d.ts +2 -0
  11. package/dist/bin/claude-sbx.js +3 -0
  12. package/dist/bin/codex-sbx.d.ts +2 -0
  13. package/dist/bin/codex-sbx.js +3 -0
  14. package/dist/bin/gemini-sbx.d.ts +2 -0
  15. package/dist/bin/gemini-sbx.js +3 -0
  16. package/dist/bin/sandbox.d.ts +2 -0
  17. package/dist/bin/sandbox.js +4 -0
  18. package/dist/bin/sbx-hide.d.ts +2 -0
  19. package/dist/bin/sbx-hide.js +3 -0
  20. package/dist/bin/sbx-show.d.ts +2 -0
  21. package/dist/bin/sbx-show.js +3 -0
  22. package/dist/cli/args.d.ts +32 -0
  23. package/dist/cli/args.js +111 -0
  24. package/dist/cli/commands/accounts.d.ts +2 -0
  25. package/dist/cli/commands/accounts.js +52 -0
  26. package/dist/cli/commands/context.d.ts +21 -0
  27. package/dist/cli/commands/context.js +54 -0
  28. package/dist/cli/commands/data.d.ts +46 -0
  29. package/dist/cli/commands/data.js +249 -0
  30. package/dist/cli/commands/index.d.ts +8 -0
  31. package/dist/cli/commands/index.js +8 -0
  32. package/dist/cli/commands/launch.d.ts +5 -0
  33. package/dist/cli/commands/launch.js +152 -0
  34. package/dist/cli/commands/maintenance.d.ts +8 -0
  35. package/dist/cli/commands/maintenance.js +125 -0
  36. package/dist/cli/commands/repair.d.ts +59 -0
  37. package/dist/cli/commands/repair.js +332 -0
  38. package/dist/cli/commands/sessions.d.ts +11 -0
  39. package/dist/cli/commands/sessions.js +113 -0
  40. package/dist/cli/commands/setup.d.ts +45 -0
  41. package/dist/cli/commands/setup.js +152 -0
  42. package/dist/cli/index.d.ts +14 -0
  43. package/dist/cli/index.js +96 -0
  44. package/dist/cli/manifest.d.ts +95 -0
  45. package/dist/cli/manifest.js +87 -0
  46. package/dist/cli/menu.d.ts +22 -0
  47. package/dist/cli/menu.js +187 -0
  48. package/dist/cli/settings.d.ts +11 -0
  49. package/dist/cli/settings.js +156 -0
  50. package/dist/core/config.d.ts +17 -0
  51. package/dist/core/config.js +103 -0
  52. package/dist/core/detect-databases.d.ts +15 -0
  53. package/dist/core/detect-databases.js +147 -0
  54. package/dist/core/detect-pwsh.d.ts +18 -0
  55. package/dist/core/detect-pwsh.js +111 -0
  56. package/dist/core/detect-toolchains.d.ts +30 -0
  57. package/dist/core/detect-toolchains.js +58 -0
  58. package/dist/core/local-config.d.ts +12 -0
  59. package/dist/core/local-config.js +64 -0
  60. package/dist/core/schema.d.ts +604 -0
  61. package/dist/core/schema.js +264 -0
  62. package/dist/core/types.d.ts +162 -0
  63. package/dist/core/types.js +1 -0
  64. package/dist/core/util.d.ts +53 -0
  65. package/dist/core/util.js +145 -0
  66. package/dist/core/workspace.d.ts +23 -0
  67. package/dist/core/workspace.js +162 -0
  68. package/dist/guest/daemon.d.ts +1 -0
  69. package/dist/guest/daemon.js +186 -0
  70. package/dist/guest/db.d.ts +1 -0
  71. package/dist/guest/db.js +60 -0
  72. package/dist/guest/env.d.ts +49 -0
  73. package/dist/guest/env.js +99 -0
  74. package/dist/guest/gitguard.d.ts +7 -0
  75. package/dist/guest/gitguard.js +60 -0
  76. package/dist/guest/gitrules.d.ts +2 -0
  77. package/dist/guest/gitrules.js +11 -0
  78. package/dist/guest/guide.d.ts +12 -0
  79. package/dist/guest/guide.js +68 -0
  80. package/dist/guest/main.d.ts +1 -0
  81. package/dist/guest/main.js +120 -0
  82. package/dist/guest/ptyhost.d.ts +40 -0
  83. package/dist/guest/ptyhost.js +94 -0
  84. package/dist/guest/setup/agents.d.ts +8 -0
  85. package/dist/guest/setup/agents.js +73 -0
  86. package/dist/guest/setup/claude.d.ts +21 -0
  87. package/dist/guest/setup/claude.js +138 -0
  88. package/dist/guest/setup/environment.d.ts +6 -0
  89. package/dist/guest/setup/environment.js +65 -0
  90. package/dist/guest/setup/hooks.d.ts +1 -0
  91. package/dist/guest/setup/hooks.js +36 -0
  92. package/dist/guest/setup/network.d.ts +10 -0
  93. package/dist/guest/setup/network.js +99 -0
  94. package/dist/guest/setup/policy.d.ts +3 -0
  95. package/dist/guest/setup/policy.js +18 -0
  96. package/dist/guest/setup/user.d.ts +7 -0
  97. package/dist/guest/setup/user.js +47 -0
  98. package/dist/guest/toolchains/binstall.d.ts +7 -0
  99. package/dist/guest/toolchains/binstall.js +55 -0
  100. package/dist/guest/toolchains/catalogue.d.ts +51 -0
  101. package/dist/guest/toolchains/catalogue.js +31 -0
  102. package/dist/guest/toolchains/cli.d.ts +10 -0
  103. package/dist/guest/toolchains/cli.js +64 -0
  104. package/dist/guest/toolchains/common.d.ts +37 -0
  105. package/dist/guest/toolchains/common.js +124 -0
  106. package/dist/guest/toolchains/databases.d.ts +7 -0
  107. package/dist/guest/toolchains/databases.js +226 -0
  108. package/dist/guest/toolchains/git.d.ts +3 -0
  109. package/dist/guest/toolchains/git.js +112 -0
  110. package/dist/guest/toolchains/go.d.ts +3 -0
  111. package/dist/guest/toolchains/go.js +33 -0
  112. package/dist/guest/toolchains/index.d.ts +13 -0
  113. package/dist/guest/toolchains/index.js +122 -0
  114. package/dist/guest/toolchains/java.d.ts +8 -0
  115. package/dist/guest/toolchains/java.js +96 -0
  116. package/dist/guest/toolchains/managers.d.ts +16 -0
  117. package/dist/guest/toolchains/managers.js +82 -0
  118. package/dist/guest/toolchains/msvc.d.ts +10 -0
  119. package/dist/guest/toolchains/msvc.js +216 -0
  120. package/dist/guest/toolchains/native.d.ts +12 -0
  121. package/dist/guest/toolchains/native.js +220 -0
  122. package/dist/guest/toolchains/node.d.ts +3 -0
  123. package/dist/guest/toolchains/node.js +132 -0
  124. package/dist/guest/toolchains/pwsh.d.ts +4 -0
  125. package/dist/guest/toolchains/pwsh.js +14 -0
  126. package/dist/guest/toolchains/python.d.ts +3 -0
  127. package/dist/guest/toolchains/python.js +47 -0
  128. package/dist/guest/toolchains/runtimes.d.ts +17 -0
  129. package/dist/guest/toolchains/runtimes.js +40 -0
  130. package/dist/guest/toolchains/scripting.d.ts +10 -0
  131. package/dist/guest/toolchains/scripting.js +137 -0
  132. package/dist/guest/toolchains/versions.d.ts +47 -0
  133. package/dist/guest/toolchains/versions.js +77 -0
  134. package/dist/guest/toolchains/vscode.d.ts +2 -0
  135. package/dist/guest/toolchains/vscode.js +53 -0
  136. package/dist/guest/view.d.ts +1 -0
  137. package/dist/guest/view.js +22 -0
  138. package/dist/host/accounts.d.ts +32 -0
  139. package/dist/host/accounts.js +119 -0
  140. package/dist/host/channel.d.ts +37 -0
  141. package/dist/host/channel.js +180 -0
  142. package/dist/host/claude-profile.d.ts +15 -0
  143. package/dist/host/claude-profile.js +103 -0
  144. package/dist/host/cleanup.d.ts +23 -0
  145. package/dist/host/cleanup.js +66 -0
  146. package/dist/host/generate.d.ts +15 -0
  147. package/dist/host/generate.js +185 -0
  148. package/dist/host/personalize.d.ts +10 -0
  149. package/dist/host/personalize.js +97 -0
  150. package/dist/host/platform.d.ts +17 -0
  151. package/dist/host/platform.js +114 -0
  152. package/dist/host/project-scan.d.ts +23 -0
  153. package/dist/host/project-scan.js +173 -0
  154. package/dist/host/repo-guard.d.ts +23 -0
  155. package/dist/host/repo-guard.js +128 -0
  156. package/dist/host/secrets.d.ts +14 -0
  157. package/dist/host/secrets.js +133 -0
  158. package/dist/host/terminal.d.ts +32 -0
  159. package/dist/host/terminal.js +127 -0
  160. package/dist/host/window.d.ts +4 -0
  161. package/dist/host/window.js +68 -0
  162. package/dist/runtime/daemon.js +20291 -0
  163. package/dist/runtime/db.js +20423 -0
  164. package/dist/runtime/gitguard.js +84 -0
  165. package/dist/runtime/main.js +22398 -0
  166. package/dist/runtime/view.js +19966 -0
  167. package/dist/selftest/cli.test.d.ts +2 -0
  168. package/dist/selftest/cli.test.js +30 -0
  169. package/dist/selftest/config.test.d.ts +3 -0
  170. package/dist/selftest/config.test.js +74 -0
  171. package/dist/selftest/data.test.d.ts +2 -0
  172. package/dist/selftest/data.test.js +73 -0
  173. package/dist/selftest/detect.test.d.ts +2 -0
  174. package/dist/selftest/detect.test.js +42 -0
  175. package/dist/selftest/fixtures.d.ts +232 -0
  176. package/dist/selftest/fixtures.js +63 -0
  177. package/dist/selftest/guest.test.d.ts +3 -0
  178. package/dist/selftest/guest.test.js +61 -0
  179. package/dist/selftest/harness.d.ts +14 -0
  180. package/dist/selftest/harness.js +36 -0
  181. package/dist/selftest/helpers.test.d.ts +2 -0
  182. package/dist/selftest/helpers.test.js +125 -0
  183. package/dist/selftest/host.test.d.ts +2 -0
  184. package/dist/selftest/host.test.js +71 -0
  185. package/dist/selftest/index.d.ts +1 -0
  186. package/dist/selftest/index.js +56 -0
  187. package/dist/selftest/live.test.d.ts +7 -0
  188. package/dist/selftest/live.test.js +127 -0
  189. package/dist/selftest/mirror.test.d.ts +3 -0
  190. package/dist/selftest/mirror.test.js +74 -0
  191. package/dist/selftest/security.test.d.ts +3 -0
  192. package/dist/selftest/security.test.js +143 -0
  193. package/dist/selftest/setup.test.d.ts +2 -0
  194. package/dist/selftest/setup.test.js +23 -0
  195. package/dist/selftest/sync.test.d.ts +3 -0
  196. package/dist/selftest/sync.test.js +82 -0
  197. package/dist/shared/download.d.ts +58 -0
  198. package/dist/shared/download.js +200 -0
  199. package/dist/shared/guest-paths.d.ts +30 -0
  200. package/dist/shared/guest-paths.js +61 -0
  201. package/dist/shared/netrules.d.ts +18 -0
  202. package/dist/shared/netrules.js +98 -0
  203. package/dist/shared/paths.d.ts +9 -0
  204. package/dist/shared/paths.js +22 -0
  205. package/dist/shared/pty-client.d.ts +86 -0
  206. package/dist/shared/pty-client.js +227 -0
  207. package/dist/shared/rundir.d.ts +30 -0
  208. package/dist/shared/rundir.js +86 -0
  209. package/dist/shared/safe-remove.d.ts +10 -0
  210. package/dist/shared/safe-remove.js +55 -0
  211. package/dist/shared/schema.d.ts +84 -0
  212. package/dist/shared/schema.js +51 -0
  213. package/dist/shared/system.d.ts +15 -0
  214. package/dist/shared/system.js +19 -0
  215. package/dist/sync/claude-mem.d.ts +8 -0
  216. package/dist/sync/claude-mem.js +77 -0
  217. package/dist/sync/history.d.ts +30 -0
  218. package/dist/sync/history.js +129 -0
  219. package/dist/sync/watcher.d.ts +13 -0
  220. package/dist/sync/watcher.js +116 -0
  221. package/hooks/post-agent/README.txt +1 -0
  222. package/hooks/pre-provision/README.txt +1 -0
  223. package/package.json +91 -4
  224. package/sandbox/bootstrap.ps1 +258 -0
  225. package/sandbox/gitshim.cs +35 -0
  226. package/sandbox/logon.cmd +16 -0
  227. package/sandbox/ptyhost.cs +603 -0
  228. package/sandbox/trust.cmd +35 -0
  229. package/sandbox/vswhere.cs +83 -0
  230. package/schemas/config.schema.json +1062 -0
  231. package/schemas/sandbox-config.schema.json +952 -0
@@ -0,0 +1,152 @@
1
+ // `launch` / `generate`, and what sandbox claude runs: start a sandbox, or another session in the running one.
2
+ import { activateAccount, activeAccount, defaultAccount } from '../../host/accounts.js';
3
+ import { attachTerminal, findUnwatchedSession, requestSession, resetRunDir, waitForReady, waitForSession } from '../../host/channel.js';
4
+ import { describePrune, pruneStorage } from '../../host/cleanup.js';
5
+ import { generateSandbox } from '../../host/generate.js';
6
+ import { findSecretFiles } from '../../host/secrets.js';
7
+ import { checkRepo, saveRepoSnapshot } from '../../host/repo-guard.js';
8
+ import { assertCanLaunch, insideSandbox, isSandboxRunning, probeHost, spawnDetached, startSandbox } from '../../host/platform.js';
9
+ import { ensureTerminal } from '../../host/terminal.js';
10
+ import { hideSandboxWindow, showSandboxWindow } from '../../host/window.js';
11
+ import { readSandboxStatus } from '../../shared/rundir.js';
12
+ import path from 'node:path';
13
+ import fs from 'node:fs';
14
+ import { syncBeforeLaunch, watcherRunning } from '../../sync/watcher.js';
15
+ import { errMsg, fail, sleep } from '../../core/util.js';
16
+ import { askYesNo, resolveRun } from '../../core/workspace.js';
17
+ import { currentGuestDir, sandboxAlive, table } from './context.js';
18
+ /** Attaches this terminal; with attach.autoHide the sandbox window hides meanwhile (best effort, never fails the attach). */
19
+ export const attachHere = (ctx, session) => {
20
+ if (ctx.loaded.config.attach.autoHide && !insideSandbox())
21
+ void hideSandboxWindow(ctx.storageRoot, 120).catch(() => false);
22
+ return attachTerminal(session);
23
+ };
24
+ /** Nothing has been installed into this data set yet: the first start downloads every toolchain once. */
25
+ const firstBoot = (dataPath) => !fs.existsSync(path.join(dataPath, 'tools', 'resolved-versions.json'));
26
+ const FIRST_BOOT = 'First start for this storage: the sandbox downloads and installs the toolchains, databases and agent this project needs.\n' +
27
+ 'That takes a few minutes, once. Everything is kept, so every later start reuses it and takes seconds. Progress:';
28
+ /** The sandbox writes sandbox.json a few seconds into its boot; a second launch right after the first waits for it. */
29
+ const runningSandboxStatus = async (dataPath) => {
30
+ for (let i = 0; i < 60; i++) {
31
+ const status = readSandboxStatus(dataPath);
32
+ if (status)
33
+ return status;
34
+ await sleep(1000);
35
+ }
36
+ 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.');
37
+ };
38
+ /** A sandbox is already running: open another agent session in it (new tab inside; optionally attach here). */
39
+ const launchIntoRunning = async (ctx) => {
40
+ const { options, loaded, dataPath } = ctx;
41
+ const status = await runningSandboxStatus(dataPath);
42
+ const ready = status.phase === 'ready' ? status : await waitForReady(dataPath, status.launchId, sandboxAlive);
43
+ const cwd = currentGuestDir(ctx, ready.workspaceHost, ready.guestWorkspace);
44
+ const harness = options.harness ?? loaded.config.defaultHarness;
45
+ // -t with nothing new for the agent: pick up this folder's session if no terminal on this PC shows it (its tab was
46
+ // closed, or you detached). Someone already attached, --new, or agent arguments: a new session, as before.
47
+ const reuse = options.terminal && !options.newSession && options.passthrough.length === 0 && loaded.config.attach.reuseSession;
48
+ const existing = reuse ? await findUnwatchedSession(dataPath, harness, cwd) : null;
49
+ if (existing) {
50
+ console.log('Reattaching to ' + harness + ' session ' + existing.id + ' in ' + cwd + ' (no other terminal is attached; --new starts another).');
51
+ return attachHere(ctx, existing);
52
+ }
53
+ console.log('Sandbox already running: starting another ' + harness + ' session in ' + cwd);
54
+ const session = await waitForSession(dataPath, requestSession(dataPath, harness, options.passthrough, cwd), sandboxAlive);
55
+ if (options.terminal)
56
+ return attachHere(ctx, session);
57
+ console.log('Session ' + session.id + ' opened in a new tab inside the sandbox. Attach here with: sandbox attach ' + session.id + (ready.headless ? ' (headless: sbx-show opens the window)' : ''));
58
+ return 0;
59
+ };
60
+ const syncSummary = (config, dataPath, workspaceHost) => {
61
+ try {
62
+ const s = syncBeforeLaunch(config, dataPath, workspaceHost);
63
+ return s.history + ' file(s) of history in, claude-mem ' + s.claudeMem;
64
+ }
65
+ catch (e) {
66
+ return 'FAILED: ' + errMsg(e);
67
+ }
68
+ };
69
+ /** Everything a real launch does before the sandbox starts (skipped by `generate`, which only writes files). */
70
+ const prepare = async (ctx, run) => {
71
+ const { options, dataPath, storageRoot } = ctx;
72
+ const { config } = run;
73
+ const account = options.account ?? defaultAccount(storageRoot);
74
+ if (account && run.harnessName === 'claude' && account !== activeAccount(dataPath))
75
+ activateAccount(storageRoot, dataPath, account);
76
+ const terminal = config.provisioning.terminal.enabled ? await ensureTerminal(dataPath, config.provisioning.refresh, { pwsh: config.toolchains.pwsh.enabled !== false }) : { exe: null, message: 'disabled' };
77
+ const history = syncSummary(config, dataPath, run.workspaceHost);
78
+ const guarded = checkRepo(dataPath);
79
+ if (guarded.length)
80
+ console.warn('\x1b[33mSECURITY: neutralised ' + guarded.length + ' change(s) the last sandbox made that could run code on this PC (see sandbox status).\x1b[0m');
81
+ saveRepoSnapshot(dataPath, run.workspaceHost);
82
+ resetRunDir(dataPath);
83
+ return { account, terminal: terminal.message, history, pruned: describePrune(pruneStorage(storageRoot)) };
84
+ };
85
+ export const launch = async (ctx, generateOnly = false) => {
86
+ const { options, loaded, packageRoot, dataPath, storageRoot } = ctx;
87
+ // attach.auto: an agent started from an interactive terminal attaches it, as with -t.
88
+ if (loaded.config.attach.auto && !options.noAgent && process.stdin.isTTY && process.stdout.isTTY)
89
+ options.terminal = true;
90
+ if (!generateOnly && isSandboxRunning()) {
91
+ if (!options.noAgent)
92
+ return launchIntoRunning(ctx);
93
+ // Only one Windows Sandbox can run: "start" on a running one brings its window up instead.
94
+ console.log('A sandbox is already running' + (showSandboxWindow(storageRoot) ? '; its window is in front now.' : '.') + ' Start an agent in it with sandbox claude (or sandbox <agent>); stop it with sandbox stop.');
95
+ return 0;
96
+ }
97
+ const run = await resolveRun({
98
+ packageRoot, loaded, cwd: process.cwd(), workspace: options.workspace, harness: options.harness, noAgent: options.noAgent,
99
+ noLocalConfig: options.noLocalConfig, trustConfig: options.trustConfig, scripts: options.scripts, databases: options.databases, headless: options.headless,
100
+ nodeVersion: options.node, pythonVersion: options.python, ask: askYesNo,
101
+ });
102
+ const { config } = run;
103
+ if (!generateOnly)
104
+ assertCanLaunch();
105
+ const prepared = generateOnly ? null : await prepare(ctx, run);
106
+ const generated = generateSandbox({ packageRoot, run, harnessArgs: options.passthrough, probe: probeHost(config, run.harnessName) });
107
+ for (const note of run.notes)
108
+ console.log(note);
109
+ const secretFiles = config.secrets.warnFiles ? findSecretFiles(run.workspaceHost) : [];
110
+ if (secretFiles.length)
111
+ console.warn('\x1b[33mHeads-up: the agent can read these files in your workspace, which often hold secrets: ' + secretFiles.slice(0, 12).join(', ') + (secretFiles.length > 12 ? ', ...' : '') + '. Move real credentials out of the folder (or use test ones). Silence with secrets.warnFiles = false.\x1b[0m');
112
+ table([
113
+ ['Workspace', run.workspaceHost + ' -> ' + config.guestWorkspace + (config.readOnly ? ' (read-only)' : '')],
114
+ ['Agent', run.harnessName + (prepared?.account ? ' (account: ' + prepared.account + ')' : '') + ' Memory: ' + generated.memoryMb + ' MB Network: ' + config.sandbox.networking + (config.sandbox.headless ? ' HEADLESS' : '')],
115
+ ['Node', run.versions.node.spec + ' (' + run.versions.node.source + ') Python: ' + run.versions.python.spec + ' (' + run.versions.python.source + ')'],
116
+ ...(prepared ? [['Terminal', prepared.terminal], ['History', prepared.history], ['Cleanup', prepared.pruned]] : []),
117
+ ['Toolchains', Object.entries(generated.toolchains).filter(([, d]) => d.on).map(([name, d]) => name + ' (' + d.reason + ')').join(', ') || 'Node only (nothing else used by this project)'],
118
+ ...(generated.databases.length
119
+ ? generated.databases.map((d, i) => [i ? '' : 'Databases', d.database + (d.database === 'sqlserver' ? ' (cannot run in the sandbox; see hostAccess.forwards)' : ' on port ' + d.port) + ' because ' + d.reasons[0] + (d.reasons.length > 1 ? ' (+' + (d.reasons.length - 1) + ' more)' : '')])
120
+ : [['Databases', 'none needed (databases.<name>.enabled = true adds one)']]),
121
+ ['Desktop', generated.personalization],
122
+ ['Claude', generated.profile.staged.length ? generated.profile.staged.join(', ') + (generated.profile.credentials ? ' (INCLUDING YOUR LOGIN)' : '') : 'no settings copied'],
123
+ ['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'],
124
+ ...generated.mappings.slice(3).map((m) => [' map ' + (m.readOnly ? 'ro' : 'rw'), m.host + ' -> ' + m.guest]),
125
+ ['Generated', generated.wsbPath],
126
+ ]);
127
+ if (generateOnly)
128
+ return 0;
129
+ startSandbox(generated.wsbPath);
130
+ // Absolute: the watcher runs from another folder, and a relative --config there would make it exit silently.
131
+ if (!watcherRunning(dataPath))
132
+ spawnDetached(ctx.entry, ['_watch', ...(options.config ? ['--config', path.resolve(options.config)] : [])]);
133
+ // Hiding runs alongside; a failure to find the window must never break the launch or an attach.
134
+ const hidden = config.sandbox.headless ? hideSandboxWindow(storageRoot).catch(() => false) : null;
135
+ if (options.terminal || options.noAgent) {
136
+ console.log(firstBoot(dataPath) ? FIRST_BOOT : 'Waiting for the sandbox (everything it needs is installed already; this takes seconds) ...');
137
+ await waitForReady(dataPath, generated.launchId, sandboxAlive);
138
+ // Without an agent there is no session to attach to: the sandbox itself is the result.
139
+ if (options.noAgent) {
140
+ console.log('Sandbox ready. Its window has a terminal in ' + config.guestWorkspace + '; from here: sandbox exec -- <command>, sandbox stop.');
141
+ return 0;
142
+ }
143
+ return attachHere(ctx, await waitForSession(dataPath, generated.launchId, sandboxAlive));
144
+ }
145
+ if (firstBoot(dataPath))
146
+ console.log(FIRST_BOOT);
147
+ if (hidden)
148
+ 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.');
149
+ else
150
+ console.log('Sandbox starting. ' + run.harnessName + ' opens in Windows Terminal inside it. Attach here any time: sandbox claude -t');
151
+ return 0;
152
+ };
@@ -0,0 +1,8 @@
1
+ import { type Context } from './context.js';
2
+ export declare const sync: (ctx: Context) => number;
3
+ export declare const logs: (ctx: Context) => Promise<number>;
4
+ export declare const clean: (ctx: Context) => number;
5
+ export declare const config: (ctx: Context) => number;
6
+ export declare const doctor: (ctx: Context) => number;
7
+ export declare const claudeHost: (ctx: Context) => number;
8
+ export declare const watcher: (ctx: Context) => Promise<void>;
@@ -0,0 +1,125 @@
1
+ // sync, logs, clean, config, doctor, host-claude and the internal sync watcher.
2
+ import fs from 'node:fs';
3
+ import path from 'node:path';
4
+ import { pathToFileURL } from 'node:url';
5
+ import _ from 'lodash';
6
+ import { DEFAULTS, userConfigPath, userDir } from '../../core/config.js';
7
+ import { describeOverrides, isTrusted, LOCAL_CONFIG_NAME, normalizeOverrides, readLocalConfig, trustStorePath } from '../../core/local-config.js';
8
+ import { fail, safeText, sleep, writeJson } from '../../core/util.js';
9
+ import { describePrune, pruneStorage } from '../../host/cleanup.js';
10
+ import { doctorRows, hostClaude, isSandboxRunning } from '../../host/platform.js';
11
+ import { readSandboxStatus } from '../../shared/rundir.js';
12
+ import { syncBack, watch } from '../../sync/watcher.js';
13
+ import { table } from './context.js';
14
+ export const sync = (ctx) => {
15
+ const running = isSandboxRunning();
16
+ const result = syncBack(ctx.dataPath, !running);
17
+ console.log('History: ' + result.history + ' item(s) synced back. claude-mem: ' + result.claudeMem + (running ? ' (claude-mem syncs when the sandbox closes)' : ''));
18
+ return 0;
19
+ };
20
+ const newestLog = (dir) => {
21
+ if (!fs.existsSync(dir))
22
+ return null;
23
+ const logs = fs.readdirSync(dir).filter((f) => /^(guest|daemon|bootstrap)-.*\.log$/.test(f) && fs.lstatSync(path.join(dir, f)).isFile());
24
+ const newest = _.maxBy(logs, (f) => fs.statSync(path.join(dir, f)).mtimeMs);
25
+ return newest ? path.join(dir, newest) : null;
26
+ };
27
+ export const logs = async (ctx) => {
28
+ const dir = path.join(ctx.dataPath, 'logs');
29
+ const file = newestLog(dir) ?? fail('No sandbox logs yet in ' + dir + '.', 'Logs appear after the first start (sandbox claude).');
30
+ console.log('== ' + file + ' ==');
31
+ let printed = 0;
32
+ // The log is written inside the sandbox: read only its tail, and make it safe for your terminal.
33
+ const print = () => {
34
+ const size = fs.statSync(file).size;
35
+ const from = printed === 0 ? Math.max(0, size - 64 * 1024) : printed;
36
+ if (size <= from)
37
+ return;
38
+ const fd = fs.openSync(file, 'r');
39
+ const buffer = Buffer.alloc(Math.min(size - from, 4 * 1024 * 1024));
40
+ fs.readSync(fd, buffer, 0, buffer.length, from);
41
+ fs.closeSync(fd);
42
+ const text = safeText(buffer.toString('utf8'));
43
+ process.stdout.write(printed === 0 ? text.split('\n').slice(-120).join('\n') : text);
44
+ printed = from + buffer.length;
45
+ };
46
+ print();
47
+ while (ctx.options.follow) {
48
+ await sleep(1000);
49
+ print();
50
+ }
51
+ return 0;
52
+ };
53
+ export const clean = (ctx) => {
54
+ const running = isSandboxRunning() ? readSandboxStatus(ctx.dataPath)?.launchId : undefined;
55
+ console.log('Cleanup: ' + describePrune(pruneStorage(ctx.storageRoot, { launchDays: ctx.options.days, keep: _.compact([running]) })));
56
+ return 0;
57
+ };
58
+ const initConfig = () => {
59
+ const file = userConfigPath();
60
+ if (fs.existsSync(file))
61
+ fail(file + ' already exists.');
62
+ fs.mkdirSync(path.join(userDir(), 'hooks', 'pre-agent'), { recursive: true });
63
+ // The schema sits next to the file (a local path), so editors trust it and show docs and autocomplete.
64
+ writeJson(file, { $schema: './config.schema.json', ...DEFAULTS });
65
+ console.log('Created ' + file + '. It lists every setting with its default; delete the ones you do not change.');
66
+ console.log('Your own hook scripts go in ' + path.join(userDir(), 'hooks') + '\\{pre-provision,pre-agent,post-agent}.');
67
+ console.log('For editor help in every project\'s .sandbox-config.json, run: sandbox config schema');
68
+ return 0;
69
+ };
70
+ /**
71
+ * VS Code setting that maps every .sandbox-config.json (and the user config) to the local schema files, so project
72
+ * files need no "$schema" line and nothing is fetched from the internet (remote schemas need trusting).
73
+ */
74
+ const printSchemaSettings = () => {
75
+ const url = (name) => pathToFileURL(path.join(userDir(), name)).href;
76
+ const setting = {
77
+ 'json.schemas': [
78
+ { fileMatch: ['**/.sandbox-config.json'], url: url('sandbox-config.schema.json') },
79
+ { fileMatch: ['**/.agent-sandbox/config.json'], url: url('config.schema.json') },
80
+ ],
81
+ };
82
+ console.log('Add this to your VS Code settings.json (Ctrl+Shift+P > "Preferences: Open User Settings (JSON)"):\n');
83
+ console.log(JSON.stringify(setting, null, 2).slice(2, -2));
84
+ console.log('\nThe schema files live in ' + userDir() + ' and are refreshed whenever the sandbox command runs.');
85
+ return 0;
86
+ };
87
+ /**
88
+ * `sandbox config check`: the folder's .sandbox-config.json validated and described (risky settings flagged, approval
89
+ * state), without launching. Fails with the same message a launch would give when the file is not valid.
90
+ */
91
+ const checkLocalConfig = (ctx) => {
92
+ const dir = path.resolve(ctx.options.workspace ?? process.cwd());
93
+ const file = readLocalConfig(dir);
94
+ if (!file) {
95
+ console.log('No ' + LOCAL_CONFIG_NAME + ' in ' + dir + '; the sandbox uses your own settings only.');
96
+ return 0;
97
+ }
98
+ const overrides = normalizeOverrides(file.data, dir, file.path);
99
+ const approved = isTrusted(trustStorePath(ctx.storageRoot), file.path, file.hash);
100
+ console.log(file.path + ': valid. ' + (approved ? 'Approved (this exact version).' : 'Not approved yet: the next launch asks you.') + '\n');
101
+ for (const line of describeOverrides(overrides))
102
+ console.log(' ' + safeText(line));
103
+ return 0;
104
+ };
105
+ export const config = (ctx) => {
106
+ const [action = 'show'] = ctx.options.positionals;
107
+ switch (action) {
108
+ case 'path':
109
+ console.log(ctx.loaded.configPath ?? userConfigPath() + ' (does not exist yet)');
110
+ return 0;
111
+ case 'init': return initConfig();
112
+ case 'show':
113
+ console.log(JSON.stringify(ctx.loaded.config, null, 2));
114
+ return 0;
115
+ case 'schema': return printSchemaSettings();
116
+ case 'check': return checkLocalConfig(ctx);
117
+ default: fail('Unknown config action "' + action + '".', 'Use one of: sandbox config show | path | init | schema | check');
118
+ }
119
+ };
120
+ export const doctor = (ctx) => {
121
+ table(doctorRows(ctx.loaded.config, ctx.options.harness ?? ctx.loaded.config.defaultHarness, ctx.storageRoot, ctx.loaded.configPath));
122
+ return 0;
123
+ };
124
+ export const claudeHost = (ctx) => hostClaude(ctx.storageRoot, ctx.options.personal, ctx.options.passthrough);
125
+ export const watcher = (ctx) => watch(ctx.dataPath);
@@ -0,0 +1,59 @@
1
+ import { UserError } from '../../core/util.js';
2
+ import type { CliOptions } from '../args.js';
3
+ import { type Context } from './context.js';
4
+ export type RepairStatus = 'ok' | 'fixed' | 'needs-you' | 'warn' | 'skipped';
5
+ export interface RepairResult {
6
+ name: string;
7
+ status: RepairStatus;
8
+ detail: string;
9
+ }
10
+ export interface RepairEnv {
11
+ ctx: Context;
12
+ running: boolean;
13
+ dataPath: string;
14
+ storageRoot: string;
15
+ toolsDir: string;
16
+ configError: UserError | null;
17
+ }
18
+ export interface RepairCheck {
19
+ name: string;
20
+ run: (env: RepairEnv) => Omit<RepairResult, 'name'>;
21
+ }
22
+ /** Interrupted downloads and copies: `<name>.staging-<hex>` and `node-staging`, in tools\ and one folder down (tools\go\1.23.staging-ab12). */
23
+ export declare const findStagingLeftovers: (toolsDir: string) => string[];
24
+ /**
25
+ * A toolchain folder and the program it must contain. `versions` means one folder per installed version (tools\go\1.23.2);
26
+ * `match` limits which version folders count (tools\nvm also holds settings and nvm.exe).
27
+ */
28
+ export interface ToolRule {
29
+ folder: string;
30
+ exe: string[];
31
+ versions?: boolean;
32
+ match?: RegExp;
33
+ }
34
+ export declare const TOOL_RULES: ToolRule[];
35
+ /** Toolchain folders that exist but lack their main program (an install that stopped half way). Never enters links. */
36
+ export declare const findHalfInstalled: (toolsDir: string, rules?: ToolRule[]) => {
37
+ path: string;
38
+ missing: string;
39
+ }[];
40
+ /**
41
+ * Every link or junction under `root` (the links themselves; nothing behind them is visited). Before descending into
42
+ * a folder its real path must still be inside root: a folder swapped for a junction between two looks is not followed.
43
+ */
44
+ export declare const findLinks: (root: string) => string[];
45
+ /** tools\resolved-versions.json is a JSON object (anything else makes the guest stumble). */
46
+ export declare const resolvedVersionsValid: (text: string) => boolean;
47
+ /** Package names from `npm ls -g --json`; [] when the output is not what npm prints. */
48
+ export declare const globalPackages: (json: string) => string[];
49
+ export declare const pidAlive: (pid: number) => boolean;
50
+ /** Sync-watcher pid files (storage\run\watcher-*.pid) whose process is gone (or whose content is not a pid). */
51
+ export declare const deadPidFiles: (runDir: string, alive?: (pid: number) => boolean) => string[];
52
+ /** Runs every check in order; a check that throws is reported, never stops the rest. */
53
+ export declare const runChecks: (env: RepairEnv, checks?: RepairCheck[]) => RepairResult[];
54
+ export declare const repair: (ctx: Context, configError?: UserError | null) => number;
55
+ /**
56
+ * Entry point that works even when the config file is broken (createContext would throw): falls back to the built-in
57
+ * defaults so the other checks still run, and reports the config problem as the first check.
58
+ */
59
+ export declare const repairCommand: (packageRoot: string, entry: string, options: CliOptions) => number;