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,42 @@
1
+ // openssl.exe stand-in on the sandbox's PATH (tools\shims), running the copy Git for Windows ships in
2
+ // <tools>\git\mingw64\bin. A real .exe, so CreateProcess callers (Rust's Command::new("openssl"), Node's spawn) find it,
3
+ // which a .cmd would not be; and mingw64\bin itself stays off PATH, where it would shadow the MinGW gcc.
4
+ // It finds Git relative to itself (<tools>\shims\openssl.exe -> <tools>\git\...) and sets OPENSSL_CONF for the child
5
+ // only when the caller has none. C# 5 (compiled in the sandbox by the .NET Framework csc).
6
+ using System;
7
+ using System.Diagnostics;
8
+ using System.IO;
9
+
10
+ static class OpensslShim
11
+ {
12
+ static int Main()
13
+ {
14
+ string here = Path.GetDirectoryName(System.Reflection.Assembly.GetExecutingAssembly().Location);
15
+ string mingw = Path.GetFullPath(Path.Combine(here, "..\\git\\mingw64"));
16
+ string exe = Path.Combine(mingw, "bin\\openssl.exe");
17
+ if (!File.Exists(exe))
18
+ {
19
+ Console.Error.WriteLine("openssl is not available: Git for Windows is not installed in this sandbox.");
20
+ return 127;
21
+ }
22
+ var start = new ProcessStartInfo(exe, ArgumentsOf(Environment.CommandLine)) { UseShellExecute = false };
23
+ string conf = Path.Combine(mingw, "etc\\ssl\\openssl.cnf");
24
+ if (string.IsNullOrEmpty(Environment.GetEnvironmentVariable("OPENSSL_CONF")) && File.Exists(conf))
25
+ start.EnvironmentVariables["OPENSSL_CONF"] = conf;
26
+ using (Process p = Process.Start(start))
27
+ {
28
+ p.WaitForExit();
29
+ return p.ExitCode;
30
+ }
31
+ }
32
+
33
+ // Everything after the program name, untouched, so quoting survives exactly as the caller wrote it.
34
+ static string ArgumentsOf(string commandLine)
35
+ {
36
+ string s = commandLine.TrimStart();
37
+ int end;
38
+ if (s.StartsWith("\"")) { end = s.IndexOf('"', 1); end = end < 0 ? s.Length : end + 1; }
39
+ else { end = s.IndexOf(' '); if (end < 0) end = s.Length; }
40
+ return s.Substring(end).TrimStart();
41
+ }
42
+ }
@@ -181,6 +181,8 @@ sealed class Client
181
181
  public readonly NetworkStream Stream;
182
182
  public readonly BlockingCollection<byte[]> Outbox = new BlockingCollection<byte[]>(4096);
183
183
  public readonly bool Remote;
184
+ /** Set by the writer thread once everything queued has been written and the connection is closed. */
185
+ public readonly ManualResetEvent Finished = new ManualResetEvent(false);
184
186
  public bool ControlOnly, Query;
185
187
  public short Cols, Rows;
186
188
  long lastSeen = DateTime.UtcNow.Ticks;
@@ -203,7 +205,11 @@ sealed class Client
203
205
  {
204
206
  try { foreach (byte[] frame in Outbox.GetConsumingEnumerable()) Stream.Write(frame, 0, frame.Length); }
205
207
  catch (Exception) { }
206
- finally { try { Tcp.Close(); } catch (Exception) { } }
208
+ finally
209
+ {
210
+ try { Tcp.Close(); } catch (Exception) { }
211
+ Finished.Set();
212
+ }
207
213
  }
208
214
  }
209
215
 
@@ -323,7 +329,7 @@ sealed class ClientHub
323
329
  var everyone = Snapshot();
324
330
  foreach (Client c in everyone) { Send(c, Frame.Exit, code); Retire(c); }
325
331
  // Give the writer threads a moment to deliver the EXIT frame before the process ends.
326
- foreach (Client c in everyone) for (int i = 0; i < 20 && c.Outbox.Count > 0; i++) Thread.Sleep(50);
332
+ foreach (Client c in everyone) c.Finished.WaitOne(1000);
327
333
  }
328
334
 
329
335
  /** Stops sending to a client after what is already queued (its writer closes the connection when done). */
@@ -343,11 +349,16 @@ sealed class ClientHub
343
349
  if (next != null) TakeOwnership(next);
344
350
  }
345
351
 
346
- /** Disconnects a client (after at most half a second for frames already queued, e.g. "access denied"). */
352
+ /**
353
+ * Disconnects a client. Frames already queued (e.g. "access denied", STATUS) are written first: the client's writer
354
+ * thread closes the connection when it is done, so wait for that rather than for an empty queue (the writer takes a
355
+ * frame off the queue before it has written it, so an empty queue does not mean "sent"), and only force the close
356
+ * if the peer is not reading.
357
+ */
347
358
  public void Kill(Client client)
348
359
  {
349
360
  Retire(client);
350
- for (int i = 0; i < 20 && client.Outbox.Count > 0; i++) Thread.Sleep(25);
361
+ client.Finished.WaitOne(3000);
351
362
  try { client.Tcp.Close(); } catch (Exception) { }
352
363
  }
353
364
 
package/sandbox/trust.cmd CHANGED
@@ -1,35 +1,35 @@
1
- @echo off
2
- rem Runs first at logon, INSIDE the disposable sandbox only (the host PC is never touched).
3
- rem Lets the sandbox's own tools run without "Open File - Security Warning", SmartScreen, script-policy or UAC prompts.
4
- rem The security boundary is Windows Sandbox itself plus workspace-only write access and the LAN block; these prompts add
5
- rem nothing inside a throwaway VM except interruptions.
6
- set "POL=HKCU\Software\Microsoft\Windows\CurrentVersion\Policies"
7
- reg add "%POL%\Associations" /v LowRiskFileTypes /t REG_SZ /d ".exe;.com;.cmd;.bat;.ps1;.psm1;.msi;.vbs;.js;.zip;.7z" /f >nul
8
- reg add "%POL%\Attachments" /v SaveZoneInformation /t REG_DWORD /d 1 /f >nul
9
- rem Mapped folders and UNC paths count as Local Intranet, where launching programs is allowed without asking.
10
- reg add "HKCU\Software\Microsoft\Windows\CurrentVersion\Internet Settings\ZoneMap" /v UNCAsIntranet /t REG_DWORD /d 1 /f >nul
11
- reg add "HKCU\Software\Microsoft\Windows\CurrentVersion\Internet Settings\ZoneMap" /v IntranetName /t REG_DWORD /d 1 /f >nul
12
- rem Files copied from this PC keep their "downloaded from the internet" mark, so launching is allowed in every zone
13
- rem (1806 = 0), for this user and machine-wide, and ShellExecute skips the zone check altogether.
14
- for %%Z in (0 1 2 3 4) do (
15
- reg add "HKCU\Software\Microsoft\Windows\CurrentVersion\Internet Settings\Zones\%%Z" /v 1806 /t REG_DWORD /d 0 /f >nul
16
- reg add "HKLM\SOFTWARE\Microsoft\Windows\CurrentVersion\Internet Settings\Zones\%%Z" /v 1806 /t REG_DWORD /d 0 /f >nul 2>&1
17
- )
18
- reg add "HKLM\SYSTEM\CurrentControlSet\Control\Session Manager\Environment" /v SEE_MASK_NOZONECHECKS /t REG_SZ /d 1 /f >nul 2>&1
19
- set "SEE_MASK_NOZONECHECKS=1"
20
- rem Paths over 260 characters (deep node_modules, Gradle and NuGet caches) work for every program.
21
- reg add "HKLM\SYSTEM\CurrentControlSet\Control\FileSystem" /v LongPathsEnabled /t REG_DWORD /d 1 /f >nul 2>&1
22
- rem "Windows Defender Firewall has blocked some features of this app" pops up whenever a program (node, a dev server,
23
- rem the session host) listens on the network. Inbound stays blocked, just silently; the attach rule lets this PC in.
24
- netsh advfirewall set allprofiles settings inboundusernotification disable >nul 2>&1
25
- for %%P in (DomainProfile PrivateProfile PublicProfile) do reg add "HKLM\SOFTWARE\Policies\Microsoft\WindowsFirewall\%%P" /v DisableNotifications /t REG_DWORD /d 1 /f >nul 2>&1
26
- rem SmartScreen ("Windows protected your PC") for downloaded, unsigned tools.
27
- reg add "HKLM\SOFTWARE\Policies\Microsoft\Windows\System" /v EnableSmartScreen /t REG_DWORD /d 0 /f >nul 2>&1
28
- reg add "HKLM\SOFTWARE\Microsoft\Windows\CurrentVersion\Explorer" /v SmartScreenEnabled /t REG_SZ /d Off /f >nul 2>&1
29
- rem PowerShell scripts (hooks, installers) run without the "Do you want to run ..." question.
30
- reg add "HKLM\SOFTWARE\Microsoft\PowerShell\1\ShellIds\Microsoft.PowerShell" /v ExecutionPolicy /t REG_SZ /d Bypass /f >nul 2>&1
31
- reg add "HKCU\SOFTWARE\Microsoft\PowerShell\1\ShellIds\Microsoft.PowerShell" /v ExecutionPolicy /t REG_SZ /d Bypass /f >nul
32
- rem Elevation requests from the (already admin) sandbox user are granted without a dialog.
33
- reg add "HKLM\SOFTWARE\Microsoft\Windows\CurrentVersion\Policies\System" /v ConsentPromptBehaviorAdmin /t REG_DWORD /d 0 /f >nul 2>&1
34
- reg add "HKLM\SOFTWARE\Microsoft\Windows\CurrentVersion\Policies\System" /v PromptOnSecureDesktop /t REG_DWORD /d 0 /f >nul 2>&1
35
- exit /b 0
1
+ @echo off
2
+ rem Runs first at logon, INSIDE the disposable sandbox only (the host PC is never touched).
3
+ rem Lets the sandbox's own tools run without "Open File - Security Warning", SmartScreen, script-policy or UAC prompts.
4
+ rem The security boundary is Windows Sandbox itself plus workspace-only write access and the LAN block; these prompts add
5
+ rem nothing inside a throwaway VM except interruptions.
6
+ set "POL=HKCU\Software\Microsoft\Windows\CurrentVersion\Policies"
7
+ reg add "%POL%\Associations" /v LowRiskFileTypes /t REG_SZ /d ".exe;.com;.cmd;.bat;.ps1;.psm1;.msi;.vbs;.js;.zip;.7z" /f >nul
8
+ reg add "%POL%\Attachments" /v SaveZoneInformation /t REG_DWORD /d 1 /f >nul
9
+ rem Mapped folders and UNC paths count as Local Intranet, where launching programs is allowed without asking.
10
+ reg add "HKCU\Software\Microsoft\Windows\CurrentVersion\Internet Settings\ZoneMap" /v UNCAsIntranet /t REG_DWORD /d 1 /f >nul
11
+ reg add "HKCU\Software\Microsoft\Windows\CurrentVersion\Internet Settings\ZoneMap" /v IntranetName /t REG_DWORD /d 1 /f >nul
12
+ rem Files copied from this PC keep their "downloaded from the internet" mark, so launching is allowed in every zone
13
+ rem (1806 = 0), for this user and machine-wide, and ShellExecute skips the zone check altogether.
14
+ for %%Z in (0 1 2 3 4) do (
15
+ reg add "HKCU\Software\Microsoft\Windows\CurrentVersion\Internet Settings\Zones\%%Z" /v 1806 /t REG_DWORD /d 0 /f >nul
16
+ reg add "HKLM\SOFTWARE\Microsoft\Windows\CurrentVersion\Internet Settings\Zones\%%Z" /v 1806 /t REG_DWORD /d 0 /f >nul 2>&1
17
+ )
18
+ reg add "HKLM\SYSTEM\CurrentControlSet\Control\Session Manager\Environment" /v SEE_MASK_NOZONECHECKS /t REG_SZ /d 1 /f >nul 2>&1
19
+ set "SEE_MASK_NOZONECHECKS=1"
20
+ rem Paths over 260 characters (deep node_modules, Gradle and NuGet caches) work for every program.
21
+ reg add "HKLM\SYSTEM\CurrentControlSet\Control\FileSystem" /v LongPathsEnabled /t REG_DWORD /d 1 /f >nul 2>&1
22
+ rem "Windows Defender Firewall has blocked some features of this app" pops up whenever a program (node, a dev server,
23
+ rem the session host) listens on the network. Inbound stays blocked, just silently; the attach rule lets this PC in.
24
+ netsh advfirewall set allprofiles settings inboundusernotification disable >nul 2>&1
25
+ for %%P in (DomainProfile PrivateProfile PublicProfile) do reg add "HKLM\SOFTWARE\Policies\Microsoft\WindowsFirewall\%%P" /v DisableNotifications /t REG_DWORD /d 1 /f >nul 2>&1
26
+ rem SmartScreen ("Windows protected your PC") for downloaded, unsigned tools.
27
+ reg add "HKLM\SOFTWARE\Policies\Microsoft\Windows\System" /v EnableSmartScreen /t REG_DWORD /d 0 /f >nul 2>&1
28
+ reg add "HKLM\SOFTWARE\Microsoft\Windows\CurrentVersion\Explorer" /v SmartScreenEnabled /t REG_SZ /d Off /f >nul 2>&1
29
+ rem PowerShell scripts (hooks, installers) run without the "Do you want to run ..." question.
30
+ reg add "HKLM\SOFTWARE\Microsoft\PowerShell\1\ShellIds\Microsoft.PowerShell" /v ExecutionPolicy /t REG_SZ /d Bypass /f >nul 2>&1
31
+ reg add "HKCU\SOFTWARE\Microsoft\PowerShell\1\ShellIds\Microsoft.PowerShell" /v ExecutionPolicy /t REG_SZ /d Bypass /f >nul
32
+ rem Elevation requests from the (already admin) sandbox user are granted without a dialog.
33
+ reg add "HKLM\SOFTWARE\Microsoft\Windows\CurrentVersion\Policies\System" /v ConsentPromptBehaviorAdmin /t REG_DWORD /d 0 /f >nul 2>&1
34
+ reg add "HKLM\SOFTWARE\Microsoft\Windows\CurrentVersion\Policies\System" /v PromptOnSecureDesktop /t REG_DWORD /d 0 /f >nul 2>&1
35
+ exit /b 0
@@ -82,6 +82,26 @@
82
82
  "maximum": 9007199254740991,
83
83
  "description": "Upper limit for memoryMb \"auto\"."
84
84
  },
85
+ "maxMemoryPercent": {
86
+ "type": "integer",
87
+ "minimum": 10,
88
+ "maximum": 100,
89
+ "description": "memoryMb \"auto\" never takes more than this share of this PC's total RAM (percent)."
90
+ },
91
+ "cpuLimit": {
92
+ "anyOf": [
93
+ {
94
+ "type": "string",
95
+ "const": "none"
96
+ },
97
+ {
98
+ "type": "integer",
99
+ "minimum": 5,
100
+ "maximum": 100
101
+ }
102
+ ],
103
+ "description": "Most of this PC's CPU the sandbox may use, in percent (5-100), e.g. 50 keeps half free for you; \"none\" = no cap (Windows Sandbox would take every processor). Covers everything this tool starts in the sandbox (agents, installs, builds, its terminal)."
104
+ },
85
105
  "reserveHostMb": {
86
106
  "type": "integer",
87
107
  "minimum": 0,
@@ -200,6 +220,16 @@
200
220
  },
201
221
  "additionalProperties": false
202
222
  },
223
+ "downloads": {
224
+ "type": "object",
225
+ "properties": {
226
+ "requireChecksum": {
227
+ "type": "boolean",
228
+ "description": "Refuse any toolchain, database or tool download that has no published checksum (SHA-256/SHA-512) to verify it against. false = allow those, as before. Downloads that do have a published checksum are always verified. A project's .sandbox-config.json can only turn this on."
229
+ }
230
+ },
231
+ "additionalProperties": false
232
+ },
203
233
  "toolchains": {
204
234
  "type": "object",
205
235
  "properties": {
@@ -874,7 +904,7 @@
874
904
  "const": "auto"
875
905
  }
876
906
  ],
877
- "description": "SQL Server. It cannot run inside Windows Sandbox; when a project needs it you get a hint to reach the one on this PC through hostAccess.forwards: true, false, or \"auto\" = only when the project needs it (found from compose files, dependencies of every language, Prisma/ORM config and connection strings)."
907
+ "description": "SQL Server. It cannot run inside Windows Sandbox; when a project needs it you get a note to use Postgres or SQLite for local work: true, false, or \"auto\" = only when the project needs it (found from compose files, dependencies of every language, Prisma/ORM config and connection strings)."
878
908
  }
879
909
  },
880
910
  "additionalProperties": false
@@ -905,6 +935,32 @@
905
935
  },
906
936
  "additionalProperties": false
907
937
  },
938
+ "agentCommands": {
939
+ "type": "object",
940
+ "properties": {
941
+ "enabled": {
942
+ "type": "boolean",
943
+ "description": "(Beta) Typing an agent's own command on this PC (claude, codex, ...) starts it in a hidden sandbox for the folder you are in and attaches that terminal. Adds one folder to the start of YOUR PATH; off removes it. SANDBOX_AGENT_DIRECT=1 runs the real agent instead."
944
+ },
945
+ "agents": {
946
+ "anyOf": [
947
+ {
948
+ "type": "string",
949
+ "const": "auto"
950
+ },
951
+ {
952
+ "type": "array",
953
+ "items": {
954
+ "type": "string",
955
+ "pattern": "^[A-Za-z0-9_-]+$"
956
+ }
957
+ }
958
+ ],
959
+ "description": "\"auto\" = every agent installed on this PC, or a list of agent names, e.g. [\"claude\", \"codex\"]."
960
+ }
961
+ },
962
+ "additionalProperties": false
963
+ },
908
964
  "secrets": {
909
965
  "type": "object",
910
966
  "properties": {
@@ -1056,6 +1112,49 @@
1056
1112
  }
1057
1113
  },
1058
1114
  "additionalProperties": false
1115
+ },
1116
+ "projects": {
1117
+ "type": "object",
1118
+ "properties": {
1119
+ "all": {
1120
+ "type": "boolean",
1121
+ "description": "Map every project you have started before (and every folder in your roots) at every start, except those in exclude, so all of them open in the running sandbox without a restart. Every agent in the sandbox can then see all of them. Each one is still only set up when you first use it."
1122
+ },
1123
+ "exclude": {
1124
+ "type": "array",
1125
+ "items": {
1126
+ "type": "string",
1127
+ "minLength": 1
1128
+ },
1129
+ "description": "Projects left out when \"all\" is on (folders on this PC)."
1130
+ },
1131
+ "roots": {
1132
+ "type": "array",
1133
+ "items": {
1134
+ "type": "string",
1135
+ "minLength": 1
1136
+ },
1137
+ "description": "Folders whose subfolders are your projects (e.g. \"F:\\\\Code\"), mapped read/write at every start. Any project inside one, including ones you create later, opens in the running sandbox at once, without a restart. Every agent in the sandbox can see all of them."
1138
+ },
1139
+ "askToAddRoot": {
1140
+ "type": "boolean",
1141
+ "description": "When you start a project whose parent folder is not a root yet, ask once whether to add it."
1142
+ },
1143
+ "declinedRoots": {
1144
+ "type": "array",
1145
+ "items": {
1146
+ "type": "string",
1147
+ "minLength": 1
1148
+ },
1149
+ "description": "Parent folders you said no to (not asked again)."
1150
+ },
1151
+ "wsbShare": {
1152
+ "type": "boolean",
1153
+ "description": "On Windows 11 24H2+ with the Windows Sandbox command line (wsb), add other folders to the running sandbox with `wsb share` (untested). Otherwise starting a project outside every root offers a restart."
1154
+ }
1155
+ },
1156
+ "additionalProperties": false,
1157
+ "description": "Several projects in one sandbox: folders of projects mapped at every start, and adding folders while it runs."
1059
1158
  }
1060
1159
  },
1061
1160
  "additionalProperties": false
@@ -94,6 +94,12 @@
94
94
  "maximum": 9007199254740991,
95
95
  "description": "Upper limit for memoryMb \"auto\"."
96
96
  },
97
+ "maxMemoryPercent": {
98
+ "type": "integer",
99
+ "minimum": 10,
100
+ "maximum": 100,
101
+ "description": "memoryMb \"auto\" never takes more than this share of this PC's total RAM (percent)."
102
+ },
97
103
  "reserveHostMb": {
98
104
  "type": "integer",
99
105
  "minimum": 0,
@@ -191,6 +197,16 @@
191
197
  },
192
198
  "additionalProperties": false
193
199
  },
200
+ "downloads": {
201
+ "type": "object",
202
+ "properties": {
203
+ "requireChecksum": {
204
+ "type": "boolean",
205
+ "description": "Refuse any toolchain, database or tool download that has no published checksum (SHA-256/SHA-512) to verify it against. false = allow those, as before. Downloads that do have a published checksum are always verified. A project's .sandbox-config.json can only turn this on."
206
+ }
207
+ },
208
+ "additionalProperties": false
209
+ },
194
210
  "toolchains": {
195
211
  "type": "object",
196
212
  "properties": {
@@ -864,7 +880,7 @@
864
880
  "const": "auto"
865
881
  }
866
882
  ],
867
- "description": "SQL Server. It cannot run inside Windows Sandbox; when a project needs it you get a hint to reach the one on this PC through hostAccess.forwards: true, false, or \"auto\" = only when the project needs it (found from compose files, dependencies of every language, Prisma/ORM config and connection strings)."
883
+ "description": "SQL Server. It cannot run inside Windows Sandbox; when a project needs it you get a note to use Postgres or SQLite for local work: true, false, or \"auto\" = only when the project needs it (found from compose files, dependencies of every language, Prisma/ORM config and connection strings)."
868
884
  }
869
885
  },
870
886
  "additionalProperties": false
@@ -1,46 +0,0 @@
1
- import { type Context } from './context.js';
2
- export type DataArea = 'tools' | 'databases' | 'state' | 'logs' | 'data' | 'host';
3
- export interface DataEntry {
4
- area: DataArea;
5
- name: string;
6
- path: string;
7
- bytes: number;
8
- }
9
- /** 1536 -> "1.5 KB". */
10
- export declare const humanBytes: (bytes: number) => string;
11
- /** Size on disk, never following links or junctions (a link counts as 0). */
12
- export declare const sizeOf: (p: string) => number;
13
- /**
14
- * Deletes a folder or file without ever following a link: links found anywhere inside are removed themselves first,
15
- * then the (now link-free) tree is deleted. A link in place of `p` is removed itself.
16
- */
17
- /** Deletes p (strictly inside root): links inside are removed first and never followed; see shared/safe-remove.ts. */
18
- export declare const removeTree: (p: string, root: string) => void;
19
- /** Every row of `sandbox data`: the sandbox's data folder by area, then what this PC keeps for it. */
20
- export declare const collectDataEntries: (dataPath: string, storageRoot: string) => DataEntry[];
21
- export interface DiskUsage {
22
- drive: string;
23
- totalBytes: number;
24
- freeBytes: number;
25
- usedBytes: number;
26
- holdsStorage: boolean;
27
- }
28
- /** Every local drive's size and free space, and which one holds the sandbox storage. */
29
- export declare const diskUsage: (storageRoot: string) => DiskUsage[];
30
- export declare const dataInfo: (ctx: Context) => number;
31
- export interface CleanTarget {
32
- /** The target as normalised (forward slashes), e.g. "tools/go". */
33
- target: string;
34
- path: string;
35
- /** Folder the target must stay inside, with no links between the two. */
36
- root: string;
37
- warning?: string;
38
- }
39
- /**
40
- * Turns `sandbox data clean` arguments into folders. Pure (no disk access); throws a UserError for anything that is
41
- * not a plain relative target inside the data folder: absolute paths, drive letters, `..`, wildcards, unknown areas.
42
- */
43
- export declare const resolveCleanTargets: (inputs: string[], dataPath: string, storageRoot: string) => CleanTarget[];
44
- export declare const dataClean: (ctx: Context) => Promise<number>;
45
- /** `sandbox data [info|clean] [targets...]`. */
46
- export declare const data: (ctx: Context) => number | Promise<number>;
@@ -1,249 +0,0 @@
1
- // `sandbox data`: what persists between sandboxes (toolchains, databases, agent state, logs, host storage) and
2
- // removing chosen parts of it. Everything under the data folder is sandbox-writable: sizes and deletes never follow
3
- // links, and targets resolve strictly inside the data folder.
4
- import fs from 'node:fs';
5
- import path from 'node:path';
6
- import readline from 'node:readline';
7
- import _ from 'lodash';
8
- import { removeRefusal, safeRemove } from '../../shared/safe-remove.js';
9
- import { assertNoLinks, dirExists, fail, isInsideOrEqual, isLink, removeLink } from '../../core/util.js';
10
- import { askYesNo } from '../../core/workspace.js';
11
- import { isSandboxRunning } from '../../host/platform.js';
12
- import { printJson, table } from './context.js';
13
- const AREA_ORDER = ['tools', 'databases', 'state', 'logs', 'data', 'host'];
14
- /** Folders of the data folder that get one row per child; anything else at the top level is listed as itself. */
15
- const PER_CHILD = ['tools', 'databases', 'state'];
16
- /** 1536 -> "1.5 KB". */
17
- export const humanBytes = (bytes) => {
18
- const units = ['B', 'KB', 'MB', 'GB', 'TB'];
19
- const i = bytes <= 0 ? 0 : Math.min(units.length - 1, Math.floor(Math.log(bytes) / Math.log(1024)));
20
- const value = bytes / 1024 ** i;
21
- return (i === 0 ? String(bytes) : value.toFixed(value < 10 ? 1 : 0)) + ' ' + units[i];
22
- };
23
- /** Size on disk, never following links or junctions (a link counts as 0). */
24
- export const sizeOf = (p) => {
25
- try {
26
- const stat = fs.lstatSync(p);
27
- if (stat.isSymbolicLink())
28
- return 0;
29
- if (!stat.isDirectory())
30
- return stat.size;
31
- return _.sumBy(fs.readdirSync(p, { withFileTypes: true }), (d) => (d.isSymbolicLink() ? 0 : sizeOf(path.join(p, d.name))));
32
- }
33
- catch {
34
- return 0;
35
- }
36
- };
37
- /**
38
- * Deletes a folder or file without ever following a link: links found anywhere inside are removed themselves first,
39
- * then the (now link-free) tree is deleted. A link in place of `p` is removed itself.
40
- */
41
- /** Deletes p (strictly inside root): links inside are removed first and never followed; see shared/safe-remove.ts. */
42
- export const removeTree = (p, root) => {
43
- const unlinkInside = (dir) => {
44
- for (const d of fs.readdirSync(dir, { withFileTypes: true })) {
45
- const child = path.join(dir, d.name);
46
- if (d.isSymbolicLink() || isLink(child))
47
- removeLink(child);
48
- else if (d.isDirectory())
49
- unlinkInside(child);
50
- }
51
- };
52
- if (isLink(p)) {
53
- removeLink(p);
54
- return;
55
- }
56
- if (!fs.existsSync(p))
57
- return;
58
- // Refused before anything is touched (safeRemove below checks again).
59
- const refusal = removeRefusal(p, root);
60
- if (refusal)
61
- throw new Error('Refusing to delete ' + p + ': ' + refusal);
62
- if (dirExists(p))
63
- unlinkInside(p);
64
- safeRemove(p, root);
65
- };
66
- /** Real (non-link) children of a real folder; a link in place of the folder yields nothing. */
67
- const entriesOf = (dir) => (dirExists(dir) && !isLink(dir) ? fs.readdirSync(dir, { withFileTypes: true }).filter((d) => !d.isSymbolicLink()) : []);
68
- const toolRows = (toolsDir) => {
69
- const entries = entriesOf(toolsDir);
70
- const folders = entries.filter((d) => d.isDirectory());
71
- const files = entries.filter((d) => !d.isDirectory());
72
- const row = (name, p) => ({ area: 'tools', name, path: p, bytes: sizeOf(p) });
73
- return [
74
- ...folders.flatMap((d) => (d.name.toLowerCase() === 'agents'
75
- ? entriesOf(path.join(toolsDir, d.name)).map((a) => row('agents/' + a.name, path.join(toolsDir, d.name, a.name)))
76
- : [row(d.name, path.join(toolsDir, d.name))])),
77
- ...(files.length ? [{ area: 'tools', name: '(files in tools)', path: toolsDir, bytes: _.sumBy(files, (f) => sizeOf(path.join(toolsDir, f.name))) }] : []),
78
- ];
79
- };
80
- /** Every row of `sandbox data`: the sandbox's data folder by area, then what this PC keeps for it. */
81
- export const collectDataEntries = (dataPath, storageRoot) => {
82
- const inData = entriesOf(dataPath);
83
- const rows = inData.flatMap((d) => {
84
- const p = path.join(dataPath, d.name);
85
- const area = d.name.toLowerCase();
86
- if (area === 'tools')
87
- return toolRows(p);
88
- if (PER_CHILD.includes(area))
89
- return entriesOf(p).map((c) => ({ area, name: area + '/' + c.name, path: path.join(p, c.name), bytes: sizeOf(path.join(p, c.name)) }));
90
- return [{ area: area === 'logs' ? 'logs' : 'data', name: d.name, path: p, bytes: sizeOf(p) }];
91
- });
92
- const otherData = entriesOf(path.join(storageRoot, 'data')).filter((d) => d.isDirectory() && path.join(storageRoot, 'data', d.name).toLowerCase() !== path.resolve(dataPath).toLowerCase());
93
- const host = [
94
- ...['launches', 'accounts', 'sync', 'logs', 'run', 'host'].map((name) => ({ area: 'host', name, path: path.join(storageRoot, name), bytes: sizeOf(path.join(storageRoot, name)) })),
95
- ...otherData.map((d) => ({ area: 'host', name: 'other data folder: ' + d.name, path: path.join(storageRoot, 'data', d.name), bytes: sizeOf(path.join(storageRoot, 'data', d.name)) })),
96
- ].filter((r) => fs.existsSync(r.path));
97
- return _.sortBy([...rows, ...host], (r) => AREA_ORDER.indexOf(r.area), (r) => -r.bytes, (r) => r.name.toLowerCase());
98
- };
99
- const AREA_LABEL = { tools: 'Toolchains', databases: 'Databases', state: 'Agent state', logs: 'Logs', data: 'Other', host: 'On this PC' };
100
- /** Every local drive's size and free space, and which one holds the sandbox storage. */
101
- export const diskUsage = (storageRoot) => {
102
- const storageDrive = path.parse(path.resolve(storageRoot)).root.toUpperCase();
103
- return 'CDEFGHIJKLMNOPQRSTUVWXYZ'.split('').map((letter) => letter + ':\\').flatMap((drive) => {
104
- try {
105
- const s = fs.statfsSync(drive);
106
- const totalBytes = s.blocks * s.bsize;
107
- if (!totalBytes)
108
- return [];
109
- return [{ drive, totalBytes, freeBytes: s.bavail * s.bsize, usedBytes: totalBytes - s.bfree * s.bsize, holdsStorage: drive.toUpperCase() === storageDrive }];
110
- }
111
- catch {
112
- return [];
113
- }
114
- });
115
- };
116
- export const dataInfo = (ctx) => {
117
- const entries = collectDataEntries(ctx.dataPath, ctx.storageRoot);
118
- const total = _.sumBy(entries, 'bytes');
119
- const disks = diskUsage(ctx.storageRoot);
120
- if (printJson(ctx, { dataPath: ctx.dataPath, storageRoot: ctx.storageRoot, totalBytes: total, entries, disks }))
121
- return 0;
122
- console.log('Disks\n');
123
- table([['DRIVE', 'USED', 'FREE', 'SIZE', ''], ...disks.map((d) => [d.drive, humanBytes(d.usedBytes) + ' (' + Math.round((d.usedBytes / d.totalBytes) * 100) + '%)', humanBytes(d.freeBytes), humanBytes(d.totalBytes), d.holdsStorage ? '<- sandbox storage (' + humanBytes(total) + ' of it)' : ''])]);
124
- console.log('');
125
- console.log('Persistent data for the sandbox: ' + ctx.dataPath + ' (C:\\AgentData inside)\n');
126
- if (!entries.length) {
127
- console.log('Nothing stored yet. Toolchains, databases and agent logins appear here after the first start.');
128
- return 0;
129
- }
130
- const rows = _.flatMap(_.groupBy(entries, 'area'), (group, area) => [
131
- [AREA_LABEL[area], humanBytes(_.sumBy(group, 'bytes')), ''],
132
- ...group.map((e) => [' ' + e.name, humanBytes(e.bytes), e.path]),
133
- ]);
134
- table([...rows, ['', '', ''], ['Total', humanBytes(total), '']]);
135
- console.log('\nFree space with: sandbox data clean <target...> (e.g. tools/go, databases/postgres-18, logs, launches)');
136
- return 0;
137
- };
138
- const DATA_AREAS = ['tools', 'databases', 'state', 'logs'];
139
- const SEGMENT = /^[A-Za-z0-9][A-Za-z0-9._ +@-]*$/;
140
- const LOGIN_WARNING = 'removes the agent logins, settings and history inside the sandbox (save a Claude login first with: sandbox account save <name>)';
141
- const ALL_WARNING = 'removes EVERYTHING the sandbox keeps: toolchains, databases, agent logins and history, logs and launch folders';
142
- /**
143
- * Turns `sandbox data clean` arguments into folders. Pure (no disk access); throws a UserError for anything that is
144
- * not a plain relative target inside the data folder: absolute paths, drive letters, `..`, wildcards, unknown areas.
145
- */
146
- export const resolveCleanTargets = (inputs, dataPath, storageRoot) => {
147
- const usage = 'Targets: all | tools | tools/<name> | tools/agents/<name> | databases | databases/<name> | state | state/<agent> | logs | launches. See them with: sandbox data';
148
- if (!inputs.length)
149
- fail('Say what to remove.', usage);
150
- const resolved = inputs.map((raw) => {
151
- const input = raw.trim();
152
- if (!input)
153
- fail('Empty target.', usage);
154
- if (path.win32.isAbsolute(input) || path.posix.isAbsolute(input) || /^[A-Za-z]:/.test(input) || input.includes(':'))
155
- fail('Refusing "' + raw + '": targets are relative to the data folder, not paths on this PC.', usage);
156
- const parts = input.replace(/\\/g, '/').split('/').filter(Boolean);
157
- const bad = parts.find((p) => !SEGMENT.test(p) || /^\.+$/.test(p) || /[. ]$/.test(p));
158
- if (bad !== undefined)
159
- fail('Refusing "' + raw + '": "' + bad + '" is not a plain folder name (no .., wildcards or special characters).', usage);
160
- const [head, ...rest] = parts;
161
- const area = head.toLowerCase();
162
- if (parts.length > 3)
163
- fail('Refusing "' + raw + '": too deep.', usage);
164
- const target = [area, ...rest].join('/');
165
- if (area === 'all' && !rest.length)
166
- return { target: 'all', path: dataPath, root: path.join(storageRoot, 'data'), warning: ALL_WARNING };
167
- if (area === 'launches')
168
- return { target, path: path.join(storageRoot, 'launches', ...rest), root: storageRoot };
169
- if (area === 'accounts' || area === 'sync')
170
- fail('"' + area + '" is kept on this PC on purpose and is not removed here.', area === 'accounts' ? 'Remove a saved login with: sandbox account remove <name>' : usage);
171
- if (!DATA_AREAS.includes(area))
172
- fail('Unknown target "' + raw + '".', usage);
173
- if (area === 'logs' && rest.length)
174
- fail('Remove all logs ("logs"), not single files.', usage);
175
- if (rest.length > (area === 'tools' && rest[0]?.toLowerCase() === 'agents' ? 2 : 1))
176
- fail('Refusing "' + raw + '": too deep.', usage);
177
- return { target, path: path.join(dataPath, area, ...rest), root: dataPath, ...(area === 'state' ? { warning: LOGIN_WARNING } : {}) };
178
- });
179
- // "all" covers everything; otherwise drop targets inside another target (tools + tools/go = tools).
180
- const all = resolved.find((t) => t.target === 'all');
181
- if (all)
182
- return [all, { target: 'launches', path: path.join(storageRoot, 'launches'), root: storageRoot }];
183
- const unique = _.uniqBy(resolved, (t) => t.path.toLowerCase());
184
- return unique.filter((t) => !unique.some((o) => o !== t && isInsideOrEqual(o.path, t.path)));
185
- };
186
- /** Asks the user to type `expected` (a stronger confirmation than y/N). */
187
- const askTyped = async (question, expected) => {
188
- if (!process.stdin.isTTY)
189
- return false;
190
- const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
191
- const answer = await new Promise((resolve) => rl.question(question + ' Type "' + expected + '" to confirm: ', resolve));
192
- rl.close();
193
- return answer.trim().toLowerCase() === expected;
194
- };
195
- export const dataClean = async (ctx) => {
196
- const targets = resolveCleanTargets(ctx.options.positionals.slice(1), ctx.dataPath, ctx.storageRoot);
197
- if (isSandboxRunning())
198
- fail('A Windows Sandbox is running, and it uses these folders.', 'Close it first (sandbox stop), then run this again.');
199
- // Every folder from the root down to the target's parent must be real; a link in place of the target is removed itself.
200
- for (const t of targets)
201
- assertNoLinks(t.root, path.dirname(t.path));
202
- const present = targets.filter((t) => fs.existsSync(t.path) || isLink(t.path));
203
- if (!present.length) {
204
- console.log('Nothing to remove: ' + targets.map((t) => t.target).join(', ') + ' do(es) not exist.');
205
- return 0;
206
- }
207
- const sized = present.map((t) => ({ ...t, bytes: sizeOf(t.path) }));
208
- const total = _.sumBy(sized, 'bytes');
209
- table(sized.map((t) => [' ' + t.target, humanBytes(t.bytes), t.path]));
210
- for (const t of sized)
211
- if (t.warning)
212
- console.log('Warning: ' + t.target + ' ' + t.warning + '.');
213
- const yes = Boolean(ctx.options.yes);
214
- if (!yes) {
215
- const ok = sized.some((t) => t.target === 'all')
216
- ? await askTyped('This cannot be undone; the next sandbox starts from scratch and reinstalls everything.', 'all')
217
- : await askYesNo('Remove ' + (sized.length === 1 ? 'this' : 'these ' + sized.length) + ' (' + humanBytes(total) + ')?');
218
- if (!ok) {
219
- console.log('Nothing removed.' + (process.stdin.isTTY ? '' : ' Pass --yes to confirm without a prompt.'));
220
- return 1;
221
- }
222
- }
223
- const failed = [];
224
- for (const t of sized) {
225
- try {
226
- removeTree(t.path, t.root);
227
- }
228
- catch (e) {
229
- failed.push(t.target + ' (' + (e instanceof Error ? e.message : String(e)) + ')');
230
- }
231
- }
232
- const freed = total - _.sumBy(sized, (t) => sizeOf(t.path));
233
- console.log('Freed ' + humanBytes(freed) + '. ' + (sized.some((t) => t.root === ctx.dataPath || t.target === 'all') ? 'The next sandbox reinstalls what it needs.' : ''));
234
- if (failed.length)
235
- fail('Could not remove everything: ' + failed.join('; '), 'Close programs using those folders and run the same command again.');
236
- return 0;
237
- };
238
- /** `sandbox data [info|clean] [targets...]`. */
239
- export const data = (ctx) => {
240
- const [action = 'info'] = ctx.options.positionals;
241
- switch (action) {
242
- case 'info':
243
- case 'show':
244
- case 'list': return dataInfo(ctx);
245
- case 'clean':
246
- case 'remove': return dataClean(ctx);
247
- default: return fail('Unknown data action "' + action + '".', 'Use: sandbox data (what is stored) or sandbox data clean <target...>');
248
- }
249
- };