windows-development-sandbox 1.0.5 → 1.1.0

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 (368) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +22 -6
  3. package/config.example.json +17 -0
  4. package/dist/api.js +2 -1
  5. package/dist/cli/args.d.ts +20 -0
  6. package/dist/cli/args.js +35 -3
  7. package/dist/cli/colors.d.ts +24 -0
  8. package/dist/cli/colors.js +42 -0
  9. package/dist/cli/commands/agent-commands.d.ts +6 -0
  10. package/dist/cli/commands/agent-commands.js +61 -0
  11. package/dist/cli/commands/context.d.ts +2 -0
  12. package/dist/cli/commands/context.js +3 -0
  13. package/dist/cli/commands/data/clean.d.ts +15 -0
  14. package/dist/cli/commands/data/clean.js +101 -0
  15. package/dist/cli/commands/data/collect.d.ts +11 -0
  16. package/dist/cli/commands/data/collect.js +54 -0
  17. package/dist/cli/commands/data/index.d.ts +4 -0
  18. package/dist/cli/commands/data/index.js +45 -0
  19. package/dist/cli/commands/data/remove.d.ts +2 -0
  20. package/dist/cli/commands/data/remove.js +30 -0
  21. package/dist/cli/commands/index.d.ts +5 -3
  22. package/dist/cli/commands/index.js +5 -3
  23. package/dist/cli/commands/launch.d.ts +26 -2
  24. package/dist/cli/commands/launch.js +220 -39
  25. package/dist/cli/commands/maintenance.js +33 -4
  26. package/dist/cli/commands/project.d.ts +29 -0
  27. package/dist/cli/commands/project.js +63 -0
  28. package/dist/cli/commands/projects.d.ts +14 -0
  29. package/dist/cli/commands/projects.js +146 -0
  30. package/dist/cli/commands/repair/checks.d.ts +2 -0
  31. package/dist/cli/commands/{repair.js → repair/checks.js} +19 -181
  32. package/dist/cli/commands/{repair.d.ts → repair/finders.d.ts} +0 -29
  33. package/dist/cli/commands/repair/finders.js +108 -0
  34. package/dist/cli/commands/repair/index.d.ts +12 -0
  35. package/dist/cli/commands/repair/index.js +61 -0
  36. package/dist/cli/commands/repair/types.d.ts +26 -0
  37. package/dist/cli/commands/repair/types.js +3 -0
  38. package/dist/cli/commands/restart.d.ts +6 -0
  39. package/dist/cli/commands/restart.js +85 -0
  40. package/dist/cli/commands/sessions.d.ts +2 -1
  41. package/dist/cli/commands/sessions.js +189 -45
  42. package/dist/cli/commands/setup/checks.d.ts +13 -0
  43. package/dist/cli/commands/setup/checks.js +71 -0
  44. package/dist/cli/commands/{setup.d.ts → setup/facts.d.ts} +1 -15
  45. package/dist/cli/commands/setup/facts.js +60 -0
  46. package/dist/cli/commands/setup/index.d.ts +2 -0
  47. package/dist/cli/commands/setup/index.js +43 -0
  48. package/dist/cli/commands/user-config.d.ts +8 -0
  49. package/dist/cli/commands/user-config.js +41 -0
  50. package/dist/cli/history.d.ts +11 -0
  51. package/dist/cli/history.js +21 -0
  52. package/dist/cli/index.d.ts +7 -1
  53. package/dist/cli/index.js +38 -13
  54. package/dist/cli/manifest.d.ts +8 -0
  55. package/dist/cli/manifest.js +35 -15
  56. package/dist/cli/menu-multi.d.ts +12 -0
  57. package/dist/cli/menu-multi.js +82 -0
  58. package/dist/cli/menu.d.ts +5 -13
  59. package/dist/cli/menu.js +149 -46
  60. package/dist/cli/progress.d.ts +9 -0
  61. package/dist/cli/progress.js +49 -0
  62. package/dist/cli/settings/edit.d.ts +13 -0
  63. package/dist/cli/settings/edit.js +19 -0
  64. package/dist/cli/settings/index.d.ts +8 -0
  65. package/dist/cli/settings/index.js +131 -0
  66. package/dist/cli/settings/prompts.d.ts +7 -0
  67. package/dist/cli/settings/prompts.js +48 -0
  68. package/dist/cli/settings/schema.d.ts +33 -0
  69. package/dist/cli/settings/schema.js +29 -0
  70. package/dist/cli/validate.d.ts +17 -0
  71. package/dist/cli/validate.js +89 -0
  72. package/dist/core/config.js +4 -1
  73. package/dist/core/disk.d.ts +16 -0
  74. package/dist/core/disk.js +55 -0
  75. package/dist/core/local-config.d.ts +8 -0
  76. package/dist/core/local-config.js +18 -5
  77. package/dist/core/project-policy.d.ts +15 -0
  78. package/dist/core/project-policy.js +35 -0
  79. package/dist/core/prompt.d.ts +4 -0
  80. package/dist/core/prompt.js +15 -0
  81. package/dist/core/safe-paths.d.ts +4 -0
  82. package/dist/core/safe-paths.js +37 -0
  83. package/dist/core/schema/common.d.ts +17 -0
  84. package/dist/core/schema/common.js +15 -0
  85. package/dist/core/schema/config.d.ts +594 -0
  86. package/dist/core/schema/config.js +63 -0
  87. package/dist/core/schema/databases.d.ts +33 -0
  88. package/dist/core/schema/databases.js +35 -0
  89. package/dist/core/schema/harness.d.ts +12 -0
  90. package/dist/core/schema/harness.js +17 -0
  91. package/dist/core/schema/json-schema.d.ts +5 -0
  92. package/dist/core/schema/json-schema.js +25 -0
  93. package/dist/core/schema/profile.d.ts +27 -0
  94. package/dist/core/schema/profile.js +29 -0
  95. package/dist/core/schema/projects.d.ts +9 -0
  96. package/dist/core/schema/projects.js +11 -0
  97. package/dist/core/schema/sandbox.d.ts +50 -0
  98. package/dist/core/schema/sandbox.js +36 -0
  99. package/dist/core/schema/toolchains.d.ts +93 -0
  100. package/dist/core/schema/toolchains.js +83 -0
  101. package/dist/core/schema.d.ts +6 -604
  102. package/dist/core/schema.js +8 -264
  103. package/dist/core/types.d.ts +29 -1
  104. package/dist/core/util.d.ts +7 -0
  105. package/dist/core/util.js +12 -3
  106. package/dist/core/workspace.d.ts +0 -5
  107. package/dist/core/workspace.js +12 -46
  108. package/dist/guest/boot-env.d.ts +4 -0
  109. package/dist/guest/boot-env.js +25 -0
  110. package/dist/guest/cmdline.d.ts +12 -0
  111. package/dist/guest/cmdline.js +23 -0
  112. package/dist/guest/daemon/exec.d.ts +5 -0
  113. package/dist/guest/daemon/exec.js +32 -0
  114. package/dist/guest/daemon/project.d.ts +14 -0
  115. package/dist/guest/daemon/project.js +132 -0
  116. package/dist/guest/daemon/session.d.ts +13 -0
  117. package/dist/guest/daemon/session.js +139 -0
  118. package/dist/guest/daemon/shutdown.d.ts +1 -0
  119. package/dist/guest/daemon/shutdown.js +21 -0
  120. package/dist/guest/daemon/state.d.ts +17 -0
  121. package/dist/guest/daemon/state.js +31 -0
  122. package/dist/guest/daemon.js +8 -141
  123. package/dist/guest/db.js +18 -3
  124. package/dist/guest/env.d.ts +2 -0
  125. package/dist/guest/env.js +4 -2
  126. package/dist/guest/guide.d.ts +6 -2
  127. package/dist/guest/guide.js +5 -4
  128. package/dist/guest/helpers.d.ts +3 -0
  129. package/dist/guest/helpers.js +26 -0
  130. package/dist/guest/main.js +17 -7
  131. package/dist/guest/ports.d.ts +6 -0
  132. package/dist/guest/ports.js +22 -0
  133. package/dist/guest/project-context.d.ts +7 -0
  134. package/dist/guest/project-context.js +10 -0
  135. package/dist/guest/project-env.d.ts +20 -0
  136. package/dist/guest/project-env.js +20 -0
  137. package/dist/guest/project-status.d.ts +5 -0
  138. package/dist/guest/project-status.js +17 -0
  139. package/dist/guest/ptyhost.d.ts +0 -18
  140. package/dist/guest/ptyhost.js +5 -63
  141. package/dist/guest/setup/agents.d.ts +5 -2
  142. package/dist/guest/setup/agents.js +22 -8
  143. package/dist/guest/setup/claude.js +11 -3
  144. package/dist/guest/setup/environment.d.ts +13 -3
  145. package/dist/guest/setup/environment.js +14 -10
  146. package/dist/guest/setup/hooks.d.ts +9 -1
  147. package/dist/guest/setup/hooks.js +28 -14
  148. package/dist/guest/toolchains/catalogue.d.ts +4 -0
  149. package/dist/guest/toolchains/catalogue.js +2 -2
  150. package/dist/guest/toolchains/cli.js +25 -17
  151. package/dist/guest/toolchains/common.d.ts +18 -1
  152. package/dist/guest/toolchains/common.js +34 -11
  153. package/dist/guest/toolchains/databases/dynamodb.d.ts +8 -0
  154. package/dist/guest/toolchains/databases/dynamodb.js +52 -0
  155. package/dist/guest/toolchains/{databases.d.ts → databases/index.d.ts} +2 -3
  156. package/dist/guest/toolchains/databases/index.js +62 -0
  157. package/dist/guest/toolchains/databases/mongodb.d.ts +3 -0
  158. package/dist/guest/toolchains/databases/mongodb.js +31 -0
  159. package/dist/guest/toolchains/databases/mysql.d.ts +3 -0
  160. package/dist/guest/toolchains/databases/mysql.js +38 -0
  161. package/dist/guest/toolchains/databases/postgres.d.ts +5 -0
  162. package/dist/guest/toolchains/databases/postgres.js +38 -0
  163. package/dist/guest/toolchains/databases/redis.d.ts +3 -0
  164. package/dist/guest/toolchains/databases/redis.js +16 -0
  165. package/dist/guest/toolchains/databases/server.d.ts +14 -0
  166. package/dist/guest/toolchains/databases/server.js +52 -0
  167. package/dist/guest/toolchains/git.js +25 -16
  168. package/dist/guest/toolchains/go.js +1 -1
  169. package/dist/guest/toolchains/index.d.ts +13 -9
  170. package/dist/guest/toolchains/index.js +40 -82
  171. package/dist/guest/toolchains/java.js +5 -5
  172. package/dist/guest/toolchains/leftovers.d.ts +2 -0
  173. package/dist/guest/toolchains/leftovers.js +26 -0
  174. package/dist/guest/toolchains/managers.js +12 -10
  175. package/dist/guest/toolchains/{binstall.js → native/binstall.js} +6 -9
  176. package/dist/guest/toolchains/native/cpp.d.ts +4 -0
  177. package/dist/guest/toolchains/native/cpp.js +38 -0
  178. package/dist/guest/toolchains/native/dotnet.d.ts +4 -0
  179. package/dist/guest/toolchains/native/dotnet.js +103 -0
  180. package/dist/guest/toolchains/native/mesa.d.ts +7 -0
  181. package/dist/guest/toolchains/native/mesa.js +46 -0
  182. package/dist/guest/toolchains/native/msvc/environment.d.ts +16 -0
  183. package/dist/guest/toolchains/native/msvc/environment.js +57 -0
  184. package/dist/guest/toolchains/native/msvc/index.d.ts +6 -0
  185. package/dist/guest/toolchains/native/msvc/index.js +48 -0
  186. package/dist/guest/toolchains/native/msvc/packages.d.ts +6 -0
  187. package/dist/guest/toolchains/native/msvc/packages.js +156 -0
  188. package/dist/guest/toolchains/native/msvc/tree.d.ts +15 -0
  189. package/dist/guest/toolchains/native/msvc/tree.js +21 -0
  190. package/dist/guest/toolchains/native/rust.d.ts +3 -0
  191. package/dist/guest/toolchains/native/rust.js +45 -0
  192. package/dist/guest/toolchains/node.js +26 -6
  193. package/dist/guest/toolchains/plan.d.ts +22 -0
  194. package/dist/guest/toolchains/plan.js +96 -0
  195. package/dist/guest/toolchains/pwsh.js +1 -1
  196. package/dist/guest/toolchains/python.js +17 -10
  197. package/dist/guest/toolchains/runtimes.d.ts +2 -0
  198. package/dist/guest/toolchains/runtimes.js +6 -8
  199. package/dist/guest/toolchains/scripting/php.d.ts +3 -0
  200. package/dist/guest/toolchains/scripting/php.js +71 -0
  201. package/dist/guest/toolchains/scripting/ruby.d.ts +3 -0
  202. package/dist/guest/toolchains/scripting/ruby.js +47 -0
  203. package/dist/guest/toolchains/scripting/vcruntime.d.ts +7 -0
  204. package/dist/guest/toolchains/scripting/vcruntime.js +45 -0
  205. package/dist/guest/view.js +21 -1
  206. package/dist/host/accounts.js +14 -5
  207. package/dist/host/agent-commands.d.ts +25 -0
  208. package/dist/host/agent-commands.js +145 -0
  209. package/dist/host/attach.d.ts +17 -0
  210. package/dist/host/attach.js +69 -0
  211. package/dist/host/channel.d.ts +23 -22
  212. package/dist/host/channel.js +66 -84
  213. package/dist/host/claude-profile.js +7 -3
  214. package/dist/host/cleanup.js +2 -11
  215. package/dist/host/database-plan.d.ts +12 -0
  216. package/dist/host/database-plan.js +52 -0
  217. package/dist/host/generate.d.ts +4 -12
  218. package/dist/host/generate.js +48 -135
  219. package/dist/host/join.d.ts +28 -0
  220. package/dist/host/join.js +76 -0
  221. package/dist/host/launch/mappings.d.ts +3 -0
  222. package/dist/host/launch/mappings.js +50 -0
  223. package/dist/host/launch/projects.d.ts +15 -0
  224. package/dist/host/launch/projects.js +56 -0
  225. package/dist/host/launch/stage.d.ts +11 -0
  226. package/dist/host/launch/stage.js +66 -0
  227. package/dist/host/launch/wsb.d.ts +10 -0
  228. package/dist/host/launch/wsb.js +34 -0
  229. package/dist/host/pc-scan.d.ts +6 -0
  230. package/dist/host/pc-scan.js +37 -0
  231. package/dist/host/platform.d.ts +2 -0
  232. package/dist/host/platform.js +13 -1
  233. package/dist/host/project-scan.d.ts +1 -17
  234. package/dist/host/project-scan.js +2 -86
  235. package/dist/host/projects.d.ts +100 -0
  236. package/dist/host/projects.js +147 -0
  237. package/dist/host/repo-guard.d.ts +15 -2
  238. package/dist/host/repo-guard.js +103 -12
  239. package/dist/host/terminal.js +3 -0
  240. package/dist/host/test-guard.d.ts +6 -0
  241. package/dist/host/test-guard.js +15 -0
  242. package/dist/host/window.js +7 -3
  243. package/dist/host/wsb.d.ts +37 -0
  244. package/dist/host/wsb.js +101 -0
  245. package/dist/runtime/daemon.js +10997 -2794
  246. package/dist/runtime/db.js +6428 -464
  247. package/dist/runtime/main.js +7359 -1135
  248. package/dist/runtime/view.js +5922 -47
  249. package/dist/selftest/accounts.test.d.ts +2 -0
  250. package/dist/selftest/accounts.test.js +44 -0
  251. package/dist/selftest/agent-commands.test.d.ts +2 -0
  252. package/dist/selftest/agent-commands.test.js +47 -0
  253. package/dist/selftest/channel.test.d.ts +2 -0
  254. package/dist/selftest/channel.test.js +37 -0
  255. package/dist/selftest/cli.test.d.ts +2 -2
  256. package/dist/selftest/cli.test.js +115 -27
  257. package/dist/selftest/cmdline.test.d.ts +2 -0
  258. package/dist/selftest/cmdline.test.js +45 -0
  259. package/dist/selftest/config.test.d.ts +2 -3
  260. package/dist/selftest/config.test.js +36 -72
  261. package/dist/selftest/data.test.d.ts +2 -2
  262. package/dist/selftest/data.test.js +77 -70
  263. package/dist/selftest/detect.test.d.ts +2 -2
  264. package/dist/selftest/detect.test.js +54 -38
  265. package/dist/selftest/fixtures.d.ts +44 -4
  266. package/dist/selftest/fixtures.js +40 -23
  267. package/dist/selftest/generate.test.d.ts +2 -0
  268. package/dist/selftest/generate.test.js +63 -0
  269. package/dist/selftest/git-access.test.d.ts +2 -0
  270. package/dist/selftest/git-access.test.js +26 -0
  271. package/dist/selftest/guest-boot.test.d.ts +2 -0
  272. package/dist/selftest/guest-boot.test.js +76 -0
  273. package/dist/selftest/harness.d.ts +44 -7
  274. package/dist/selftest/harness.js +62 -21
  275. package/dist/selftest/host-sim.test.d.ts +2 -0
  276. package/dist/selftest/host-sim.test.js +93 -0
  277. package/dist/selftest/hostile-input.test.d.ts +2 -0
  278. package/dist/selftest/hostile-input.test.js +54 -0
  279. package/dist/selftest/index.d.ts +8 -1
  280. package/dist/selftest/index.js +134 -35
  281. package/dist/selftest/links.test.d.ts +2 -0
  282. package/dist/selftest/links.test.js +50 -0
  283. package/dist/selftest/live-daemon.test.d.ts +2 -0
  284. package/dist/selftest/live-daemon.test.js +91 -0
  285. package/dist/selftest/live-git.test.d.ts +2 -0
  286. package/dist/selftest/live-git.test.js +60 -0
  287. package/dist/selftest/live-projects.test.d.ts +2 -0
  288. package/dist/selftest/live-projects.test.js +127 -0
  289. package/dist/selftest/live.d.ts +65 -0
  290. package/dist/selftest/live.js +109 -0
  291. package/dist/selftest/local-config.test.d.ts +2 -0
  292. package/dist/selftest/local-config.test.js +107 -0
  293. package/dist/selftest/mirror.test.d.ts +2 -3
  294. package/dist/selftest/mirror.test.js +92 -65
  295. package/dist/selftest/network.test.d.ts +2 -0
  296. package/dist/selftest/network.test.js +28 -0
  297. package/dist/selftest/personalization.test.d.ts +2 -0
  298. package/dist/selftest/personalization.test.js +53 -0
  299. package/dist/selftest/projects.test.d.ts +2 -0
  300. package/dist/selftest/projects.test.js +254 -0
  301. package/dist/selftest/repair.test.d.ts +2 -0
  302. package/dist/selftest/repair.test.js +42 -0
  303. package/dist/selftest/repo-guard.test.d.ts +2 -0
  304. package/dist/selftest/repo-guard.test.js +63 -0
  305. package/dist/selftest/secrets.test.d.ts +2 -0
  306. package/dist/selftest/secrets.test.js +71 -0
  307. package/dist/selftest/setup.test.d.ts +2 -2
  308. package/dist/selftest/setup.test.js +45 -22
  309. package/dist/selftest/suite.d.ts +13 -0
  310. package/dist/selftest/suite.js +1 -0
  311. package/dist/selftest/sync.test.d.ts +2 -3
  312. package/dist/selftest/sync.test.js +90 -78
  313. package/dist/selftest/test-guard.test.d.ts +2 -0
  314. package/dist/selftest/test-guard.test.js +49 -0
  315. package/dist/selftest/toolchains.test.d.ts +2 -0
  316. package/dist/selftest/toolchains.test.js +103 -0
  317. package/dist/selftest/workspace.test.d.ts +2 -0
  318. package/dist/selftest/workspace.test.js +53 -0
  319. package/dist/shared/download.d.ts +3 -1
  320. package/dist/shared/download.js +19 -4
  321. package/dist/shared/paths.d.ts +5 -0
  322. package/dist/shared/paths.js +6 -0
  323. package/dist/shared/projects.d.ts +53 -0
  324. package/dist/shared/projects.js +67 -0
  325. package/dist/shared/rundir.d.ts +10 -1
  326. package/dist/shared/rundir.js +12 -4
  327. package/dist/shared/safe-remove.js +11 -0
  328. package/dist/shared/schema.d.ts +443 -0
  329. package/dist/shared/schema.js +41 -2
  330. package/dist/shared/test-guard.d.ts +4 -0
  331. package/dist/shared/test-guard.js +37 -0
  332. package/dist/sync/claude-mem.js +3 -1
  333. package/dist/sync/history.d.ts +23 -12
  334. package/dist/sync/history.js +52 -20
  335. package/dist/sync/watcher.d.ts +8 -0
  336. package/dist/sync/watcher.js +23 -4
  337. package/package.json +6 -6
  338. package/sandbox/bootstrap.ps1 +258 -258
  339. package/sandbox/cpucap.ps1 +51 -0
  340. package/sandbox/logon.cmd +18 -16
  341. package/sandbox/openssl.cs +42 -0
  342. package/sandbox/ptyhost.cs +15 -4
  343. package/sandbox/trust.cmd +35 -35
  344. package/schemas/config.schema.json +100 -1
  345. package/schemas/sandbox-config.schema.json +17 -1
  346. package/dist/cli/commands/data.d.ts +0 -46
  347. package/dist/cli/commands/data.js +0 -249
  348. package/dist/cli/commands/setup.js +0 -152
  349. package/dist/cli/settings.d.ts +0 -11
  350. package/dist/cli/settings.js +0 -156
  351. package/dist/guest/toolchains/databases.js +0 -226
  352. package/dist/guest/toolchains/msvc.d.ts +0 -10
  353. package/dist/guest/toolchains/msvc.js +0 -216
  354. package/dist/guest/toolchains/native.d.ts +0 -12
  355. package/dist/guest/toolchains/native.js +0 -220
  356. package/dist/guest/toolchains/scripting.d.ts +0 -10
  357. package/dist/guest/toolchains/scripting.js +0 -137
  358. package/dist/selftest/guest.test.d.ts +0 -3
  359. package/dist/selftest/guest.test.js +0 -61
  360. package/dist/selftest/helpers.test.d.ts +0 -2
  361. package/dist/selftest/helpers.test.js +0 -125
  362. package/dist/selftest/host.test.d.ts +0 -2
  363. package/dist/selftest/host.test.js +0 -71
  364. package/dist/selftest/live.test.d.ts +0 -7
  365. package/dist/selftest/live.test.js +0 -127
  366. package/dist/selftest/security.test.d.ts +0 -3
  367. package/dist/selftest/security.test.js +0 -143
  368. /package/dist/guest/toolchains/{binstall.d.ts → native/binstall.d.ts} +0 -0
@@ -0,0 +1,60 @@
1
+ // What `sandbox setup` learns about this PC: the Windows edition, CPU virtualization and the Windows Sandbox feature.
2
+ import os from 'node:os';
3
+ import _ from 'lodash';
4
+ import { z } from 'zod';
5
+ import { freeBytesAt } from '../../../core/disk.js';
6
+ import { captureOutput, fileExists } from '../../../core/util.js';
7
+ import { SYSTEM, system32 } from '../../../shared/system.js';
8
+ const FEATURE = 'Containers-DisposableClientVM';
9
+ export const ENABLE_COMMAND = 'Enable-WindowsOptionalFeature -Online -FeatureName ' + FEATURE + ' -All -NoRestart';
10
+ /** `reg query <key>` output to { valueName: data }. */
11
+ export const parseRegQuery = (text) => Object.fromEntries(_.compact(text.split(/\r?\n/).map((line) => /^\s+(\S+)\s+REG_\w+\s+(.*)$/.exec(line)?.slice(1, 3))));
12
+ /** The "State : ..." line of `Dism /Get-FeatureInfo` (English output only; anything else is unknown). */
13
+ export const parseDismState = (text) => {
14
+ const state = /^State\s*:\s*(.+?)\s*$/im.exec(text)?.[1].toLowerCase() ?? '';
15
+ if (state === 'enabled')
16
+ return 'enabled';
17
+ if (state.includes('pending'))
18
+ return 'pending';
19
+ return state.startsWith('disabled') ? 'disabled' : 'unknown';
20
+ };
21
+ /** Win32_OptionalFeature.InstallState: 1 enabled, 2 disabled, 3 absent. */
22
+ export const featureFromInstallState = (state) => state === 1 ? 'enabled' : state === 2 || state === 3 ? 'disabled' : 'unknown';
23
+ // Fixed script (no outside data): CPU/hypervisor facts and the feature state, readable without admin rights.
24
+ const PROBE = [
25
+ '$ErrorActionPreference = "SilentlyContinue"',
26
+ '$cpu = @(Get-CimInstance Win32_Processor)',
27
+ '$cs = Get-CimInstance Win32_ComputerSystem',
28
+ '$f = Get-CimInstance Win32_OptionalFeature -Filter "Name=\'' + FEATURE + '\'"',
29
+ '$v = $null; if ($cpu.Count) { $v = [bool]($cpu | Where-Object { $_.VirtualizationFirmwareEnabled }) }',
30
+ '$h = $null; if ($cs) { $h = [bool]$cs.HypervisorPresent }',
31
+ '$s = $null; if ($f) { $s = [int]$f.InstallState }',
32
+ '@{ virtualizationFirmware = $v; hypervisorPresent = $h; installState = $s } | ConvertTo-Json -Compress',
33
+ ].join('; ');
34
+ const Probe = z.object({ virtualizationFirmware: z.boolean().nullable(), hypervisorPresent: z.boolean().nullable(), installState: z.number().nullable() }).partial();
35
+ export const parseProbe = (text) => {
36
+ try {
37
+ return Probe.parse(JSON.parse(_.last(text.trim().split(/\r?\n/)) ?? ''));
38
+ }
39
+ catch {
40
+ return {};
41
+ }
42
+ };
43
+ export const gatherFacts = (storageRoot) => {
44
+ const reg = parseRegQuery(captureOutput(SYSTEM.reg(), ['query', 'HKLM\\SOFTWARE\\Microsoft\\Windows NT\\CurrentVersion']));
45
+ const probe = parseProbe(captureOutput(SYSTEM.powershell(), ['-NoProfile', '-NonInteractive', '-Command', PROBE]));
46
+ // DISM knows about a pending restart but needs admin rights; the CIM state works for everyone.
47
+ const dism = parseDismState(captureOutput(SYSTEM.dism(), ['/English', '/Online', '/Get-FeatureInfo', '/FeatureName:' + FEATURE]));
48
+ return {
49
+ edition: reg.EditionID ?? null,
50
+ build: Number(reg.CurrentBuildNumber ?? reg.CurrentBuild) || null,
51
+ feature: dism === 'unknown' ? featureFromInstallState(probe.installState) : dism,
52
+ sandboxExe: fileExists(system32('WindowsSandbox.exe')),
53
+ virtualizationFirmware: probe.virtualizationFirmware ?? null,
54
+ hypervisorPresent: probe.hypervisorPresent ?? null,
55
+ totalRamBytes: os.totalmem(),
56
+ freeDiskBytes: freeBytesAt(storageRoot),
57
+ storageRoot,
58
+ nodeVersion: process.version,
59
+ };
60
+ };
@@ -0,0 +1,2 @@
1
+ import { type Context } from '../context.js';
2
+ export declare const setup: (ctx: Context) => Promise<number>;
@@ -0,0 +1,43 @@
1
+ // `sandbox setup`: checks this PC can run Windows Sandbox and offers to turn the feature on.
2
+ import { spawnSync } from 'node:child_process';
3
+ import { askYesNo } from '../../../core/prompt.js';
4
+ import { fail, isWin } from '../../../core/util.js';
5
+ import { refuseInSelfTest } from '../../../host/test-guard.js';
6
+ import { SYSTEM } from '../../../shared/system.js';
7
+ import { c } from '../../colors.js';
8
+ import { table } from '../context.js';
9
+ import { buildChecks, canEnableFeature } from './checks.js';
10
+ import { ENABLE_COMMAND, gatherFacts } from './facts.js';
11
+ const LABEL = { ok: c.green('OK'), warn: c.yellow('WARNING'), action: c.red('ACTION NEEDED') };
12
+ const print = (checks) => table(checks.map((check) => [LABEL[check.status], check.name, check.detail]));
13
+ /** Runs the enable command in an elevated PowerShell (Windows asks for permission); true if it worked. */
14
+ const enableFeature = () => {
15
+ refuseInSelfTest('turn on Windows features');
16
+ const inner = "'-NoProfile','-Command','" + ENABLE_COMMAND + " | Out-Null'";
17
+ const outer = "try { $p = Start-Process -FilePath '" + SYSTEM.powershell() + "' -Verb RunAs -Wait -PassThru -WindowStyle Hidden -ArgumentList " + inner + '; exit $p.ExitCode } catch { exit 1 }';
18
+ return spawnSync(SYSTEM.powershell(), ['-NoProfile', '-NonInteractive', '-Command', outer], { stdio: 'inherit' }).status === 0;
19
+ };
20
+ export const setup = async (ctx) => {
21
+ if (!isWin)
22
+ fail('Windows Sandbox only runs on Windows.', 'Run sandbox setup on a Windows 10/11 Pro, Enterprise or Education PC.');
23
+ console.log('Checking this PC for Windows Sandbox...\n');
24
+ const facts = gatherFacts(ctx.storageRoot);
25
+ let checks = buildChecks(facts);
26
+ print(checks);
27
+ if (canEnableFeature(facts)) {
28
+ console.log('');
29
+ if (await askYesNo('Turn on Windows Sandbox now? Windows will ask for admin permission.')) {
30
+ console.log('Enabling Windows Sandbox (this can take a minute)...');
31
+ if (!enableFeature())
32
+ fail('Windows Sandbox could not be enabled (or the admin prompt was declined).', 'In an admin PowerShell run: ' + ENABLE_COMMAND + ', then restart Windows.');
33
+ console.log('Done. Restart Windows to finish, then run sandbox setup again.\n');
34
+ checks = buildChecks(gatherFacts(ctx.storageRoot));
35
+ print(checks);
36
+ }
37
+ else
38
+ console.log('To do it yourself, in an admin PowerShell run:\n ' + ENABLE_COMMAND + '\nthen restart Windows.');
39
+ }
40
+ const todo = checks.filter((check) => check.status === 'action').length;
41
+ console.log('\n' + (todo ? todo + ' thing(s) to fix before the sandbox can start (see ACTION NEEDED above).' : 'This PC is ready. cd into a project and run sandbox claude (or sandbox for no agent).'));
42
+ return todo ? 1 : 0;
43
+ };
@@ -0,0 +1,8 @@
1
+ import type { Context } from './context.js';
2
+ export declare const configFile: (ctx: Context) => string;
3
+ /** The value as written in your file (not merged with the defaults). */
4
+ export declare const fileSetting: <T>(ctx: Context, key: string[], fallback: T) => T;
5
+ export declare const setSetting: (ctx: Context, key: string[], value: unknown) => void;
6
+ /** Adds a folder to a list setting (projects.roots, projects.exclude, ...) unless it is there already. */
7
+ export declare const addToList: (ctx: Context, key: string[], folder: string) => boolean;
8
+ export declare const removeFromList: (ctx: Context, key: string[], folder: string) => boolean;
@@ -0,0 +1,41 @@
1
+ // Changing your own config file from a command (sandbox projects ..., the "add this root?" question): one setting at
2
+ // a time through the settings editor, so comments and formatting stay and an invalid result is never written.
3
+ import fs from 'node:fs';
4
+ import path from 'node:path';
5
+ import { parse } from 'jsonc-parser';
6
+ import _ from 'lodash';
7
+ import { userConfigPath } from '../../core/config.js';
8
+ import { fail } from '../../core/util.js';
9
+ import { editConfigText } from '../settings/edit.js';
10
+ export const configFile = (ctx) => ctx.loaded.configPath ?? userConfigPath();
11
+ /** The value as written in your file (not merged with the defaults). */
12
+ export const fileSetting = (ctx, key, fallback) => {
13
+ const file = configFile(ctx);
14
+ const text = fs.existsSync(file) ? fs.readFileSync(file, 'utf8') : '';
15
+ return _.get(parse(text) ?? {}, key) ?? fallback;
16
+ };
17
+ export const setSetting = (ctx, key, value) => {
18
+ const file = configFile(ctx);
19
+ const text = fs.existsSync(file) ? fs.readFileSync(file, 'utf8') : '';
20
+ const result = editConfigText(text, key, value);
21
+ if ('problems' in result)
22
+ fail('Could not change ' + key.join('.') + ':\n ' + result.problems.join('\n '));
23
+ fs.mkdirSync(path.dirname(file), { recursive: true });
24
+ fs.writeFileSync(file, result.text, 'utf8');
25
+ };
26
+ /** Adds a folder to a list setting (projects.roots, projects.exclude, ...) unless it is there already. */
27
+ export const addToList = (ctx, key, folder) => {
28
+ const list = fileSetting(ctx, key, []);
29
+ if (list.some((p) => path.resolve(p).toLowerCase() === path.resolve(folder).toLowerCase()))
30
+ return false;
31
+ setSetting(ctx, key, [...list, folder]);
32
+ return true;
33
+ };
34
+ export const removeFromList = (ctx, key, folder) => {
35
+ const list = fileSetting(ctx, key, []);
36
+ const rest = list.filter((p) => path.resolve(p).toLowerCase() !== path.resolve(folder).toLowerCase());
37
+ if (rest.length === list.length)
38
+ return false;
39
+ setSetting(ctx, key, rest);
40
+ return true;
41
+ };
@@ -0,0 +1,11 @@
1
+ import { z } from 'zod';
2
+ declare const History: z.ZodArray<z.ZodObject<{
3
+ argv: z.ZodArray<z.ZodString>;
4
+ at: z.ZodString;
5
+ }, z.core.$strict>>;
6
+ export declare const readHistory: () => z.output<typeof History>;
7
+ /** Remembers a command (menu or typed) so the menu can offer it again. Never fails the command itself. */
8
+ export declare const recordHistory: (argv: string[]) => void;
9
+ /** argv as you would type it (quoted where needed). */
10
+ export declare const commandLine: (argv: string[]) => string;
11
+ export {};
@@ -0,0 +1,21 @@
1
+ // The last commands run (menu or typed), kept in ~/.agent-sandbox/history.json so the menu can offer them again.
2
+ import path from 'node:path';
3
+ import { z } from 'zod';
4
+ import { userDir } from '../core/config.js';
5
+ import { writeJson } from '../core/util.js';
6
+ import { readValid } from '../shared/rundir.js';
7
+ const History = z.array(z.strictObject({ argv: z.array(z.string()).max(200), at: z.string() })).max(20);
8
+ const historyFile = () => path.join(userDir(), 'history.json');
9
+ const HIDDEN = new Set(['menu', '_watch', 'self-test']);
10
+ export const readHistory = () => readValid(History, historyFile()) ?? [];
11
+ /** Remembers a command (menu or typed) so the menu can offer it again. Never fails the command itself. */
12
+ export const recordHistory = (argv) => {
13
+ if (!argv.length || HIDDEN.has(argv[0]) || argv.includes('--help'))
14
+ return;
15
+ try {
16
+ writeJson(historyFile(), [{ argv, at: new Date().toISOString() }, ...readHistory().filter((h) => h.argv.join('\0') !== argv.join('\0'))].slice(0, 10));
17
+ }
18
+ catch { /* history is a convenience */ }
19
+ };
20
+ /** argv as you would type it (quoted where needed). */
21
+ export const commandLine = (argv) => ['sandbox', ...argv].map((a) => (/[\s"]/.test(a) || a === '' ? '"' + a.replace(/"/g, '\\"') + '"' : a)).join(' ');
@@ -4,11 +4,17 @@ export interface Preset {
4
4
  harness?: string;
5
5
  command?: string;
6
6
  }
7
+ /** `--config <file>` right after the command name (where every command accepts it); argv unchanged without a config. */
8
+ export declare const withConfig: (argv: string[], config: string | undefined) => string[];
9
+ /** `sandbox --config f.json status` works like `sandbox status --config f.json`: a leading --config moves behind the command. */
10
+ export declare const hoistConfig: (argv: string[]) => string[];
7
11
  /**
8
12
  * CLI entry. `preset` lets wrapper binaries fix the command/agent (sandbox claude = launch --harness claude). No arguments
9
13
  * opens the menu; `sandbox <agent> [agent args]` starts that agent.
10
14
  */
11
- export declare const main: (argv: string[], preset?: Preset) => Promise<number>;
15
+ export declare const main: (rawArgv: string[], preset?: Preset) => Promise<number>;
16
+ /** argv without `--config <file>` / `--config=<file>`. */
17
+ export declare const withoutConfig: (argv: string[]) => string[];
12
18
  /** Prints a problem the way a user can act on it: what happened, how to fix it; stack traces only for real bugs. */
13
19
  export declare const report: (e: unknown) => number;
14
20
  export declare const run: (argv: string[], preset?: Preset) => void;
package/dist/cli/index.js CHANGED
@@ -3,7 +3,10 @@ import { fileURLToPath } from 'node:url';
3
3
  import { CommanderError } from 'commander';
4
4
  import { buildProgram, parseCli } from './args.js';
5
5
  import { commandSpec } from './manifest.js';
6
- import { menu, recordHistory } from './menu.js';
6
+ import { c } from './colors.js';
7
+ import { recordHistory } from './history.js';
8
+ import { menu } from './menu.js';
9
+ import { validateOptions } from './validate.js';
7
10
  import { loadConfig } from '../core/config.js';
8
11
  import * as commands from './commands/index.js';
9
12
  import { UserError, errMsg, fail } from '../core/util.js';
@@ -17,12 +20,14 @@ const HANDLERS = {
17
20
  sessions: commands.sessions,
18
21
  status: commands.status,
19
22
  exec: commands.exec,
23
+ 'agent-commands': commands.agentCommandsCommand,
20
24
  db: commands.db,
21
25
  restart: commands.restart,
22
26
  stop: commands.stop,
23
27
  show: commands.show,
24
28
  hide: commands.hide,
25
29
  account: commands.account,
30
+ projects: commands.projects,
26
31
  sync: commands.sync,
27
32
  logs: commands.logs,
28
33
  clean: commands.clean,
@@ -33,15 +38,27 @@ const HANDLERS = {
33
38
  'host-claude': commands.claudeHost,
34
39
  _watch: commands.watcher,
35
40
  };
36
- const red = (s) => '\x1b[31m' + s + '\x1b[0m';
37
- const dim = (s) => '\x1b[2m' + s + '\x1b[0m';
38
41
  /** 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));
42
+ const runFromMenu = (config) => (argv) => main(withConfig(argv, config)).catch((e) => report(e));
43
+ /** `--config <file>` right after the command name (where every command accepts it); argv unchanged without a config. */
44
+ export const withConfig = (argv, config) => (config && argv.length ? [argv[0], '--config', config, ...argv.slice(1)] : argv);
45
+ /** `sandbox --config f.json status` works like `sandbox status --config f.json`: a leading --config moves behind the command. */
46
+ export const hoistConfig = (argv) => {
47
+ const eq = argv[0]?.startsWith('--config=');
48
+ if (argv[0] !== '--config' && !eq)
49
+ return argv;
50
+ const value = eq ? argv[0].slice('--config='.length) : argv[1];
51
+ const rest = argv.slice(eq ? 1 : 2);
52
+ if (value === undefined || value === '')
53
+ return argv;
54
+ return rest.length ? withConfig(rest, value) : ['menu', '--config', value];
55
+ };
40
56
  /**
41
57
  * CLI entry. `preset` lets wrapper binaries fix the command/agent (sandbox claude = launch --harness claude). No arguments
42
58
  * opens the menu; `sandbox <agent> [agent args]` starts that agent.
43
59
  */
44
- export const main = async (argv, preset = {}) => {
60
+ export const main = async (rawArgv, preset = {}) => {
61
+ const argv = preset.command || preset.harness ? rawArgv : hoistConfig(rawArgv);
45
62
  const isAgent = (name) => {
46
63
  if (!name || name.startsWith('-') || commandSpec(name))
47
64
  return false;
@@ -62,32 +79,40 @@ export const main = async (argv, preset = {}) => {
62
79
  const options = parseCli(preset.command ? [preset.command, ...argv] : argv, Boolean(preset.harness));
63
80
  if (preset.harness)
64
81
  options.harness = preset.harness;
65
- recordHistory(preset.harness ? ['launch', '--harness', preset.harness, ...argv] : preset.command ? [preset.command, ...argv] : argv);
82
+ const typed = preset.harness ? ['launch', '--harness', preset.harness, ...argv] : preset.command ? [preset.command, ...argv] : argv;
66
83
  if (options.command === 'menu')
67
- return menu(commands.createContext(packageRoot, entry, options), runFromMenu);
84
+ return menu(commands.createContext(packageRoot, entry, options), runFromMenu(options.config));
68
85
  // Loaded only when asked for: normal commands never load test code.
69
86
  if (options.command === 'self-test')
70
- return (await import('../selftest/index.js')).selfTest(packageRoot);
87
+ return (await import('../selftest/index.js')).selfTest(packageRoot, options.positionals);
71
88
  // repair must run even when the config file is broken (createContext would refuse it).
72
- if (options.command === 'repair')
89
+ if (options.command === 'repair') {
90
+ recordHistory(withoutConfig(typed));
73
91
  return commands.repairCommand(packageRoot, entry, options);
92
+ }
74
93
  const handler = HANDLERS[options.command];
75
94
  if (!handler)
76
95
  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;
96
+ const ctx = commands.createContext(packageRoot, entry, options);
97
+ validateOptions(ctx);
98
+ // Only what parsed and validated is offered again by the menu, without the --config (it is the same every time).
99
+ recordHistory(withoutConfig(typed));
100
+ return (await handler(ctx)) ?? 0;
78
101
  };
102
+ /** argv without `--config <file>` / `--config=<file>`. */
103
+ export const withoutConfig = (argv) => argv.filter((a, i) => a !== '--config' && argv[i - 1] !== '--config' && !a.startsWith('--config='));
79
104
  /** Prints a problem the way a user can act on it: what happened, how to fix it; stack traces only for real bugs. */
80
105
  export const report = (e) => {
81
106
  if (e instanceof CommanderError)
82
107
  return ['commander.helpDisplayed', 'commander.help', 'commander.version'].includes(e.code) ? 0 : e.exitCode || 1;
83
108
  if (e instanceof UserError) {
84
- console.error(red('Error: ') + e.message);
109
+ console.error(c.red('Error: ') + e.message);
85
110
  if (e.fix)
86
111
  console.error(' Fix: ' + e.fix);
87
112
  return 1;
88
113
  }
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')));
114
+ console.error(c.red('Unexpected error: ') + errMsg(e));
115
+ console.error(c.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
116
  console.error(' Please report it with the output of: sandbox doctor');
92
117
  return 1;
93
118
  };
@@ -17,6 +17,8 @@ export declare const OptionSpec: z.ZodObject<{
17
17
  accounts: "accounts";
18
18
  }>]>>;
19
19
  advanced: z.ZodOptional<z.ZodBoolean>;
20
+ when: z.ZodOptional<z.ZodString>;
21
+ noMenu: z.ZodOptional<z.ZodBoolean>;
20
22
  }, z.core.$strict>;
21
23
  export declare const ArgSpec: z.ZodObject<{
22
24
  name: z.ZodString;
@@ -28,7 +30,10 @@ export declare const ArgSpec: z.ZodObject<{
28
30
  sessions: "sessions";
29
31
  accounts: "accounts";
30
32
  }>]>>;
33
+ when: z.ZodOptional<z.ZodUnion<readonly [z.ZodString, z.ZodArray<z.ZodString>]>>;
31
34
  }, z.core.$strict>;
35
+ /** Is an argument or option with this `when` asked, given the first argument's answer? */
36
+ export declare const askedWhen: (when: string | string[] | undefined, first: string) => boolean;
32
37
  export declare const Group: z.ZodEnum<{
33
38
  Agents: "Agents";
34
39
  Sandbox: "Sandbox";
@@ -60,6 +65,7 @@ export declare const CommandSpec: z.ZodObject<{
60
65
  sessions: "sessions";
61
66
  accounts: "accounts";
62
67
  }>]>>;
68
+ when: z.ZodOptional<z.ZodUnion<readonly [z.ZodString, z.ZodArray<z.ZodString>]>>;
63
69
  }, z.core.$strict>>;
64
70
  options: z.ZodArray<z.ZodObject<{
65
71
  flag: z.ZodString;
@@ -79,6 +85,8 @@ export declare const CommandSpec: z.ZodObject<{
79
85
  accounts: "accounts";
80
86
  }>]>>;
81
87
  advanced: z.ZodOptional<z.ZodBoolean>;
88
+ when: z.ZodOptional<z.ZodString>;
89
+ noMenu: z.ZodOptional<z.ZodBoolean>;
82
90
  }, z.core.$strict>>;
83
91
  passthrough: z.ZodOptional<z.ZodEnum<{
84
92
  command: "command";
@@ -13,6 +13,10 @@ export const OptionSpec = z.strictObject({
13
13
  choices: Choices.optional(),
14
14
  /** Advanced options are only asked in the menu after "Show advanced options". */
15
15
  advanced: z.boolean().optional(),
16
+ /** Only asked in the menu when the command's first argument has this value (account save: --from-host). */
17
+ when: z.string().optional(),
18
+ /** Never offered in the menu (dangerous shortcuts such as --yes); typing it on the command line still works. */
19
+ noMenu: z.boolean().optional(),
16
20
  });
17
21
  export const ArgSpec = z.strictObject({
18
22
  name: z.string(),
@@ -20,7 +24,11 @@ export const ArgSpec = z.strictObject({
20
24
  required: z.boolean(),
21
25
  variadic: z.boolean().optional(),
22
26
  choices: Choices.optional(),
27
+ /** Only asked in the menu when the command's first argument has this value, or one of these (data clean: targets). */
28
+ when: z.union([z.string(), z.array(z.string())]).optional(),
23
29
  });
30
+ /** Is an argument or option with this `when` asked, given the first argument's answer? */
31
+ export const askedWhen = (when, first) => when === undefined || (Array.isArray(when) ? when.includes(first) : when === first);
24
32
  export const Group = z.enum(['Agents', 'Sandbox', 'Sessions', 'Accounts', 'Data', 'Health', 'Config']);
25
33
  export const CommandSpec = z.strictObject({
26
34
  name: z.string(),
@@ -38,49 +46,61 @@ export const CommandSpec = z.strictObject({
38
46
  const bool = (flag, description, extra = {}) => ({ flag, type: 'boolean', description, ...extra });
39
47
  const text = (flag, value, description, extra = {}) => ({ flag, type: 'string', value, description, ...extra });
40
48
  const SANDBOX_OPTIONS = [
41
- bool('--terminal', 'run in THIS terminal (reattaches to this folder\'s session if nobody is watching it)', { short: '-t' }),
49
+ bool('--terminal', 'attach this terminal (reuses this folder\'s session if nobody is watching it)', { short: '-t' }),
42
50
  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'),
51
+ bool('--new', 'always start a new session, even if this folder has an unattached one'),
44
52
  text('--workspace', 'dir', 'use this folder instead of the current one'),
45
53
  text('--node', 'version', 'Node version for nvm (default: .sandbox-config.json > .nvmrc > this PC)', { advanced: true }),
46
54
  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 },
55
+ { flag: '--database', type: 'list', value: 'name', description: 'also start this database (postgres, mysql, mongodb, redis, dynamodb; repeatable)', choices: ['postgres', 'mysql', 'mongodb', 'redis', 'dynamodb'], advanced: true },
48
56
  { flag: '--script', type: 'list', value: 'file', description: 'run this .ps1/.mjs/.cmd inside the sandbox first (repeatable)', advanced: true },
49
57
  bool('--window', 'show the sandbox window even if config says headless', { advanced: true }),
50
58
  bool('--trust-config', 'approve the folder\'s .sandbox-config.json without asking', { advanced: true }),
51
59
  bool('--no-local-config', 'ignore the folder\'s .sandbox-config.json', { advanced: true }),
60
+ bool('--yes', 'when this folder is not in the running sandbox and adding it needs a restart, restart without asking', { noMenu: true }),
52
61
  ];
53
62
  const AGENT_OPTIONS = [
54
63
  text('--harness', 'name', 'agent to run', { choices: 'harnesses' }),
64
+ { flag: '--session', type: 'list', value: 'agent@folder', description: 'start several sessions at once, e.g. --session claude@C:\\code\\api --session "codex@C:\\code\\web|--model o3" (repeatable; after | = that agent\'s own arguments; -t attaches the first)', noMenu: true },
55
65
  text('--account', 'name', 'Claude account for this sandbox', { choices: 'accounts' }),
56
66
  ...SANDBOX_OPTIONS,
57
- bool('--no-agent', 'provision the sandbox without starting an agent', { advanced: true }),
67
+ bool('--no-agent', 'set up the sandbox but do not start an agent', { advanced: true }),
58
68
  ];
59
69
  const json = true;
60
70
  export const COMMANDS = z.array(CommandSpec).parse([
61
71
  { 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' },
72
+ { name: 'start', summary: 'start a sandbox with no agent (tools and databases only)', group: 'Sandbox', args: [], options: SANDBOX_OPTIONS.filter((o) => o.flag !== '--new'), interactive: true },
73
+ { name: 'generate', summary: 'show what a start would do, without starting anything', group: 'Health', args: [], options: AGENT_OPTIONS, passthrough: 'agent' },
64
74
  { 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
75
  { 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 },
76
+ { name: 'status', summary: 'show what the sandbox is doing', group: 'Sandbox', args: [], options: [], json },
67
77
  { 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
78
  { 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: [] },
79
+ { name: 'restart', summary: 'restart this project\'s running agents in place; they resume their conversation', group: 'Sessions', args: [], options: [text('--harness', 'name', 'only this agent', { choices: 'harnesses' }), bool('--all', 'every project\'s sessions, not only this folder\'s')] },
80
+ { name: 'stop', summary: 'shut the sandbox down (every project\'s sessions end)', group: 'Sandbox', args: [], options: [bool('--yes', 'do not ask when other projects\' sessions would end', { noMenu: true })] },
81
+ {
82
+ name: 'projects', summary: 'the projects a sandbox can open without a restart: list, choose, roots, all/exclude', group: 'Sandbox', json,
83
+ args: [
84
+ { name: 'action', description: 'list, choose, add, remove, include, exclude, all, roots', required: false, choices: ['list', 'choose', 'add', 'remove', 'include', 'exclude', 'all', 'roots'] },
85
+ { name: 'value', description: 'a folder (add, remove, include, exclude), on/off (all), or list/add/remove (roots)', required: false, when: ['add', 'remove', 'include', 'exclude', 'all', 'roots'] },
86
+ { name: 'folder', description: '(roots add/remove) the folder', required: false, when: 'roots' },
87
+ ],
88
+ options: [],
89
+ },
71
90
  { name: 'show', summary: 'show the sandbox window', group: 'Sandbox', args: [], options: [] },
72
91
  { 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 },
92
+ { name: 'account', summary: 'Claude logins: 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', { when: 'save' }), bool('--no-restart', '(use) do not restart running sessions', { when: 'use' })], json },
93
+ { name: 'data', summary: 'show what is kept between sandboxes (tools, databases, logins, logs), or delete some of 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, when: 'clean' }], options: [bool('--yes', 'do not ask before deleting', { noMenu: true })], json },
75
94
  { 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: [] },
95
+ { name: 'sync', summary: 'copy conversation history back to this PC now', group: 'Data', args: [], options: [] },
77
96
  { name: 'logs', summary: 'print the newest sandbox log', group: 'Health', args: [], options: [bool('--follow', 'keep printing new lines', { short: '-f' })] },
78
97
  { name: 'setup', summary: 'check this PC can run Windows Sandbox and offer to turn it on', group: 'Health', args: [], options: [] },
79
98
  { 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
99
  { 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 },
100
+ { name: 'self-test', summary: 'run the built-in tests', group: 'Health', args: [{ name: 'suite', description: 'run only these test suites, e.g. config live-daemon (default: all)', required: false, variadic: true }], options: [] },
101
+ { name: 'config', summary: 'show or create your settings file, 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')] },
102
+ { name: 'agent-commands', summary: '(Beta) make claude, codex ... on this PC start in a hidden sandbox and attach', group: 'Agents', args: [{ name: 'action', description: 'on, off or status', required: false, choices: ['status', 'on', 'off'] }], options: [] },
103
+ { name: 'host-claude', summary: 'run Claude on this PC, NOT sandboxed, with its own profile', group: 'Agents', args: [], options: [bool('--personal', 'use your normal Claude profile')], passthrough: 'agent', interactive: true },
84
104
  { name: 'menu', summary: 'the interactive menu (what `sandbox` opens with no arguments)', group: 'Config', args: [], options: [], hidden: true, interactive: true },
85
105
  { name: '_watch', summary: 'background history sync', group: 'Data', args: [], options: [], hidden: true },
86
106
  ]);
@@ -0,0 +1,12 @@
1
+ import type { Context } from './commands/context.js';
2
+ /** Folders worth offering: this one, the projects started before, and the folders inside your roots. */
3
+ export declare const candidateFolders: (ctx: Context, cwd?: string) => string[];
4
+ /** The command line for several sessions: one --session per (agent, folder), with the shared options. */
5
+ export declare const severalAgentsArgv: (sessions: {
6
+ harness: string;
7
+ folder: string;
8
+ }[], o: {
9
+ headless: boolean;
10
+ attachFirst: boolean;
11
+ }) => string[];
12
+ export declare const askSeveralAgents: (ctx: Context) => Promise<string[] | null>;
@@ -0,0 +1,82 @@
1
+ // The menu's "Start several agents": projects, then agents for each, then options; it only builds a command line
2
+ // (sandbox launch --session <agent>@<folder> ...), so "repeat last" works and typing it does the same.
3
+ import fs from 'node:fs';
4
+ import path from 'node:path';
5
+ import { checkbox, confirm, input, select } from '@inquirer/prompts';
6
+ import { assertSafeWorkspace } from '../core/safe-paths.js';
7
+ import { expandPath, errMsg } from '../core/util.js';
8
+ import { readKnown } from '../host/projects.js';
9
+ import { isWithin } from '../shared/projects.js';
10
+ import { c, pageSizeFor, shortenMiddle, termWidth } from './colors.js';
11
+ import { commandLine } from './history.js';
12
+ const ADD = '@add';
13
+ /** Folders worth offering: this one, the projects started before, and the folders inside your roots. */
14
+ export const candidateFolders = (ctx, cwd = process.cwd()) => {
15
+ const roots = ctx.loaded.config.projects.roots.map((r) => expandPath(r, ctx.loaded.base));
16
+ const inRoots = roots.flatMap((r) => { try {
17
+ return fs.readdirSync(r, { withFileTypes: true }).filter((e) => e.isDirectory() && !e.isSymbolicLink() && !e.name.startsWith('.')).slice(0, 200).map((e) => path.join(r, e.name));
18
+ }
19
+ catch {
20
+ return [];
21
+ } });
22
+ const all = [path.resolve(cwd), ...readKnown(ctx.storageRoot).map((k) => k.host), ...inRoots];
23
+ return all.filter((f, i) => all.findIndex((g) => g.toLowerCase() === f.toLowerCase()) === i && fs.existsSync(f));
24
+ };
25
+ /** The command line for several sessions: one --session per (agent, folder), with the shared options. */
26
+ export const severalAgentsArgv = (sessions, o) => ['launch', ...sessions.flatMap((s) => ['--session', s.harness + '@' + s.folder]), ...(o.headless ? ['--headless'] : []), ...(o.attachFirst ? ['-t'] : [])];
27
+ export const askSeveralAgents = async (ctx) => {
28
+ const here = path.resolve(process.cwd());
29
+ const width = Math.max(20, termWidth() - 14);
30
+ let folders = await checkbox({
31
+ message: 'Which projects? (each gets its sessions; all of them are mapped into one sandbox)',
32
+ pageSize: pageSizeFor(16),
33
+ choices: [
34
+ ...candidateFolders(ctx).map((f) => ({ value: f, name: shortenMiddle(f, width) + (isWithin(f, here) && isWithin(here, f) ? c.dim(' (this folder)') : ''), checked: f.toLowerCase() === here.toLowerCase() })),
35
+ { value: ADD, name: 'Add a folder...' },
36
+ ],
37
+ });
38
+ if (folders.includes(ADD)) {
39
+ const extra = await input({
40
+ message: 'Folder to add:',
41
+ validate: (v) => {
42
+ const dir = path.resolve(v.trim());
43
+ if (!v.trim() || !fs.existsSync(dir) || !fs.statSync(dir).isDirectory())
44
+ return 'Not a folder: ' + dir;
45
+ try {
46
+ assertSafeWorkspace(dir, ctx.packageRoot, ctx.storageRoot);
47
+ return true;
48
+ }
49
+ catch (e) {
50
+ return errMsg(e);
51
+ }
52
+ },
53
+ });
54
+ folders = [...folders.filter((f) => f !== ADD), path.resolve(extra.trim())];
55
+ }
56
+ if (!folders.length)
57
+ return null;
58
+ const agents = Object.keys(ctx.loaded.config.harnesses);
59
+ const sessions = [];
60
+ for (const folder of folders) {
61
+ const picked = await checkbox({
62
+ message: 'Agents for ' + shortenMiddle(folder, width - 10) + ' (one session each):',
63
+ pageSize: pageSizeFor(agents.length),
64
+ choices: agents.map((a) => ({ value: a, name: a, checked: a === ctx.loaded.config.defaultHarness })),
65
+ required: true,
66
+ });
67
+ sessions.push(...picked.map((harness) => ({ harness, folder })));
68
+ }
69
+ const headless = await confirm({ message: 'Hide the sandbox window (headless)?', default: ctx.loaded.config.sandbox.headless });
70
+ const attach = await select({
71
+ message: 'Attach this terminal to:',
72
+ choices: [{ name: 'none (each has a tab inside; sandbox attach shows any of them)', value: -1 }, ...sessions.map((s, i) => ({ name: s.harness + ' in ' + shortenMiddle(s.folder, width - 20), value: i }))],
73
+ default: 0,
74
+ });
75
+ // The one to attach goes first: -t attaches the first session.
76
+ const ordered = attach >= 0 ? [sessions[attach], ...sessions.filter((_s, i) => i !== attach)] : sessions;
77
+ const projects = [...new Set(sessions.map((s) => path.basename(s.folder)))];
78
+ console.log('Start 1 sandbox with ' + sessions.length + ' session' + (sessions.length === 1 ? '' : 's') + ': ' + ordered.map((s) => s.harness + ' in ' + path.basename(s.folder)).join(', ') + ' - map ' + projects.join(' and '));
79
+ const argv = severalAgentsArgv(ordered, { headless, attachFirst: attach >= 0 });
80
+ console.log(c.dim(' ' + commandLine(argv)));
81
+ return await confirm({ message: 'Start them?', default: true }) ? argv : null;
82
+ };
@@ -1,17 +1,10 @@
1
- import { z } from 'zod';
2
1
  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;
2
+ import { splitArgs } from './args.js';
3
+ import { type CommandSpecT, type OptionSpecT } from './manifest.js';
13
4
  /** Splits "a b 'c d'" like a shell would (enough for agent arguments and exec commands). */
14
- export declare const splitArgs: (text: string) => string[];
5
+ export { splitArgs };
6
+ /** What this option accepts, checked while typing (undefined = anything). */
7
+ export declare const optionProblem: (o: OptionSpecT, v: string) => string | undefined;
15
8
  /** Asks everything a command takes and returns its argv (or null when the user backs out). */
16
9
  export declare const askCommand: (ctx: Context, spec: CommandSpecT, preset?: string[]) => Promise<string[] | null>;
17
10
  /**
@@ -19,4 +12,3 @@ export declare const askCommand: (ctx: Context, spec: CommandSpecT, preset?: str
19
12
  * an argv exactly like typing it.
20
13
  */
21
14
  export declare const menu: (ctx: Context, run: (argv: string[]) => Promise<number>) => Promise<number>;
22
- export {};