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,14 @@
1
+ /** The package folder (this file is dist/cli/index.js). */
2
+ export declare const packageRoot: string;
3
+ export interface Preset {
4
+ harness?: string;
5
+ command?: string;
6
+ }
7
+ /**
8
+ * CLI entry. `preset` lets wrapper binaries fix the command/agent (sandbox claude = launch --harness claude). No arguments
9
+ * opens the menu; `sandbox <agent> [agent args]` starts that agent.
10
+ */
11
+ export declare const main: (argv: string[], preset?: Preset) => Promise<number>;
12
+ /** Prints a problem the way a user can act on it: what happened, how to fix it; stack traces only for real bugs. */
13
+ export declare const report: (e: unknown) => number;
14
+ export declare const run: (argv: string[], preset?: Preset) => void;
@@ -0,0 +1,96 @@
1
+ import path from 'node:path';
2
+ import { fileURLToPath } from 'node:url';
3
+ import { CommanderError } from 'commander';
4
+ import { buildProgram, parseCli } from './args.js';
5
+ import { commandSpec } from './manifest.js';
6
+ import { menu, recordHistory } from './menu.js';
7
+ import { loadConfig } from '../core/config.js';
8
+ import * as commands from './commands/index.js';
9
+ import { UserError, errMsg, fail } from '../core/util.js';
10
+ /** The package folder (this file is dist/cli/index.js). */
11
+ export const packageRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..', '..');
12
+ const entry = path.join(packageRoot, 'dist', 'bin', 'agent-sandbox.js');
13
+ const HANDLERS = {
14
+ launch: (ctx) => commands.launch(ctx),
15
+ generate: (ctx) => commands.launch(ctx, true),
16
+ attach: commands.attach,
17
+ sessions: commands.sessions,
18
+ status: commands.status,
19
+ exec: commands.exec,
20
+ db: commands.db,
21
+ restart: commands.restart,
22
+ stop: commands.stop,
23
+ show: commands.show,
24
+ hide: commands.hide,
25
+ account: commands.account,
26
+ sync: commands.sync,
27
+ logs: commands.logs,
28
+ clean: commands.clean,
29
+ config: commands.config,
30
+ doctor: commands.doctor,
31
+ setup: commands.setup,
32
+ data: commands.data,
33
+ 'host-claude': commands.claudeHost,
34
+ _watch: commands.watcher,
35
+ };
36
+ const red = (s) => '\x1b[31m' + s + '\x1b[0m';
37
+ const dim = (s) => '\x1b[2m' + s + '\x1b[0m';
38
+ /** Runs an argv from the menu exactly as if it had been typed, reporting errors instead of throwing. */
39
+ const runFromMenu = (argv) => main(argv).catch((e) => report(e));
40
+ /**
41
+ * CLI entry. `preset` lets wrapper binaries fix the command/agent (sandbox claude = launch --harness claude). No arguments
42
+ * opens the menu; `sandbox <agent> [agent args]` starts that agent.
43
+ */
44
+ export const main = async (argv, preset = {}) => {
45
+ const isAgent = (name) => {
46
+ if (!name || name.startsWith('-') || commandSpec(name))
47
+ return false;
48
+ const at = argv.indexOf('--config');
49
+ try {
50
+ return Boolean(loadConfig(at >= 0 ? argv[at + 1] : undefined).config.harnesses[name]);
51
+ }
52
+ catch {
53
+ return false;
54
+ }
55
+ };
56
+ if (!preset.command && !preset.harness) {
57
+ if (argv.length === 0)
58
+ return process.stdin.isTTY ? main(['menu']) : (buildProgram(() => undefined).outputHelp(), 0);
59
+ if (isAgent(argv[0]))
60
+ return main(argv.slice(1), { harness: argv[0] });
61
+ }
62
+ const options = parseCli(preset.command ? [preset.command, ...argv] : argv, Boolean(preset.harness));
63
+ if (preset.harness)
64
+ options.harness = preset.harness;
65
+ recordHistory(preset.harness ? ['launch', '--harness', preset.harness, ...argv] : preset.command ? [preset.command, ...argv] : argv);
66
+ if (options.command === 'menu')
67
+ return menu(commands.createContext(packageRoot, entry, options), runFromMenu);
68
+ // Loaded only when asked for: normal commands never load test code.
69
+ if (options.command === 'self-test')
70
+ return (await import('../selftest/index.js')).selfTest(packageRoot);
71
+ // repair must run even when the config file is broken (createContext would refuse it).
72
+ if (options.command === 'repair')
73
+ return commands.repairCommand(packageRoot, entry, options);
74
+ const handler = HANDLERS[options.command];
75
+ if (!handler)
76
+ return fail('Unknown command "' + options.command + '".', 'Run sandbox --help for the list of commands, or sandbox for the menu.');
77
+ return (await handler(commands.createContext(packageRoot, entry, options))) ?? 0;
78
+ };
79
+ /** Prints a problem the way a user can act on it: what happened, how to fix it; stack traces only for real bugs. */
80
+ export const report = (e) => {
81
+ if (e instanceof CommanderError)
82
+ return ['commander.helpDisplayed', 'commander.help', 'commander.version'].includes(e.code) ? 0 : e.exitCode || 1;
83
+ if (e instanceof UserError) {
84
+ console.error(red('Error: ') + e.message);
85
+ if (e.fix)
86
+ console.error(' Fix: ' + e.fix);
87
+ return 1;
88
+ }
89
+ console.error(red('Unexpected error: ') + errMsg(e));
90
+ console.error(dim('This is a bug in windows-development-sandbox. Details: ' + (e instanceof Error && e.stack ? e.stack.split('\n').slice(1, 4).join(' | ').trim() : 'none')));
91
+ console.error(' Please report it with the output of: sandbox doctor');
92
+ return 1;
93
+ };
94
+ export const run = (argv, preset) => {
95
+ main(argv, preset).then((code) => { process.exitCode = code; }, (e) => { process.exitCode = report(e); });
96
+ };
@@ -0,0 +1,95 @@
1
+ import { z } from 'zod';
2
+ export declare const OptionSpec: z.ZodObject<{
3
+ flag: z.ZodString;
4
+ short: z.ZodOptional<z.ZodString>;
5
+ type: z.ZodEnum<{
6
+ string: "string";
7
+ number: "number";
8
+ boolean: "boolean";
9
+ list: "list";
10
+ }>;
11
+ value: z.ZodOptional<z.ZodString>;
12
+ description: z.ZodString;
13
+ default: z.ZodOptional<z.ZodUnion<readonly [z.ZodString, z.ZodNumber]>>;
14
+ choices: z.ZodOptional<z.ZodUnion<readonly [z.ZodArray<z.ZodString>, z.ZodEnum<{
15
+ harnesses: "harnesses";
16
+ sessions: "sessions";
17
+ accounts: "accounts";
18
+ }>]>>;
19
+ advanced: z.ZodOptional<z.ZodBoolean>;
20
+ }, z.core.$strict>;
21
+ export declare const ArgSpec: z.ZodObject<{
22
+ name: z.ZodString;
23
+ description: z.ZodString;
24
+ required: z.ZodBoolean;
25
+ variadic: z.ZodOptional<z.ZodBoolean>;
26
+ choices: z.ZodOptional<z.ZodUnion<readonly [z.ZodArray<z.ZodString>, z.ZodEnum<{
27
+ harnesses: "harnesses";
28
+ sessions: "sessions";
29
+ accounts: "accounts";
30
+ }>]>>;
31
+ }, z.core.$strict>;
32
+ export declare const Group: z.ZodEnum<{
33
+ Agents: "Agents";
34
+ Sandbox: "Sandbox";
35
+ Sessions: "Sessions";
36
+ Accounts: "Accounts";
37
+ Data: "Data";
38
+ Health: "Health";
39
+ Config: "Config";
40
+ }>;
41
+ export declare const CommandSpec: z.ZodObject<{
42
+ name: z.ZodString;
43
+ summary: z.ZodString;
44
+ group: z.ZodEnum<{
45
+ Agents: "Agents";
46
+ Sandbox: "Sandbox";
47
+ Sessions: "Sessions";
48
+ Accounts: "Accounts";
49
+ Data: "Data";
50
+ Health: "Health";
51
+ Config: "Config";
52
+ }>;
53
+ args: z.ZodArray<z.ZodObject<{
54
+ name: z.ZodString;
55
+ description: z.ZodString;
56
+ required: z.ZodBoolean;
57
+ variadic: z.ZodOptional<z.ZodBoolean>;
58
+ choices: z.ZodOptional<z.ZodUnion<readonly [z.ZodArray<z.ZodString>, z.ZodEnum<{
59
+ harnesses: "harnesses";
60
+ sessions: "sessions";
61
+ accounts: "accounts";
62
+ }>]>>;
63
+ }, z.core.$strict>>;
64
+ options: z.ZodArray<z.ZodObject<{
65
+ flag: z.ZodString;
66
+ short: z.ZodOptional<z.ZodString>;
67
+ type: z.ZodEnum<{
68
+ string: "string";
69
+ number: "number";
70
+ boolean: "boolean";
71
+ list: "list";
72
+ }>;
73
+ value: z.ZodOptional<z.ZodString>;
74
+ description: z.ZodString;
75
+ default: z.ZodOptional<z.ZodUnion<readonly [z.ZodString, z.ZodNumber]>>;
76
+ choices: z.ZodOptional<z.ZodUnion<readonly [z.ZodArray<z.ZodString>, z.ZodEnum<{
77
+ harnesses: "harnesses";
78
+ sessions: "sessions";
79
+ accounts: "accounts";
80
+ }>]>>;
81
+ advanced: z.ZodOptional<z.ZodBoolean>;
82
+ }, z.core.$strict>>;
83
+ passthrough: z.ZodOptional<z.ZodEnum<{
84
+ command: "command";
85
+ agent: "agent";
86
+ }>>;
87
+ json: z.ZodOptional<z.ZodBoolean>;
88
+ interactive: z.ZodOptional<z.ZodBoolean>;
89
+ hidden: z.ZodOptional<z.ZodBoolean>;
90
+ }, z.core.$strict>;
91
+ export type OptionSpecT = z.output<typeof OptionSpec>;
92
+ export type ArgSpecT = z.output<typeof ArgSpec>;
93
+ export type CommandSpecT = z.output<typeof CommandSpec>;
94
+ export declare const COMMANDS: CommandSpecT[];
95
+ export declare const commandSpec: (name: string) => CommandSpecT | undefined;
@@ -0,0 +1,87 @@
1
+ // Every command, defined once: name, group, arguments and options with their types. args.ts builds the commander
2
+ // parser from it and menu.ts builds the interactive menu from it, so a command added here works both ways.
3
+ import { z } from 'zod';
4
+ const Choices = z.union([z.array(z.string()), z.enum(['harnesses', 'accounts', 'sessions'])])
5
+ .describe('Fixed values, or a list the menu fills in at run time.');
6
+ export const OptionSpec = z.strictObject({
7
+ flag: z.string().regex(/^--[a-z][a-z0-9-]*$/).describe('Long flag, e.g. --terminal.'),
8
+ short: z.string().regex(/^-[a-zA-Z]$/).optional(),
9
+ type: z.enum(['boolean', 'string', 'number', 'list']).describe('list = repeatable string option.'),
10
+ value: z.string().optional().describe('Placeholder shown in help, e.g. name for --account <name>.'),
11
+ description: z.string(),
12
+ default: z.union([z.string(), z.number()]).optional(),
13
+ choices: Choices.optional(),
14
+ /** Advanced options are only asked in the menu after "Show advanced options". */
15
+ advanced: z.boolean().optional(),
16
+ });
17
+ export const ArgSpec = z.strictObject({
18
+ name: z.string(),
19
+ description: z.string(),
20
+ required: z.boolean(),
21
+ variadic: z.boolean().optional(),
22
+ choices: Choices.optional(),
23
+ });
24
+ export const Group = z.enum(['Agents', 'Sandbox', 'Sessions', 'Accounts', 'Data', 'Health', 'Config']);
25
+ export const CommandSpec = z.strictObject({
26
+ name: z.string(),
27
+ summary: z.string(),
28
+ group: Group,
29
+ args: z.array(ArgSpec),
30
+ options: z.array(OptionSpec),
31
+ /** Unknown options and extra arguments are passed on to the agent (launch) or the program (exec). */
32
+ passthrough: z.enum(['agent', 'command']).optional(),
33
+ json: z.boolean().optional(),
34
+ /** Takes over the terminal or ends the session: the menu does not come back afterwards. */
35
+ interactive: z.boolean().optional(),
36
+ hidden: z.boolean().optional(),
37
+ });
38
+ const bool = (flag, description, extra = {}) => ({ flag, type: 'boolean', description, ...extra });
39
+ const text = (flag, value, description, extra = {}) => ({ flag, type: 'string', value, description, ...extra });
40
+ const SANDBOX_OPTIONS = [
41
+ bool('--terminal', 'run in THIS terminal (reattaches to this folder\'s session if nobody is watching it)', { short: '-t' }),
42
+ bool('--headless', 'hide the sandbox window (sbx-show brings it back)'),
43
+ bool('--new', 'always start a new session, even if this folder has one nobody is attached to'),
44
+ text('--workspace', 'dir', 'use this folder instead of the current one'),
45
+ text('--node', 'version', 'Node version for nvm (default: .sandbox-config.json > .nvmrc > this PC)', { advanced: true }),
46
+ text('--python', 'version', 'Python 3 version (default: .sandbox-config.json > .python-version > config)', { advanced: true }),
47
+ { flag: '--database', type: 'list', value: 'name', description: 'also start this database (postgres, mysql, mongodb, redis, dynamodb; repeatable)', advanced: true },
48
+ { flag: '--script', type: 'list', value: 'file', description: 'run this .ps1/.mjs/.cmd inside the sandbox first (repeatable)', advanced: true },
49
+ bool('--window', 'show the sandbox window even if config says headless', { advanced: true }),
50
+ bool('--trust-config', 'approve the folder\'s .sandbox-config.json without asking', { advanced: true }),
51
+ bool('--no-local-config', 'ignore the folder\'s .sandbox-config.json', { advanced: true }),
52
+ ];
53
+ const AGENT_OPTIONS = [
54
+ text('--harness', 'name', 'agent to run', { choices: 'harnesses' }),
55
+ text('--account', 'name', 'Claude account for this sandbox', { choices: 'accounts' }),
56
+ ...SANDBOX_OPTIONS,
57
+ bool('--no-agent', 'provision the sandbox without starting an agent', { advanced: true }),
58
+ ];
59
+ const json = true;
60
+ export const COMMANDS = z.array(CommandSpec).parse([
61
+ { name: 'launch', summary: 'start the sandbox and an agent (or another session in the running sandbox)', group: 'Agents', args: [], options: AGENT_OPTIONS, passthrough: 'agent', interactive: true },
62
+ { name: 'start', summary: 'start the sandbox WITHOUT an agent: just the machine, toolchains and databases', group: 'Sandbox', args: [], options: SANDBOX_OPTIONS.filter((o) => o.flag !== '--new'), interactive: true },
63
+ { name: 'generate', summary: 'dry run: write the .wsb and launch folder and show what would happen, start nothing', group: 'Health', args: [], options: AGENT_OPTIONS, passthrough: 'agent' },
64
+ { name: 'attach', summary: 'show a running session in this terminal (Ctrl+] detaches)', group: 'Sessions', args: [{ name: 'id', description: 'session (default: the newest)', required: false, choices: 'sessions' }], options: [], interactive: true },
65
+ { name: 'sessions', summary: 'list agent sessions and who is attached', group: 'Sessions', args: [], options: [], json },
66
+ { name: 'status', summary: 'sandbox overview', group: 'Sandbox', args: [], options: [], json },
67
+ { name: 'db', summary: 'list, start or stop databases in the running sandbox', group: 'Sandbox', args: [{ name: 'action', description: 'list, start or stop', required: false, choices: ['list', 'start', 'stop'] }, { name: 'name', description: 'postgres, mysql, mongodb, redis or dynamodb', required: false, choices: ['postgres', 'mysql', 'mongodb', 'redis', 'dynamodb'] }], options: [] },
68
+ { name: 'exec', summary: 'run a command inside the sandbox and print its output', group: 'Sandbox', args: [{ name: 'command', description: 'command and its arguments', required: true, variadic: true }], options: [{ flag: '--timeout', type: 'number', value: 'seconds', description: 'give up after this many seconds', default: 600 }, text('--workspace', 'dir', 'folder (inside the mapped workspace) to run in')], passthrough: 'command', json },
69
+ { name: 'restart', summary: 'restart running agents in place; they resume their conversation', group: 'Sessions', args: [], options: [text('--harness', 'name', 'only this agent', { choices: 'harnesses' })] },
70
+ { name: 'stop', summary: 'shut the sandbox down', group: 'Sandbox', args: [], options: [] },
71
+ { name: 'show', summary: 'show the sandbox window', group: 'Sandbox', args: [], options: [] },
72
+ { name: 'hide', summary: 'hide the sandbox window (it keeps running)', group: 'Sandbox', args: [], options: [] },
73
+ { name: 'account', summary: 'Claude accounts: list, save, use, set the default, remove', group: 'Accounts', args: [{ name: 'action', description: 'what to do', required: false, choices: ['list', 'save', 'use', 'default', 'remove'] }, { name: 'name', description: 'account name', required: false, choices: 'accounts' }], options: [bool('--from-host', '(save) save this PC\'s login instead of the sandbox\'s'), bool('--no-restart', '(use) do not restart running sessions')], json },
74
+ { name: 'data', summary: 'what persists between sandboxes (tools, databases, logins, logs), and cleaning it', group: 'Data', args: [{ name: 'action', description: 'show or clean', required: false, choices: ['show', 'clean'] }, { name: 'targets', description: 'what to clean, e.g. tools/go databases logs', required: false, variadic: true }], options: [bool('--yes', 'do not ask before deleting')], json },
75
+ { name: 'clean', summary: 'delete old launch folders, logs and leftovers', group: 'Data', args: [], options: [{ flag: '--days', type: 'number', value: 'n', description: 'launch folders older than this many days', default: 7 }] },
76
+ { name: 'sync', summary: 'copy history (and claude-mem, once closed) back to this PC now', group: 'Data', args: [], options: [] },
77
+ { name: 'logs', summary: 'print the newest sandbox log', group: 'Health', args: [], options: [bool('--follow', 'keep printing new lines', { short: '-f' })] },
78
+ { name: 'setup', summary: 'check this PC can run Windows Sandbox and offer to turn it on', group: 'Health', args: [], options: [] },
79
+ { name: 'doctor', summary: 'check this PC and show what will be copied or downloaded', group: 'Health', args: [], options: [text('--harness', 'name', 'agent to check', { choices: 'harnesses' })] },
80
+ { name: 'repair', summary: 'find and fix common problems (stale state, broken downloads, half-installed tools)', group: 'Health', args: [], options: [] },
81
+ { name: 'self-test', summary: 'run the built-in tests', group: 'Health', args: [], options: [] },
82
+ { name: 'config', summary: 'show the effective config, its path, create it, editor schemas, or check a project\'s .sandbox-config.json', group: 'Config', args: [{ name: 'action', description: 'what to do', required: false, choices: ['show', 'path', 'init', 'schema', 'check'] }], options: [text('--workspace', 'dir', '(check) the project folder; default: the current one')] },
83
+ { name: 'host-claude', summary: 'Claude on this PC (NOT sandboxed) with a separate profile', group: 'Agents', args: [], options: [bool('--personal', 'use your normal Claude profile')], passthrough: 'agent', interactive: true },
84
+ { name: 'menu', summary: 'the interactive menu (what `sandbox` opens with no arguments)', group: 'Config', args: [], options: [], hidden: true, interactive: true },
85
+ { name: '_watch', summary: 'background history sync', group: 'Data', args: [], options: [], hidden: true },
86
+ ]);
87
+ export const commandSpec = (name) => COMMANDS.find((c) => c.name === name);
@@ -0,0 +1,22 @@
1
+ import { z } from 'zod';
2
+ import type { Context } from './commands/context.js';
3
+ import { type CommandSpecT } from './manifest.js';
4
+ declare const History: z.ZodArray<z.ZodObject<{
5
+ argv: z.ZodArray<z.ZodString>;
6
+ at: z.ZodString;
7
+ }, z.core.$strict>>;
8
+ export declare const readHistory: () => z.output<typeof History>;
9
+ /** Remembers a command (menu or typed) so the menu can offer it again. Never fails the command itself. */
10
+ export declare const recordHistory: (argv: string[]) => void;
11
+ /** argv as you would type it (quoted where needed). */
12
+ export declare const commandLine: (argv: string[]) => string;
13
+ /** Splits "a b 'c d'" like a shell would (enough for agent arguments and exec commands). */
14
+ export declare const splitArgs: (text: string) => string[];
15
+ /** Asks everything a command takes and returns its argv (or null when the user backs out). */
16
+ export declare const askCommand: (ctx: Context, spec: CommandSpecT, preset?: string[]) => Promise<string[] | null>;
17
+ /**
18
+ * Runs the menu until Exit, or until a command that takes over the terminal (launch, attach) has run. `run` executes
19
+ * an argv exactly like typing it.
20
+ */
21
+ export declare const menu: (ctx: Context, run: (argv: string[]) => Promise<number>) => Promise<number>;
22
+ export {};
@@ -0,0 +1,187 @@
1
+ // The interactive menu (`sandbox` with no arguments). Built from manifest.ts: pick a command, answer one question per
2
+ // argument and option (on/off options as checkboxes), see the exact command line, run it. The command line it shows is
3
+ // what you would type yourself, and the last one is offered again at the top next time.
4
+ import fs from 'node:fs';
5
+ import path from 'node:path';
6
+ import { checkbox, confirm, input, select, Separator } from '@inquirer/prompts';
7
+ import { z } from 'zod';
8
+ import { userDir } from '../core/config.js';
9
+ import { tryReadJson, writeJson } from '../core/util.js';
10
+ import { listAccounts } from '../host/accounts.js';
11
+ import { runningSessions } from '../host/channel.js';
12
+ import { isSandboxRunning } from '../host/platform.js';
13
+ import { settingsMenu } from './settings.js';
14
+ import { COMMANDS, Group } from './manifest.js';
15
+ const c = { bold: (s) => '\x1b[1m' + s + '\x1b[0m', dim: (s) => '\x1b[2m' + s + '\x1b[0m', cyan: (s) => '\x1b[36m' + s + '\x1b[0m', green: (s) => '\x1b[32m' + s + '\x1b[0m', yellow: (s) => '\x1b[33m' + s + '\x1b[0m' };
16
+ // ---- history ---------------------------------------------------------------------------------------------------------
17
+ const History = z.array(z.strictObject({ argv: z.array(z.string()).max(200), at: z.string() })).max(20);
18
+ const historyFile = () => path.join(userDir(), 'history.json');
19
+ const HIDDEN = new Set(['menu', '_watch', 'self-test']);
20
+ export const readHistory = () => History.safeParse(tryReadJson(historyFile())).data ?? [];
21
+ /** Remembers a command (menu or typed) so the menu can offer it again. Never fails the command itself. */
22
+ export const recordHistory = (argv) => {
23
+ if (!argv.length || HIDDEN.has(argv[0]) || argv.includes('--help'))
24
+ return;
25
+ try {
26
+ writeJson(historyFile(), [{ argv, at: new Date().toISOString() }, ...readHistory().filter((h) => h.argv.join('\0') !== argv.join('\0'))].slice(0, 10));
27
+ }
28
+ catch { /* history is a convenience */ }
29
+ };
30
+ /** argv as you would type it (quoted where needed). */
31
+ export const commandLine = (argv) => ['sandbox', ...argv].map((a) => (/[\s"]/.test(a) || a === '' ? '"' + a.replace(/"/g, '\\"') + '"' : a)).join(' ');
32
+ // ---- asking for one command ------------------------------------------------------------------------------------------
33
+ /** Values for a choices list the manifest names (filled in from config and the running sandbox). */
34
+ const choicesFor = (ctx, choices) => {
35
+ if (Array.isArray(choices))
36
+ return choices;
37
+ if (choices === 'harnesses')
38
+ return Object.keys(ctx.loaded.config.harnesses);
39
+ if (choices === 'accounts')
40
+ return listAccounts(ctx.storageRoot, ctx.dataPath).map((a) => a.name);
41
+ if (choices === 'sessions')
42
+ return isSandboxRunning() ? runningSessions(ctx.dataPath).map((s) => s.id) : [];
43
+ return [];
44
+ };
45
+ /** Splits "a b 'c d'" like a shell would (enough for agent arguments and exec commands). */
46
+ export const splitArgs = (text) => [...text.matchAll(/"([^"]*)"|'([^']*)'|(\S+)/g)].map((m) => m[1] ?? m[2] ?? m[3]);
47
+ const askArg = async (ctx, arg) => {
48
+ const list = choicesFor(ctx, arg.choices);
49
+ if (list.length && !arg.variadic) {
50
+ const picked = await select({ message: arg.description, choices: [...list.map((v) => ({ name: v, value: v })), ...(arg.required ? [] : [{ name: c.dim('(skip)'), value: '' }])] });
51
+ return picked ? [picked] : [];
52
+ }
53
+ const answer = await input({ message: arg.description + (arg.required ? '' : c.dim(' (optional)')), validate: (v) => (!arg.required || v.trim() !== '' ? true : 'required') });
54
+ return arg.variadic ? splitArgs(answer) : answer.trim() ? [answer.trim()] : [];
55
+ };
56
+ const askOptions = async (ctx, options) => {
57
+ const argv = [];
58
+ const flags = options.filter((o) => o.type === 'boolean');
59
+ if (flags.length)
60
+ argv.push(...await checkbox({ message: 'Options ' + c.dim('(space to toggle, enter to continue)'), choices: flags.map((o) => ({ name: o.flag + c.dim(' ' + o.description), value: o.flag })) }));
61
+ for (const o of options.filter((x) => x.type !== 'boolean')) {
62
+ const list = choicesFor(ctx, o.choices);
63
+ const label = o.flag + c.dim(' ' + o.description);
64
+ const value = list.length
65
+ ? await select({ message: label, choices: [{ name: c.dim('(default)'), value: '' }, ...list.map((v) => ({ name: v, value: v }))] })
66
+ : await input({ message: label + c.dim(o.default !== undefined ? ' (default ' + o.default + ')' : ' (empty = default)'), validate: (v) => (o.type !== 'number' || v === '' || Number(v) > 0 ? true : 'a positive number') });
67
+ if (!value)
68
+ continue;
69
+ for (const v of o.type === 'list' ? splitArgs(value) : [value])
70
+ argv.push(o.flag, v);
71
+ }
72
+ return argv;
73
+ };
74
+ /** Asks everything a command takes and returns its argv (or null when the user backs out). */
75
+ export const askCommand = async (ctx, spec, preset = []) => {
76
+ console.log('\n' + c.bold(spec.name) + c.dim(' ' + spec.summary));
77
+ const argv = [spec.name, ...preset];
78
+ for (const arg of spec.args)
79
+ argv.push(...await askArg(ctx, arg));
80
+ const basic = spec.options.filter((o) => !o.advanced && !preset.includes(o.flag));
81
+ const advanced = spec.options.filter((o) => o.advanced);
82
+ argv.push(...await askOptions(ctx, basic));
83
+ if (advanced.length && await confirm({ message: 'Change advanced options (' + advanced.map((o) => o.flag).join(', ') + ')?', default: false }))
84
+ argv.push(...await askOptions(ctx, advanced));
85
+ if (spec.json && await confirm({ message: 'Machine-readable output (--json)?', default: false }))
86
+ argv.push('--json');
87
+ if (spec.passthrough) {
88
+ const extra = splitArgs(await input({ message: spec.passthrough === 'agent' ? 'Extra arguments for the agent ' + c.dim('(optional, e.g. --continue)') : 'Arguments for the command' + c.dim(' (optional)') }));
89
+ if (extra.length)
90
+ argv.push(...(spec.passthrough === 'agent' ? ['--', ...extra] : extra));
91
+ }
92
+ console.log(c.dim(' ' + commandLine(argv)));
93
+ return await confirm({ message: 'Run it?', default: true }) ? argv : null;
94
+ };
95
+ // ---- the menu --------------------------------------------------------------------------------------------------------
96
+ /** Is this agent already in the sandbox's persistent storage (so starting it needs no install)? */
97
+ const agentInstalled = (ctx, name) => {
98
+ const h = ctx.loaded.config.harnesses[name];
99
+ const dir = path.join(ctx.dataPath, 'tools', 'agents', name);
100
+ return Boolean(h) && [h.hostExecutable, h.npmBin && h.npmBin + '.cmd', h.uvBin && h.uvBin + '.exe'].some((f) => f && fs.existsSync(path.join(dir, f)));
101
+ };
102
+ const GROUP_TITLES = { Agents: 'Start an agent', Sandbox: 'Sandbox', Sessions: 'Sessions (attach, restart)', Accounts: 'Claude accounts', Data: 'Persisted data, sync and cleanup', Health: 'Diagnostics and repair (doctor, setup, repair, dry run, logs)', Config: 'Configuration' };
103
+ const pickAgent = async (ctx) => {
104
+ const names = Object.keys(ctx.loaded.config.harnesses);
105
+ const name = await select({
106
+ message: 'Which agent?',
107
+ pageSize: 16,
108
+ choices: [
109
+ ...names.map((n) => ({ name: n.padEnd(10) + (agentInstalled(ctx, n) ? c.green(' installed') : c.dim(' installs on first start')) + (n === ctx.loaded.config.defaultHarness ? c.dim(' (default)') : ''), value: n })),
110
+ new Separator(),
111
+ { name: 'Claude on this PC, NOT sandboxed (host-claude)', value: '@host' },
112
+ { name: c.dim('< back'), value: '' },
113
+ ],
114
+ });
115
+ if (name === '@host')
116
+ return { kind: 'ask', spec: COMMANDS.find((s) => s.name === 'host-claude') };
117
+ return name ? { kind: 'ask', spec: COMMANDS.find((s) => s.name === 'launch'), preset: ['--harness', name] } : { kind: 'group', group: '' };
118
+ };
119
+ const mainMenu = async (ctx) => {
120
+ const last = readHistory()[0];
121
+ const running = isSandboxRunning();
122
+ console.log('\n' + c.bold(c.cyan('Windows Development Sandbox')) + c.dim(' ' + process.cwd()) + '\n' + (running ? c.green(' A sandbox is running') + c.dim(' (' + runningSessions(ctx.dataPath).length + ' session(s))') : c.dim(' No sandbox running')));
123
+ return select({
124
+ message: 'What would you like to do?',
125
+ pageSize: 14,
126
+ choices: [
127
+ ...(last ? [{ name: 'Repeat last: ' + c.cyan(commandLine(last.argv)), value: { kind: 'run', argv: last.argv } }] : []),
128
+ { name: 'Start ' + ctx.loaded.config.defaultHarness + ' in this folder', value: { kind: 'ask', spec: COMMANDS.find((s) => s.name === 'launch'), preset: ['--harness', ctx.loaded.config.defaultHarness] } },
129
+ { name: 'Start another agent...', value: { kind: 'group', group: '@agents' } },
130
+ { name: 'Start a sandbox without an agent', value: { kind: 'ask', spec: COMMANDS.find((s) => s.name === 'start') } },
131
+ new Separator(),
132
+ ...Group.options.filter((g) => g !== 'Agents').map((g) => ({ name: GROUP_TITLES[g] + '...', value: { kind: 'group', group: g } })),
133
+ { name: 'Settings ' + c.dim('(every setting, saved to your config file)'), value: { kind: 'group', group: '@settings' } },
134
+ new Separator(),
135
+ { name: 'Exit', value: { kind: 'exit' } },
136
+ ],
137
+ });
138
+ };
139
+ const groupMenu = async (group) => select({
140
+ message: GROUP_TITLES[group] ?? group,
141
+ pageSize: 16,
142
+ choices: [
143
+ ...COMMANDS.filter((s) => s.group === group && !s.hidden).map((s) => ({ name: s.name.padEnd(10) + c.dim(' ' + s.summary), value: { kind: 'ask', spec: s } })),
144
+ { name: c.dim('< back'), value: { kind: 'group', group: '' } },
145
+ ],
146
+ });
147
+ /**
148
+ * Runs the menu until Exit, or until a command that takes over the terminal (launch, attach) has run. `run` executes
149
+ * an argv exactly like typing it.
150
+ */
151
+ export const menu = async (ctx, run) => {
152
+ if (!process.stdin.isTTY) {
153
+ console.log('The menu needs an interactive terminal. Run sandbox --help for the commands.');
154
+ return 1;
155
+ }
156
+ try {
157
+ for (let pick = await mainMenu(ctx);;) {
158
+ if (pick.kind === 'exit')
159
+ return 0;
160
+ if (pick.kind === 'group' && pick.group === '@settings') {
161
+ await settingsMenu(ctx.loaded.configPath);
162
+ pick = await mainMenu(ctx);
163
+ }
164
+ else if (pick.kind === 'group')
165
+ pick = pick.group === '' ? await mainMenu(ctx) : pick.group === '@agents' ? await pickAgent(ctx) : await groupMenu(pick.group);
166
+ else {
167
+ const argv = pick.kind === 'run' ? pick.argv : await askCommand(ctx, pick.spec, pick.preset);
168
+ if (!argv) {
169
+ pick = await mainMenu(ctx);
170
+ continue;
171
+ }
172
+ const code = await run(argv);
173
+ if (COMMANDS.find((s) => s.name === argv[0])?.interactive)
174
+ return code;
175
+ await input({ message: c.dim((code === 0 ? 'Done' : yellowCode(code)) + '. Press Enter for the menu') });
176
+ pick = await mainMenu(ctx);
177
+ }
178
+ }
179
+ }
180
+ catch (e) {
181
+ // Ctrl+C in a prompt: leave quietly.
182
+ if (e instanceof Error && e.name === 'ExitPromptError')
183
+ return 130;
184
+ throw e;
185
+ }
186
+ };
187
+ const yellowCode = (code) => c.yellow('Exit code ' + code);
@@ -0,0 +1,11 @@
1
+ /**
2
+ * Applies one change (undefined = back to the default) to the config text and returns the new text, or the problems
3
+ * that make the result invalid. Pure: the caller writes the file.
4
+ */
5
+ export declare const editConfigText: (text: string, key: string[], value: unknown) => {
6
+ text: string;
7
+ } | {
8
+ problems: string[];
9
+ };
10
+ /** The settings screen. Changes are saved to the user config file (created when missing). */
11
+ export declare const settingsMenu: (configPath: string | null) => Promise<void>;