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,111 @@
1
+ // Command-line parsing with commander. parseCli() only parses (no side effects), so it is easy to test; cli/index.ts
2
+ // dispatches the result. sandbox claude & co parse as `launch`: every option launch does not own goes to the agent, in order.
3
+ import { Command, InvalidArgumentError, Option } from 'commander';
4
+ import { fail } from '../core/util.js';
5
+ import { COMMANDS } from './manifest.js';
6
+ /** Reserved spelling for our launch options that can never clash with an agent's flags: --sbx-terminal, ... */
7
+ const SBX_PREFIX = '--sbx-';
8
+ const positiveNumber = (value) => {
9
+ const n = Number(value);
10
+ if (!Number.isFinite(n) || n <= 0)
11
+ throw new InvalidArgumentError('expected a positive number');
12
+ return n;
13
+ };
14
+ const collect = (value, previous = []) => [...previous, value];
15
+ /** One commander option from its manifest entry. */
16
+ const toOption = (o) => {
17
+ const flags = (o.short ? o.short + ', ' : '') + o.flag + (o.type === 'boolean' ? '' : ' <' + (o.value ?? 'value') + '>');
18
+ const option = new Option(flags, o.description);
19
+ if (o.type === 'number')
20
+ option.argParser(positiveNumber);
21
+ if (o.type === 'list')
22
+ option.argParser(collect);
23
+ if (o.default !== undefined)
24
+ option.default(o.default);
25
+ return option;
26
+ };
27
+ /** Every command, its options and help text, from manifest.ts. Actions only record what was parsed. */
28
+ export const buildProgram = (record) => {
29
+ const program = new Command('sandbox')
30
+ .description('A disposable Windows Sandbox for development, with or without an AI agent, for the folder you are in. Run it with no arguments for the menu.')
31
+ .enablePositionalOptions()
32
+ .showHelpAfterError('(run with --help for usage)')
33
+ .exitOverride();
34
+ for (const spec of COMMANDS) {
35
+ const cmd = program.command(spec.name, { hidden: spec.hidden }).description(spec.summary).option('--config <file>', 'config file (default: ~/.agent-sandbox/config.json)');
36
+ for (const option of spec.options)
37
+ cmd.addOption(toOption(option));
38
+ if (spec.json)
39
+ cmd.option('--json', 'machine-readable output');
40
+ for (const arg of spec.args)
41
+ cmd.argument((arg.required ? '<' : '[') + arg.name + (arg.variadic ? '...' : '') + (arg.required ? '>' : ']'), arg.description);
42
+ if (spec.passthrough === 'agent')
43
+ cmd.argument('[agentArgs...]', 'passed to the agent').allowUnknownOption().allowExcessArguments();
44
+ if (spec.passthrough === 'command')
45
+ cmd.passThroughOptions();
46
+ cmd.action((...args) => record(spec.name, args[args.length - 1]));
47
+ }
48
+ return program;
49
+ };
50
+ /** Turns `--sbx-<option>` into `--<option>` (only for options launch knows; anything else is an error). */
51
+ const expandReserved = (argv, known) => {
52
+ const end = argv.indexOf('--');
53
+ return argv.map((token, i) => {
54
+ if ((end !== -1 && i > end) || !token.startsWith(SBX_PREFIX))
55
+ return token;
56
+ const [flag, value] = token.split(/=(.*)/s);
57
+ const option = '--' + flag.slice(SBX_PREFIX.length);
58
+ if (!known.has(option))
59
+ fail('Unknown option: ' + flag, 'Reserved --sbx- options: ' + [...known].map((o) => o.replace('--', SBX_PREFIX)).join(', '));
60
+ return value === undefined ? option : option + '=' + value;
61
+ });
62
+ };
63
+ /** Parses argv into the command name and options. Throws commander errors for unknown commands/options. */
64
+ export const parseCli = (argv, wrapper = false) => {
65
+ let parsed = null;
66
+ const program = buildProgram((name, cmd) => { parsed = { name, cmd }; });
67
+ const launchFlags = new Set(program.commands.find((c) => c.name() === 'launch').options.map((o) => o.long).filter(Boolean));
68
+ const args = expandReserved(wrapper ? ['launch', ...argv] : argv, launchFlags);
69
+ program.parse(args, { from: 'user' });
70
+ if (!parsed)
71
+ return { ...defaults(), command: '' };
72
+ const { name, cmd } = parsed;
73
+ const o = { ...program.opts(), ...cmd.opts() };
74
+ const forAgent = ['launch', 'start', 'generate', 'host-claude'].includes(name);
75
+ // Commander keeps our `--` separator among the operands when unknown options are allowed; it is ours, not the agent's.
76
+ const separator = forAgent ? cmd.args.indexOf('--') : -1;
77
+ const operands = separator === -1 ? cmd.args : [...cmd.args.slice(0, separator), ...cmd.args.slice(separator + 1)];
78
+ return {
79
+ ...defaults(),
80
+ // `start` is launch without an agent.
81
+ command: name === 'start' ? 'launch' : name,
82
+ positionals: forAgent || name === 'exec' ? [] : operands,
83
+ passthrough: forAgent || name === 'exec' ? operands : [],
84
+ config: o.config,
85
+ workspace: o.workspace,
86
+ harness: o.harness,
87
+ account: o.account,
88
+ node: o.node,
89
+ python: o.python,
90
+ scripts: o.script ?? [],
91
+ databases: (o.database ?? []).flatMap((d) => d.split(',')).map((d) => d.trim()).filter(Boolean),
92
+ headless: o.window ? false : o.headless ? true : undefined,
93
+ terminal: Boolean(o.terminal),
94
+ newSession: Boolean(o.new),
95
+ noAgent: o.agent === false || name === 'start',
96
+ trustConfig: Boolean(o.trustConfig),
97
+ noLocalConfig: o.localConfig === false,
98
+ personal: Boolean(o.personal),
99
+ fromHost: Boolean(o.fromHost),
100
+ noRestart: o.restart === false,
101
+ follow: Boolean(o.follow),
102
+ json: Boolean(o.json),
103
+ yes: Boolean(o.yes),
104
+ timeout: o.timeout ?? 600,
105
+ days: o.days ?? 7,
106
+ };
107
+ };
108
+ const defaults = () => ({
109
+ command: '', positionals: [], passthrough: [], scripts: [], databases: [], terminal: false, newSession: false, noAgent: false, trustConfig: false, noLocalConfig: false,
110
+ personal: false, fromHost: false, noRestart: false, follow: false, json: false, yes: false, timeout: 600, days: 7,
111
+ });
@@ -0,0 +1,2 @@
1
+ import { type Context } from './context.js';
2
+ export declare const account: (ctx: Context) => Promise<number>;
@@ -0,0 +1,52 @@
1
+ // `account list | save | use | default | remove`
2
+ import { activateAccount, listAccounts, removeAccount, saveAccount, setDefaultAccount } from '../../host/accounts.js';
3
+ import { restartSessions, runningSessions } from '../../host/channel.js';
4
+ import { hostClaudeDir, hostGlobalStateFile } from '../../host/claude-profile.js';
5
+ import { isSandboxRunning } from '../../host/platform.js';
6
+ import { fail } from '../../core/util.js';
7
+ import { printJson, sandboxAlive, table } from './context.js';
8
+ const label = (a) => '"' + a.name + '"' + (a.email ? ' (' + a.email + ')' : '');
9
+ /** This PC's Claude login location (config folder + global state file). */
10
+ const hostLogin = (ctx) => {
11
+ const profile = ctx.loaded.config.claudeProfile;
12
+ return { claudeDir: hostClaudeDir(profile), globalStateFile: hostGlobalStateFile(profile) };
13
+ };
14
+ const list = (ctx) => {
15
+ const rows = listAccounts(ctx.storageRoot, ctx.dataPath);
16
+ if (printJson(ctx, rows))
17
+ return 0;
18
+ if (rows.length === 0) {
19
+ console.log('No saved accounts. Sign in inside the sandbox, then: sandbox account save <name> (or --from-host to save this PC\'s login)');
20
+ return 0;
21
+ }
22
+ table([['', 'NAME', 'EMAIL', 'SAVED'], ...rows.map((a) => [(a.isActive ? '*' : ' ') + (a.isDefault ? 'D' : ' '), a.name, a.email ?? '-', a.savedAt.slice(0, 16).replace('T', ' ')])]);
23
+ console.log('\n* = active in the sandbox D = default for new launches');
24
+ return 0;
25
+ };
26
+ const use = async (ctx, name) => {
27
+ console.log('Sandbox now uses ' + label(activateAccount(ctx.storageRoot, ctx.dataPath, name)) + '.');
28
+ const claudeRunning = isSandboxRunning() && runningSessions(ctx.dataPath).some((s) => s.harness === 'claude');
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.');
31
+ return 0;
32
+ };
33
+ export const account = async (ctx) => {
34
+ const [action = 'list', name] = ctx.options.positionals;
35
+ const needName = () => name ?? fail('Missing account name.', 'Usage: sandbox account ' + action + ' <name>');
36
+ switch (action) {
37
+ case 'list': return list(ctx);
38
+ case 'save':
39
+ console.log('Saved account ' + label(saveAccount(ctx.storageRoot, ctx.dataPath, needName(), ctx.options.fromHost ? hostLogin(ctx) : null)) + '.');
40
+ return 0;
41
+ case 'use': return use(ctx, needName());
42
+ case 'default':
43
+ setDefaultAccount(ctx.storageRoot, needName());
44
+ console.log('Default account: ' + name);
45
+ return 0;
46
+ case 'remove':
47
+ removeAccount(ctx.storageRoot, needName());
48
+ console.log('Removed account ' + name);
49
+ return 0;
50
+ default: fail('Unknown account action "' + action + '".', 'Use one of: sandbox account list | save <name> | use <name> | default <name> | remove <name>');
51
+ }
52
+ };
@@ -0,0 +1,21 @@
1
+ import type { CliOptions } from '../args.js';
2
+ import type { LoadedConfig } from '../../core/types.js';
3
+ export interface Context {
4
+ packageRoot: string;
5
+ entry: string;
6
+ options: CliOptions;
7
+ loaded: LoadedConfig;
8
+ storageRoot: string;
9
+ dataPath: string;
10
+ }
11
+ export declare const createContext: (packageRoot: string, entry: string, options: CliOptions) => Context;
12
+ /** Prints rows as aligned columns (the last column is not padded). */
13
+ export declare const table: (rows: string[][]) => void;
14
+ /** Prints `value` as JSON when --json was given; returns whether it did. */
15
+ export declare const printJson: (ctx: Context, value: unknown) => boolean;
16
+ export declare const sandboxAlive: () => boolean;
17
+ export declare const requireRunning: (hint?: string) => void;
18
+ /** Maps a folder on this PC to its path inside the running sandbox (only folders inside the mapped workspace exist there). */
19
+ export declare const guestPathFor: (hostDir: string, workspaceHost: string, guestWorkspace: string) => string;
20
+ /** The guest folder for the current directory (or --workspace) in the running sandbox. */
21
+ export declare const currentGuestDir: (ctx: Context, workspaceHost: string, guestWorkspace: string) => string;
@@ -0,0 +1,54 @@
1
+ // What every command receives, plus the small helpers they share.
2
+ import fs from 'node:fs';
3
+ import path from 'node:path';
4
+ import _ from 'lodash';
5
+ import { loadConfig, userDir } from '../../core/config.js';
6
+ import { expandPath, fail, isInsideOrEqual } from '../../core/util.js';
7
+ import { GUEST_DATA_DIR, insideSandbox, isSandboxRunning } from '../../host/platform.js';
8
+ /** Keeps ~/.agent-sandbox/*.schema.json in step with the installed version (local schema paths editors trust). */
9
+ const refreshSchemas = (packageRoot) => {
10
+ const source = path.join(packageRoot, 'schemas');
11
+ if (!fs.existsSync(source))
12
+ return;
13
+ try {
14
+ fs.mkdirSync(userDir(), { recursive: true });
15
+ for (const name of fs.readdirSync(source)) {
16
+ const target = path.join(userDir(), name);
17
+ const text = fs.readFileSync(path.join(source, name), 'utf8');
18
+ if (!fs.existsSync(target) || fs.readFileSync(target, 'utf8') !== text)
19
+ fs.writeFileSync(target, text, 'utf8');
20
+ }
21
+ }
22
+ catch { /* editor help only; never block a command over it */ }
23
+ };
24
+ export const createContext = (packageRoot, entry, options) => {
25
+ refreshSchemas(packageRoot);
26
+ const loaded = loadConfig(options.config);
27
+ const storageRoot = expandPath(loaded.config.storage.root, loaded.base);
28
+ // Inside a sandbox, the sandbox's own data folder is the one to talk to.
29
+ return { packageRoot, entry, options, loaded, storageRoot, dataPath: insideSandbox() ? GUEST_DATA_DIR : path.join(storageRoot, 'data', loaded.config.storage.dataName) };
30
+ };
31
+ /** Prints rows as aligned columns (the last column is not padded). */
32
+ export const table = (rows) => {
33
+ const widths = _.range(_.max(rows.map((r) => r.length)) ?? 0).map((i) => _.max(rows.map((r) => (r[i] ?? '').length)) ?? 0);
34
+ for (const row of rows)
35
+ console.log(row.map((cell, i) => (i === row.length - 1 ? cell : _.padEnd(cell, widths[i] + 2))).join(''));
36
+ };
37
+ /** Prints `value` as JSON when --json was given; returns whether it did. */
38
+ export const printJson = (ctx, value) => {
39
+ if (ctx.options.json)
40
+ console.log(JSON.stringify(value, null, 2));
41
+ return ctx.options.json;
42
+ };
43
+ export const sandboxAlive = () => isSandboxRunning();
44
+ export const requireRunning = (hint = '') => { if (!isSandboxRunning())
45
+ fail('No Windows Sandbox is running.', hint || 'Start one with sandbox claude.'); };
46
+ /** Maps a folder on this PC to its path inside the running sandbox (only folders inside the mapped workspace exist there). */
47
+ export const guestPathFor = (hostDir, workspaceHost, guestWorkspace) => {
48
+ if (!isInsideOrEqual(workspaceHost, hostDir))
49
+ fail('The running sandbox works on ' + workspaceHost + ', and Windows Sandbox cannot add folders while it runs.', 'Run from inside that folder, or stop the sandbox (sandbox stop) and launch again from ' + hostDir + '.');
50
+ const rel = path.relative(workspaceHost, hostDir);
51
+ return rel ? path.win32.join(guestWorkspace, rel) : guestWorkspace;
52
+ };
53
+ /** The guest folder for the current directory (or --workspace) in the running sandbox. */
54
+ export const currentGuestDir = (ctx, workspaceHost, guestWorkspace) => guestPathFor(path.resolve(ctx.options.workspace ?? process.cwd()), workspaceHost, guestWorkspace);
@@ -0,0 +1,46 @@
1
+ import { type Context } from './context.js';
2
+ export type DataArea = 'tools' | 'databases' | 'state' | 'logs' | 'data' | 'host';
3
+ export interface DataEntry {
4
+ area: DataArea;
5
+ name: string;
6
+ path: string;
7
+ bytes: number;
8
+ }
9
+ /** 1536 -> "1.5 KB". */
10
+ export declare const humanBytes: (bytes: number) => string;
11
+ /** Size on disk, never following links or junctions (a link counts as 0). */
12
+ export declare const sizeOf: (p: string) => number;
13
+ /**
14
+ * Deletes a folder or file without ever following a link: links found anywhere inside are removed themselves first,
15
+ * then the (now link-free) tree is deleted. A link in place of `p` is removed itself.
16
+ */
17
+ /** Deletes p (strictly inside root): links inside are removed first and never followed; see shared/safe-remove.ts. */
18
+ export declare const removeTree: (p: string, root: string) => void;
19
+ /** Every row of `sandbox data`: the sandbox's data folder by area, then what this PC keeps for it. */
20
+ export declare const collectDataEntries: (dataPath: string, storageRoot: string) => DataEntry[];
21
+ export interface DiskUsage {
22
+ drive: string;
23
+ totalBytes: number;
24
+ freeBytes: number;
25
+ usedBytes: number;
26
+ holdsStorage: boolean;
27
+ }
28
+ /** Every local drive's size and free space, and which one holds the sandbox storage. */
29
+ export declare const diskUsage: (storageRoot: string) => DiskUsage[];
30
+ export declare const dataInfo: (ctx: Context) => number;
31
+ export interface CleanTarget {
32
+ /** The target as normalised (forward slashes), e.g. "tools/go". */
33
+ target: string;
34
+ path: string;
35
+ /** Folder the target must stay inside, with no links between the two. */
36
+ root: string;
37
+ warning?: string;
38
+ }
39
+ /**
40
+ * Turns `sandbox data clean` arguments into folders. Pure (no disk access); throws a UserError for anything that is
41
+ * not a plain relative target inside the data folder: absolute paths, drive letters, `..`, wildcards, unknown areas.
42
+ */
43
+ export declare const resolveCleanTargets: (inputs: string[], dataPath: string, storageRoot: string) => CleanTarget[];
44
+ export declare const dataClean: (ctx: Context) => Promise<number>;
45
+ /** `sandbox data [info|clean] [targets...]`. */
46
+ export declare const data: (ctx: Context) => number | Promise<number>;
@@ -0,0 +1,249 @@
1
+ // `sandbox data`: what persists between sandboxes (toolchains, databases, agent state, logs, host storage) and
2
+ // removing chosen parts of it. Everything under the data folder is sandbox-writable: sizes and deletes never follow
3
+ // links, and targets resolve strictly inside the data folder.
4
+ import fs from 'node:fs';
5
+ import path from 'node:path';
6
+ import readline from 'node:readline';
7
+ import _ from 'lodash';
8
+ import { removeRefusal, safeRemove } from '../../shared/safe-remove.js';
9
+ import { assertNoLinks, dirExists, fail, isInsideOrEqual, isLink, removeLink } from '../../core/util.js';
10
+ import { askYesNo } from '../../core/workspace.js';
11
+ import { isSandboxRunning } from '../../host/platform.js';
12
+ import { printJson, table } from './context.js';
13
+ const AREA_ORDER = ['tools', 'databases', 'state', 'logs', 'data', 'host'];
14
+ /** Folders of the data folder that get one row per child; anything else at the top level is listed as itself. */
15
+ const PER_CHILD = ['tools', 'databases', 'state'];
16
+ /** 1536 -> "1.5 KB". */
17
+ export const humanBytes = (bytes) => {
18
+ const units = ['B', 'KB', 'MB', 'GB', 'TB'];
19
+ const i = bytes <= 0 ? 0 : Math.min(units.length - 1, Math.floor(Math.log(bytes) / Math.log(1024)));
20
+ const value = bytes / 1024 ** i;
21
+ return (i === 0 ? String(bytes) : value.toFixed(value < 10 ? 1 : 0)) + ' ' + units[i];
22
+ };
23
+ /** Size on disk, never following links or junctions (a link counts as 0). */
24
+ export const sizeOf = (p) => {
25
+ try {
26
+ const stat = fs.lstatSync(p);
27
+ if (stat.isSymbolicLink())
28
+ return 0;
29
+ if (!stat.isDirectory())
30
+ return stat.size;
31
+ return _.sumBy(fs.readdirSync(p, { withFileTypes: true }), (d) => (d.isSymbolicLink() ? 0 : sizeOf(path.join(p, d.name))));
32
+ }
33
+ catch {
34
+ return 0;
35
+ }
36
+ };
37
+ /**
38
+ * Deletes a folder or file without ever following a link: links found anywhere inside are removed themselves first,
39
+ * then the (now link-free) tree is deleted. A link in place of `p` is removed itself.
40
+ */
41
+ /** Deletes p (strictly inside root): links inside are removed first and never followed; see shared/safe-remove.ts. */
42
+ export const removeTree = (p, root) => {
43
+ const unlinkInside = (dir) => {
44
+ for (const d of fs.readdirSync(dir, { withFileTypes: true })) {
45
+ const child = path.join(dir, d.name);
46
+ if (d.isSymbolicLink() || isLink(child))
47
+ removeLink(child);
48
+ else if (d.isDirectory())
49
+ unlinkInside(child);
50
+ }
51
+ };
52
+ if (isLink(p)) {
53
+ removeLink(p);
54
+ return;
55
+ }
56
+ if (!fs.existsSync(p))
57
+ return;
58
+ // Refused before anything is touched (safeRemove below checks again).
59
+ const refusal = removeRefusal(p, root);
60
+ if (refusal)
61
+ throw new Error('Refusing to delete ' + p + ': ' + refusal);
62
+ if (dirExists(p))
63
+ unlinkInside(p);
64
+ safeRemove(p, root);
65
+ };
66
+ /** Real (non-link) children of a real folder; a link in place of the folder yields nothing. */
67
+ const entriesOf = (dir) => (dirExists(dir) && !isLink(dir) ? fs.readdirSync(dir, { withFileTypes: true }).filter((d) => !d.isSymbolicLink()) : []);
68
+ const toolRows = (toolsDir) => {
69
+ const entries = entriesOf(toolsDir);
70
+ const folders = entries.filter((d) => d.isDirectory());
71
+ const files = entries.filter((d) => !d.isDirectory());
72
+ const row = (name, p) => ({ area: 'tools', name, path: p, bytes: sizeOf(p) });
73
+ return [
74
+ ...folders.flatMap((d) => (d.name.toLowerCase() === 'agents'
75
+ ? entriesOf(path.join(toolsDir, d.name)).map((a) => row('agents/' + a.name, path.join(toolsDir, d.name, a.name)))
76
+ : [row(d.name, path.join(toolsDir, d.name))])),
77
+ ...(files.length ? [{ area: 'tools', name: '(files in tools)', path: toolsDir, bytes: _.sumBy(files, (f) => sizeOf(path.join(toolsDir, f.name))) }] : []),
78
+ ];
79
+ };
80
+ /** Every row of `sandbox data`: the sandbox's data folder by area, then what this PC keeps for it. */
81
+ export const collectDataEntries = (dataPath, storageRoot) => {
82
+ const inData = entriesOf(dataPath);
83
+ const rows = inData.flatMap((d) => {
84
+ const p = path.join(dataPath, d.name);
85
+ const area = d.name.toLowerCase();
86
+ if (area === 'tools')
87
+ return toolRows(p);
88
+ if (PER_CHILD.includes(area))
89
+ return entriesOf(p).map((c) => ({ area, name: area + '/' + c.name, path: path.join(p, c.name), bytes: sizeOf(path.join(p, c.name)) }));
90
+ return [{ area: area === 'logs' ? 'logs' : 'data', name: d.name, path: p, bytes: sizeOf(p) }];
91
+ });
92
+ const otherData = entriesOf(path.join(storageRoot, 'data')).filter((d) => d.isDirectory() && path.join(storageRoot, 'data', d.name).toLowerCase() !== path.resolve(dataPath).toLowerCase());
93
+ const host = [
94
+ ...['launches', 'accounts', 'sync', 'logs', 'run', 'host'].map((name) => ({ area: 'host', name, path: path.join(storageRoot, name), bytes: sizeOf(path.join(storageRoot, name)) })),
95
+ ...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)) })),
96
+ ].filter((r) => fs.existsSync(r.path));
97
+ return _.sortBy([...rows, ...host], (r) => AREA_ORDER.indexOf(r.area), (r) => -r.bytes, (r) => r.name.toLowerCase());
98
+ };
99
+ const AREA_LABEL = { tools: 'Toolchains', databases: 'Databases', state: 'Agent state', logs: 'Logs', data: 'Other', host: 'On this PC' };
100
+ /** Every local drive's size and free space, and which one holds the sandbox storage. */
101
+ export const diskUsage = (storageRoot) => {
102
+ const storageDrive = path.parse(path.resolve(storageRoot)).root.toUpperCase();
103
+ return 'CDEFGHIJKLMNOPQRSTUVWXYZ'.split('').map((letter) => letter + ':\\').flatMap((drive) => {
104
+ try {
105
+ const s = fs.statfsSync(drive);
106
+ const totalBytes = s.blocks * s.bsize;
107
+ if (!totalBytes)
108
+ return [];
109
+ return [{ drive, totalBytes, freeBytes: s.bavail * s.bsize, usedBytes: totalBytes - s.bfree * s.bsize, holdsStorage: drive.toUpperCase() === storageDrive }];
110
+ }
111
+ catch {
112
+ return [];
113
+ }
114
+ });
115
+ };
116
+ export const dataInfo = (ctx) => {
117
+ const entries = collectDataEntries(ctx.dataPath, ctx.storageRoot);
118
+ const total = _.sumBy(entries, 'bytes');
119
+ const disks = diskUsage(ctx.storageRoot);
120
+ if (printJson(ctx, { dataPath: ctx.dataPath, storageRoot: ctx.storageRoot, totalBytes: total, entries, disks }))
121
+ return 0;
122
+ console.log('Disks\n');
123
+ table([['DRIVE', 'USED', 'FREE', 'SIZE', ''], ...disks.map((d) => [d.drive, humanBytes(d.usedBytes) + ' (' + Math.round((d.usedBytes / d.totalBytes) * 100) + '%)', humanBytes(d.freeBytes), humanBytes(d.totalBytes), d.holdsStorage ? '<- sandbox storage (' + humanBytes(total) + ' of it)' : ''])]);
124
+ console.log('');
125
+ console.log('Persistent data for the sandbox: ' + ctx.dataPath + ' (C:\\AgentData inside)\n');
126
+ if (!entries.length) {
127
+ console.log('Nothing stored yet. Toolchains, databases and agent logins appear here after the first start.');
128
+ return 0;
129
+ }
130
+ const rows = _.flatMap(_.groupBy(entries, 'area'), (group, area) => [
131
+ [AREA_LABEL[area], humanBytes(_.sumBy(group, 'bytes')), ''],
132
+ ...group.map((e) => [' ' + e.name, humanBytes(e.bytes), e.path]),
133
+ ]);
134
+ table([...rows, ['', '', ''], ['Total', humanBytes(total), '']]);
135
+ console.log('\nFree space with: sandbox data clean <target...> (e.g. tools/go, databases/postgres-18, logs, launches)');
136
+ return 0;
137
+ };
138
+ const DATA_AREAS = ['tools', 'databases', 'state', 'logs'];
139
+ const SEGMENT = /^[A-Za-z0-9][A-Za-z0-9._ +@-]*$/;
140
+ const LOGIN_WARNING = 'removes the agent logins, settings and history inside the sandbox (save a Claude login first with: sandbox account save <name>)';
141
+ const ALL_WARNING = 'removes EVERYTHING the sandbox keeps: toolchains, databases, agent logins and history, logs and launch folders';
142
+ /**
143
+ * Turns `sandbox data clean` arguments into folders. Pure (no disk access); throws a UserError for anything that is
144
+ * not a plain relative target inside the data folder: absolute paths, drive letters, `..`, wildcards, unknown areas.
145
+ */
146
+ export const resolveCleanTargets = (inputs, dataPath, storageRoot) => {
147
+ const usage = 'Targets: all | tools | tools/<name> | tools/agents/<name> | databases | databases/<name> | state | state/<agent> | logs | launches. See them with: sandbox data';
148
+ if (!inputs.length)
149
+ fail('Say what to remove.', usage);
150
+ const resolved = inputs.map((raw) => {
151
+ const input = raw.trim();
152
+ if (!input)
153
+ fail('Empty target.', usage);
154
+ if (path.win32.isAbsolute(input) || path.posix.isAbsolute(input) || /^[A-Za-z]:/.test(input) || input.includes(':'))
155
+ fail('Refusing "' + raw + '": targets are relative to the data folder, not paths on this PC.', usage);
156
+ const parts = input.replace(/\\/g, '/').split('/').filter(Boolean);
157
+ const bad = parts.find((p) => !SEGMENT.test(p) || /^\.+$/.test(p) || /[. ]$/.test(p));
158
+ if (bad !== undefined)
159
+ fail('Refusing "' + raw + '": "' + bad + '" is not a plain folder name (no .., wildcards or special characters).', usage);
160
+ const [head, ...rest] = parts;
161
+ const area = head.toLowerCase();
162
+ if (parts.length > 3)
163
+ fail('Refusing "' + raw + '": too deep.', usage);
164
+ const target = [area, ...rest].join('/');
165
+ if (area === 'all' && !rest.length)
166
+ return { target: 'all', path: dataPath, root: path.join(storageRoot, 'data'), warning: ALL_WARNING };
167
+ if (area === 'launches')
168
+ return { target, path: path.join(storageRoot, 'launches', ...rest), root: storageRoot };
169
+ if (area === 'accounts' || area === 'sync')
170
+ 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);
171
+ if (!DATA_AREAS.includes(area))
172
+ fail('Unknown target "' + raw + '".', usage);
173
+ if (area === 'logs' && rest.length)
174
+ fail('Remove all logs ("logs"), not single files.', usage);
175
+ if (rest.length > (area === 'tools' && rest[0]?.toLowerCase() === 'agents' ? 2 : 1))
176
+ fail('Refusing "' + raw + '": too deep.', usage);
177
+ return { target, path: path.join(dataPath, area, ...rest), root: dataPath, ...(area === 'state' ? { warning: LOGIN_WARNING } : {}) };
178
+ });
179
+ // "all" covers everything; otherwise drop targets inside another target (tools + tools/go = tools).
180
+ const all = resolved.find((t) => t.target === 'all');
181
+ if (all)
182
+ return [all, { target: 'launches', path: path.join(storageRoot, 'launches'), root: storageRoot }];
183
+ const unique = _.uniqBy(resolved, (t) => t.path.toLowerCase());
184
+ return unique.filter((t) => !unique.some((o) => o !== t && isInsideOrEqual(o.path, t.path)));
185
+ };
186
+ /** Asks the user to type `expected` (a stronger confirmation than y/N). */
187
+ const askTyped = async (question, expected) => {
188
+ if (!process.stdin.isTTY)
189
+ return false;
190
+ const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
191
+ const answer = await new Promise((resolve) => rl.question(question + ' Type "' + expected + '" to confirm: ', resolve));
192
+ rl.close();
193
+ return answer.trim().toLowerCase() === expected;
194
+ };
195
+ export const dataClean = async (ctx) => {
196
+ const targets = resolveCleanTargets(ctx.options.positionals.slice(1), ctx.dataPath, ctx.storageRoot);
197
+ if (isSandboxRunning())
198
+ fail('A Windows Sandbox is running, and it uses these folders.', 'Close it first (sandbox stop), then run this again.');
199
+ // Every folder from the root down to the target's parent must be real; a link in place of the target is removed itself.
200
+ for (const t of targets)
201
+ assertNoLinks(t.root, path.dirname(t.path));
202
+ const present = targets.filter((t) => fs.existsSync(t.path) || isLink(t.path));
203
+ if (!present.length) {
204
+ console.log('Nothing to remove: ' + targets.map((t) => t.target).join(', ') + ' do(es) not exist.');
205
+ return 0;
206
+ }
207
+ const sized = present.map((t) => ({ ...t, bytes: sizeOf(t.path) }));
208
+ const total = _.sumBy(sized, 'bytes');
209
+ table(sized.map((t) => [' ' + t.target, humanBytes(t.bytes), t.path]));
210
+ for (const t of sized)
211
+ if (t.warning)
212
+ console.log('Warning: ' + t.target + ' ' + t.warning + '.');
213
+ const yes = Boolean(ctx.options.yes);
214
+ if (!yes) {
215
+ const ok = sized.some((t) => t.target === 'all')
216
+ ? await askTyped('This cannot be undone; the next sandbox starts from scratch and reinstalls everything.', 'all')
217
+ : await askYesNo('Remove ' + (sized.length === 1 ? 'this' : 'these ' + sized.length) + ' (' + humanBytes(total) + ')?');
218
+ if (!ok) {
219
+ console.log('Nothing removed.' + (process.stdin.isTTY ? '' : ' Pass --yes to confirm without a prompt.'));
220
+ return 1;
221
+ }
222
+ }
223
+ const failed = [];
224
+ for (const t of sized) {
225
+ try {
226
+ removeTree(t.path, t.root);
227
+ }
228
+ catch (e) {
229
+ failed.push(t.target + ' (' + (e instanceof Error ? e.message : String(e)) + ')');
230
+ }
231
+ }
232
+ const freed = total - _.sumBy(sized, (t) => sizeOf(t.path));
233
+ console.log('Freed ' + humanBytes(freed) + '. ' + (sized.some((t) => t.root === ctx.dataPath || t.target === 'all') ? 'The next sandbox reinstalls what it needs.' : ''));
234
+ if (failed.length)
235
+ fail('Could not remove everything: ' + failed.join('; '), 'Close programs using those folders and run the same command again.');
236
+ return 0;
237
+ };
238
+ /** `sandbox data [info|clean] [targets...]`. */
239
+ export const data = (ctx) => {
240
+ const [action = 'info'] = ctx.options.positionals;
241
+ switch (action) {
242
+ case 'info':
243
+ case 'show':
244
+ case 'list': return dataInfo(ctx);
245
+ case 'clean':
246
+ case 'remove': return dataClean(ctx);
247
+ default: return fail('Unknown data action "' + action + '".', 'Use: sandbox data (what is stored) or sandbox data clean <target...>');
248
+ }
249
+ };
@@ -0,0 +1,8 @@
1
+ export { createContext, type Context } from './context.js';
2
+ export { launch } from './launch.js';
3
+ export { setup } from './setup.js';
4
+ export { data } from './data.js';
5
+ export { repair, repairCommand } from './repair.js';
6
+ export { attach, db, exec, hide, restart, sessions, show, status, stop } from './sessions.js';
7
+ export { account } from './accounts.js';
8
+ export { claudeHost, clean, config, doctor, logs, sync, watcher } from './maintenance.js';
@@ -0,0 +1,8 @@
1
+ export { createContext } from './context.js';
2
+ export { launch } from './launch.js';
3
+ export { setup } from './setup.js';
4
+ export { data } from './data.js';
5
+ export { repair, repairCommand } from './repair.js';
6
+ export { attach, db, exec, hide, restart, sessions, show, status, stop } from './sessions.js';
7
+ export { account } from './accounts.js';
8
+ export { claudeHost, clean, config, doctor, logs, sync, watcher } from './maintenance.js';
@@ -0,0 +1,5 @@
1
+ import { attachTerminal } from '../../host/channel.js';
2
+ import { type Context } from './context.js';
3
+ /** Attaches this terminal; with attach.autoHide the sandbox window hides meanwhile (best effort, never fails the attach). */
4
+ export declare const attachHere: (ctx: Context, session: Parameters<typeof attachTerminal>[0]) => Promise<number>;
5
+ export declare const launch: (ctx: Context, generateOnly?: boolean) => Promise<number>;