windows-development-sandbox 1.0.5 → 1.4.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 (453) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +25 -8
  3. package/config.example.json +55 -5
  4. package/dist/api.js +2 -1
  5. package/dist/cli/args.d.ts +23 -0
  6. package/dist/cli/args.js +38 -4
  7. package/dist/cli/colors.d.ts +24 -0
  8. package/dist/cli/colors.js +42 -0
  9. package/dist/cli/commands/accounts.js +2 -2
  10. package/dist/cli/commands/agent-commands.d.ts +6 -0
  11. package/dist/cli/commands/agent-commands.js +61 -0
  12. package/dist/cli/commands/context.d.ts +2 -0
  13. package/dist/cli/commands/context.js +5 -1
  14. package/dist/cli/commands/data/clean.d.ts +15 -0
  15. package/dist/cli/commands/data/clean.js +106 -0
  16. package/dist/cli/commands/data/collect.d.ts +11 -0
  17. package/dist/cli/commands/data/collect.js +54 -0
  18. package/dist/cli/commands/data/index.d.ts +4 -0
  19. package/dist/cli/commands/data/index.js +45 -0
  20. package/dist/cli/commands/data/remove.d.ts +2 -0
  21. package/dist/cli/commands/data/remove.js +30 -0
  22. package/dist/cli/commands/index.d.ts +6 -4
  23. package/dist/cli/commands/index.js +6 -4
  24. package/dist/cli/commands/launch.d.ts +26 -2
  25. package/dist/cli/commands/launch.js +256 -42
  26. package/dist/cli/commands/maintenance.d.ts +2 -0
  27. package/dist/cli/commands/maintenance.js +67 -5
  28. package/dist/cli/commands/project.d.ts +29 -0
  29. package/dist/cli/commands/project.js +63 -0
  30. package/dist/cli/commands/projects.d.ts +14 -0
  31. package/dist/cli/commands/projects.js +146 -0
  32. package/dist/cli/commands/repair/checks.d.ts +2 -0
  33. package/dist/cli/commands/{repair.js → repair/checks.js} +19 -181
  34. package/dist/cli/commands/{repair.d.ts → repair/finders.d.ts} +0 -29
  35. package/dist/cli/commands/repair/finders.js +108 -0
  36. package/dist/cli/commands/repair/index.d.ts +12 -0
  37. package/dist/cli/commands/repair/index.js +61 -0
  38. package/dist/cli/commands/repair/types.d.ts +26 -0
  39. package/dist/cli/commands/repair/types.js +3 -0
  40. package/dist/cli/commands/restart.d.ts +6 -0
  41. package/dist/cli/commands/restart.js +85 -0
  42. package/dist/cli/commands/sessions.d.ts +2 -1
  43. package/dist/cli/commands/sessions.js +189 -45
  44. package/dist/cli/commands/setup/checks.d.ts +13 -0
  45. package/dist/cli/commands/setup/checks.js +71 -0
  46. package/dist/cli/commands/{setup.d.ts → setup/facts.d.ts} +1 -15
  47. package/dist/cli/commands/setup/facts.js +60 -0
  48. package/dist/cli/commands/setup/index.d.ts +2 -0
  49. package/dist/cli/commands/setup/index.js +43 -0
  50. package/dist/cli/commands/user-config.d.ts +8 -0
  51. package/dist/cli/commands/user-config.js +41 -0
  52. package/dist/cli/history.d.ts +11 -0
  53. package/dist/cli/history.js +21 -0
  54. package/dist/cli/index.d.ts +7 -1
  55. package/dist/cli/index.js +42 -15
  56. package/dist/cli/manifest.d.ts +8 -0
  57. package/dist/cli/manifest.js +40 -15
  58. package/dist/cli/menu/apps.d.ts +21 -0
  59. package/dist/cli/menu/apps.js +122 -0
  60. package/dist/cli/menu/command-form.d.ts +28 -0
  61. package/dist/cli/menu/command-form.js +219 -0
  62. package/dist/cli/menu/index.d.ts +26 -0
  63. package/dist/cli/menu/index.js +180 -0
  64. package/dist/cli/menu/settings/categories.d.ts +9 -0
  65. package/dist/cli/menu/settings/categories.js +28 -0
  66. package/dist/cli/menu/settings/config-file.d.ts +40 -0
  67. package/dist/cli/menu/settings/config-file.js +71 -0
  68. package/dist/cli/menu/settings/editors.d.ts +30 -0
  69. package/dist/cli/menu/settings/editors.js +305 -0
  70. package/dist/cli/menu/settings/index.d.ts +32 -0
  71. package/dist/cli/menu/settings/index.js +136 -0
  72. package/dist/cli/menu/settings/schema-node.d.ts +62 -0
  73. package/dist/cli/menu/settings/schema-node.js +104 -0
  74. package/dist/cli/menu/several.d.ts +20 -0
  75. package/dist/cli/menu/several.js +102 -0
  76. package/dist/cli/progress.d.ts +9 -0
  77. package/dist/cli/progress.js +49 -0
  78. package/dist/cli/tui/app.d.ts +39 -0
  79. package/dist/cli/tui/app.js +52 -0
  80. package/dist/cli/tui/dialogs.d.ts +57 -0
  81. package/dist/cli/tui/dialogs.js +162 -0
  82. package/dist/cli/tui/frame.d.ts +18 -0
  83. package/dist/cli/tui/frame.js +27 -0
  84. package/dist/cli/tui/index.d.ts +7 -0
  85. package/dist/cli/tui/index.js +8 -0
  86. package/dist/cli/tui/list-screen.d.ts +101 -0
  87. package/dist/cli/tui/list-screen.js +299 -0
  88. package/dist/cli/tui/terminal.d.ts +37 -0
  89. package/dist/cli/tui/terminal.js +89 -0
  90. package/dist/cli/tui/text.d.ts +20 -0
  91. package/dist/cli/tui/text.js +105 -0
  92. package/dist/cli/tui/theme.d.ts +45 -0
  93. package/dist/cli/tui/theme.js +30 -0
  94. package/dist/cli/validate.d.ts +17 -0
  95. package/dist/cli/validate.js +89 -0
  96. package/dist/core/config.js +12 -6
  97. package/dist/core/disk.d.ts +16 -0
  98. package/dist/core/disk.js +55 -0
  99. package/dist/core/local-config.d.ts +23 -0
  100. package/dist/core/local-config.js +79 -7
  101. package/dist/core/project-policy.d.ts +15 -0
  102. package/dist/core/project-policy.js +35 -0
  103. package/dist/core/prompt.d.ts +4 -0
  104. package/dist/core/prompt.js +15 -0
  105. package/dist/core/safe-paths.d.ts +4 -0
  106. package/dist/core/safe-paths.js +37 -0
  107. package/dist/core/schema/apps.d.ts +40 -0
  108. package/dist/core/schema/apps.js +27 -0
  109. package/dist/core/schema/common.d.ts +17 -0
  110. package/dist/core/schema/common.js +15 -0
  111. package/dist/core/schema/config.d.ts +659 -0
  112. package/dist/core/schema/config.js +75 -0
  113. package/dist/core/schema/databases.d.ts +33 -0
  114. package/dist/core/schema/databases.js +35 -0
  115. package/dist/core/schema/harness.d.ts +12 -0
  116. package/dist/core/schema/harness.js +17 -0
  117. package/dist/core/schema/json-schema.d.ts +5 -0
  118. package/dist/core/schema/json-schema.js +25 -0
  119. package/dist/core/schema/profile.d.ts +32 -0
  120. package/dist/core/schema/profile.js +31 -0
  121. package/dist/core/schema/projects.d.ts +9 -0
  122. package/dist/core/schema/projects.js +11 -0
  123. package/dist/core/schema/sandbox.d.ts +78 -0
  124. package/dist/core/schema/sandbox.js +64 -0
  125. package/dist/core/schema/toolchains.d.ts +96 -0
  126. package/dist/core/schema/toolchains.js +88 -0
  127. package/dist/core/schema.d.ts +6 -604
  128. package/dist/core/schema.js +8 -264
  129. package/dist/core/types.d.ts +35 -1
  130. package/dist/core/util.d.ts +9 -0
  131. package/dist/core/util.js +14 -3
  132. package/dist/core/workspace.d.ts +0 -5
  133. package/dist/core/workspace.js +33 -50
  134. package/dist/guest/apps/index.d.ts +10 -0
  135. package/dist/guest/apps/index.js +206 -0
  136. package/dist/guest/apps/resolvers.d.ts +8 -0
  137. package/dist/guest/apps/resolvers.js +90 -0
  138. package/dist/guest/audit-parse.d.ts +7 -0
  139. package/dist/guest/audit-parse.js +24 -0
  140. package/dist/guest/audit.d.ts +4 -0
  141. package/dist/guest/audit.js +47 -0
  142. package/dist/guest/boot-env.d.ts +4 -0
  143. package/dist/guest/boot-env.js +25 -0
  144. package/dist/guest/cmdline.d.ts +12 -0
  145. package/dist/guest/cmdline.js +23 -0
  146. package/dist/guest/daemon/exec.d.ts +5 -0
  147. package/dist/guest/daemon/exec.js +32 -0
  148. package/dist/guest/daemon/project.d.ts +14 -0
  149. package/dist/guest/daemon/project.js +132 -0
  150. package/dist/guest/daemon/relays.d.ts +1 -0
  151. package/dist/guest/daemon/relays.js +18 -0
  152. package/dist/guest/daemon/session.d.ts +13 -0
  153. package/dist/guest/daemon/session.js +139 -0
  154. package/dist/guest/daemon/shutdown.d.ts +1 -0
  155. package/dist/guest/daemon/shutdown.js +21 -0
  156. package/dist/guest/daemon/state.d.ts +17 -0
  157. package/dist/guest/daemon/state.js +31 -0
  158. package/dist/guest/daemon.js +12 -141
  159. package/dist/guest/db.js +18 -3
  160. package/dist/guest/env.d.ts +3 -0
  161. package/dist/guest/env.js +7 -3
  162. package/dist/guest/guide.d.ts +6 -2
  163. package/dist/guest/guide.js +5 -4
  164. package/dist/guest/helpers.d.ts +3 -0
  165. package/dist/guest/helpers.js +26 -0
  166. package/dist/guest/main.js +31 -8
  167. package/dist/guest/netproxy.d.ts +1 -0
  168. package/dist/guest/netproxy.js +100 -0
  169. package/dist/guest/ports.d.ts +6 -0
  170. package/dist/guest/ports.js +22 -0
  171. package/dist/guest/project-context.d.ts +7 -0
  172. package/dist/guest/project-context.js +10 -0
  173. package/dist/guest/project-env.d.ts +20 -0
  174. package/dist/guest/project-env.js +20 -0
  175. package/dist/guest/project-status.d.ts +5 -0
  176. package/dist/guest/project-status.js +17 -0
  177. package/dist/guest/ptyhost.d.ts +0 -18
  178. package/dist/guest/ptyhost.js +17 -65
  179. package/dist/guest/setup/agents.d.ts +5 -2
  180. package/dist/guest/setup/agents.js +22 -8
  181. package/dist/guest/setup/claude.d.ts +0 -4
  182. package/dist/guest/setup/claude.js +59 -5
  183. package/dist/guest/setup/environment.d.ts +13 -3
  184. package/dist/guest/setup/environment.js +14 -10
  185. package/dist/guest/setup/hooks.d.ts +9 -1
  186. package/dist/guest/setup/hooks.js +28 -14
  187. package/dist/guest/setup/network.d.ts +9 -0
  188. package/dist/guest/setup/network.js +97 -4
  189. package/dist/guest/setup/user.js +4 -0
  190. package/dist/guest/toolchains/catalogue.d.ts +4 -0
  191. package/dist/guest/toolchains/catalogue.js +2 -2
  192. package/dist/guest/toolchains/cli.js +25 -17
  193. package/dist/guest/toolchains/common.d.ts +18 -1
  194. package/dist/guest/toolchains/common.js +34 -11
  195. package/dist/guest/toolchains/databases/dynamodb.d.ts +8 -0
  196. package/dist/guest/toolchains/databases/dynamodb.js +52 -0
  197. package/dist/guest/toolchains/{databases.d.ts → databases/index.d.ts} +2 -3
  198. package/dist/guest/toolchains/databases/index.js +62 -0
  199. package/dist/guest/toolchains/databases/mongodb.d.ts +3 -0
  200. package/dist/guest/toolchains/databases/mongodb.js +31 -0
  201. package/dist/guest/toolchains/databases/mysql.d.ts +3 -0
  202. package/dist/guest/toolchains/databases/mysql.js +38 -0
  203. package/dist/guest/toolchains/databases/postgres.d.ts +5 -0
  204. package/dist/guest/toolchains/databases/postgres.js +38 -0
  205. package/dist/guest/toolchains/databases/redis.d.ts +3 -0
  206. package/dist/guest/toolchains/databases/redis.js +16 -0
  207. package/dist/guest/toolchains/databases/server.d.ts +14 -0
  208. package/dist/guest/toolchains/databases/server.js +52 -0
  209. package/dist/guest/toolchains/git.js +25 -16
  210. package/dist/guest/toolchains/go.js +1 -1
  211. package/dist/guest/toolchains/index.d.ts +13 -9
  212. package/dist/guest/toolchains/index.js +40 -82
  213. package/dist/guest/toolchains/java.js +5 -5
  214. package/dist/guest/toolchains/leftovers.d.ts +2 -0
  215. package/dist/guest/toolchains/leftovers.js +26 -0
  216. package/dist/guest/toolchains/managers.js +12 -10
  217. package/dist/guest/toolchains/{binstall.js → native/binstall.js} +6 -9
  218. package/dist/guest/toolchains/native/cpp.d.ts +4 -0
  219. package/dist/guest/toolchains/native/cpp.js +38 -0
  220. package/dist/guest/toolchains/native/dotnet.d.ts +4 -0
  221. package/dist/guest/toolchains/native/dotnet.js +103 -0
  222. package/dist/guest/toolchains/native/mesa.d.ts +7 -0
  223. package/dist/guest/toolchains/native/mesa.js +56 -0
  224. package/dist/guest/toolchains/native/msvc/environment.d.ts +16 -0
  225. package/dist/guest/toolchains/native/msvc/environment.js +57 -0
  226. package/dist/guest/toolchains/native/msvc/index.d.ts +6 -0
  227. package/dist/guest/toolchains/native/msvc/index.js +48 -0
  228. package/dist/guest/toolchains/native/msvc/packages.d.ts +6 -0
  229. package/dist/guest/toolchains/native/msvc/packages.js +156 -0
  230. package/dist/guest/toolchains/native/msvc/tree.d.ts +15 -0
  231. package/dist/guest/toolchains/native/msvc/tree.js +21 -0
  232. package/dist/guest/toolchains/native/rust.d.ts +3 -0
  233. package/dist/guest/toolchains/native/rust.js +45 -0
  234. package/dist/guest/toolchains/node.js +26 -6
  235. package/dist/guest/toolchains/plan.d.ts +24 -0
  236. package/dist/guest/toolchains/plan.js +140 -0
  237. package/dist/guest/toolchains/pwsh.js +1 -1
  238. package/dist/guest/toolchains/python.js +43 -12
  239. package/dist/guest/toolchains/runtimes.d.ts +2 -0
  240. package/dist/guest/toolchains/runtimes.js +6 -8
  241. package/dist/guest/toolchains/scripting/php.d.ts +3 -0
  242. package/dist/guest/toolchains/scripting/php.js +71 -0
  243. package/dist/guest/toolchains/scripting/ruby.d.ts +3 -0
  244. package/dist/guest/toolchains/scripting/ruby.js +47 -0
  245. package/dist/guest/toolchains/scripting/vcruntime.d.ts +7 -0
  246. package/dist/guest/toolchains/scripting/vcruntime.js +45 -0
  247. package/dist/guest/toolchains/vscode-profile.d.ts +30 -0
  248. package/dist/guest/toolchains/vscode-profile.js +69 -0
  249. package/dist/guest/toolchains/vscode.d.ts +17 -0
  250. package/dist/guest/toolchains/vscode.js +87 -1
  251. package/dist/guest/view.js +21 -1
  252. package/dist/host/accounts.js +14 -5
  253. package/dist/host/agent-commands.d.ts +25 -0
  254. package/dist/host/agent-commands.js +145 -0
  255. package/dist/host/attach.d.ts +17 -0
  256. package/dist/host/attach.js +70 -0
  257. package/dist/host/audit/collect.d.ts +34 -0
  258. package/dist/host/audit/collect.js +126 -0
  259. package/dist/host/audit/log.d.ts +46 -0
  260. package/dist/host/audit/log.js +143 -0
  261. package/dist/host/channel.d.ts +23 -22
  262. package/dist/host/channel.js +66 -84
  263. package/dist/host/claude-profile.js +7 -3
  264. package/dist/host/cleanup.js +2 -11
  265. package/dist/host/database-plan.d.ts +12 -0
  266. package/dist/host/database-plan.js +52 -0
  267. package/dist/host/generate.d.ts +10 -12
  268. package/dist/host/generate.js +58 -135
  269. package/dist/host/join.d.ts +28 -0
  270. package/dist/host/join.js +76 -0
  271. package/dist/host/launch/mappings.d.ts +3 -0
  272. package/dist/host/launch/mappings.js +54 -0
  273. package/dist/host/launch/projects.d.ts +15 -0
  274. package/dist/host/launch/projects.js +56 -0
  275. package/dist/host/launch/stage.d.ts +11 -0
  276. package/dist/host/launch/stage.js +66 -0
  277. package/dist/host/launch/wsb.d.ts +10 -0
  278. package/dist/host/launch/wsb.js +34 -0
  279. package/dist/host/motw.d.ts +17 -0
  280. package/dist/host/motw.js +74 -0
  281. package/dist/host/pc-scan.d.ts +6 -0
  282. package/dist/host/pc-scan.js +37 -0
  283. package/dist/host/platform.d.ts +2 -0
  284. package/dist/host/platform.js +16 -1
  285. package/dist/host/port-forward.d.ts +8 -0
  286. package/dist/host/port-forward.js +37 -0
  287. package/dist/host/project-scan.d.ts +1 -17
  288. package/dist/host/project-scan.js +2 -86
  289. package/dist/host/projects.d.ts +100 -0
  290. package/dist/host/projects.js +147 -0
  291. package/dist/host/repo-guard.d.ts +25 -5
  292. package/dist/host/repo-guard.js +242 -23
  293. package/dist/host/stops.d.ts +67 -0
  294. package/dist/host/stops.js +126 -0
  295. package/dist/host/terminal.d.ts +8 -0
  296. package/dist/host/terminal.js +48 -1
  297. package/dist/host/test-guard.d.ts +6 -0
  298. package/dist/host/test-guard.js +15 -0
  299. package/dist/host/window.js +7 -3
  300. package/dist/host/workspace-view/index.d.ts +20 -0
  301. package/dist/host/workspace-view/index.js +53 -0
  302. package/dist/host/workspace-view/rules.d.ts +26 -0
  303. package/dist/host/workspace-view/rules.js +93 -0
  304. package/dist/host/workspace-view/view.d.ts +50 -0
  305. package/dist/host/workspace-view/view.js +281 -0
  306. package/dist/host/wsb.d.ts +37 -0
  307. package/dist/host/wsb.js +101 -0
  308. package/dist/runtime/daemon.js +11791 -2821
  309. package/dist/runtime/db.js +6585 -466
  310. package/dist/runtime/main.js +10238 -1724
  311. package/dist/runtime/netproxy.js +25936 -0
  312. package/dist/runtime/view.js +6183 -59
  313. package/dist/selftest/accounts.test.d.ts +2 -0
  314. package/dist/selftest/accounts.test.js +44 -0
  315. package/dist/selftest/agent-commands.test.d.ts +2 -0
  316. package/dist/selftest/agent-commands.test.js +47 -0
  317. package/dist/selftest/audit.test.d.ts +2 -0
  318. package/dist/selftest/audit.test.js +57 -0
  319. package/dist/selftest/channel.test.d.ts +2 -0
  320. package/dist/selftest/channel.test.js +44 -0
  321. package/dist/selftest/cli.test.d.ts +2 -2
  322. package/dist/selftest/cli.test.js +145 -27
  323. package/dist/selftest/cmdline.test.d.ts +2 -0
  324. package/dist/selftest/cmdline.test.js +45 -0
  325. package/dist/selftest/config.test.d.ts +2 -3
  326. package/dist/selftest/config.test.js +36 -72
  327. package/dist/selftest/data.test.d.ts +2 -2
  328. package/dist/selftest/data.test.js +77 -70
  329. package/dist/selftest/detect.test.d.ts +2 -2
  330. package/dist/selftest/detect.test.js +54 -38
  331. package/dist/selftest/fixtures.d.ts +89 -4
  332. package/dist/selftest/fixtures.js +43 -24
  333. package/dist/selftest/generate.test.d.ts +2 -0
  334. package/dist/selftest/generate.test.js +68 -0
  335. package/dist/selftest/git-access.test.d.ts +2 -0
  336. package/dist/selftest/git-access.test.js +26 -0
  337. package/dist/selftest/guest-boot.test.d.ts +2 -0
  338. package/dist/selftest/guest-boot.test.js +76 -0
  339. package/dist/selftest/harness.d.ts +44 -7
  340. package/dist/selftest/harness.js +62 -21
  341. package/dist/selftest/host-sim.test.d.ts +2 -0
  342. package/dist/selftest/host-sim.test.js +93 -0
  343. package/dist/selftest/hostile-input.test.d.ts +2 -0
  344. package/dist/selftest/hostile-input.test.js +54 -0
  345. package/dist/selftest/index.d.ts +8 -1
  346. package/dist/selftest/index.js +137 -35
  347. package/dist/selftest/links.test.d.ts +2 -0
  348. package/dist/selftest/links.test.js +50 -0
  349. package/dist/selftest/live-daemon.test.d.ts +2 -0
  350. package/dist/selftest/live-daemon.test.js +91 -0
  351. package/dist/selftest/live-git.test.d.ts +2 -0
  352. package/dist/selftest/live-git.test.js +60 -0
  353. package/dist/selftest/live-network.test.d.ts +2 -0
  354. package/dist/selftest/live-network.test.js +63 -0
  355. package/dist/selftest/live-projects.test.d.ts +2 -0
  356. package/dist/selftest/live-projects.test.js +127 -0
  357. package/dist/selftest/live.d.ts +65 -0
  358. package/dist/selftest/live.js +109 -0
  359. package/dist/selftest/local-config.test.d.ts +2 -0
  360. package/dist/selftest/local-config.test.js +120 -0
  361. package/dist/selftest/mirror.test.d.ts +2 -3
  362. package/dist/selftest/mirror.test.js +92 -65
  363. package/dist/selftest/network.test.d.ts +2 -0
  364. package/dist/selftest/network.test.js +40 -0
  365. package/dist/selftest/personalization.test.d.ts +2 -0
  366. package/dist/selftest/personalization.test.js +53 -0
  367. package/dist/selftest/projects.test.d.ts +2 -0
  368. package/dist/selftest/projects.test.js +254 -0
  369. package/dist/selftest/repair.test.d.ts +2 -0
  370. package/dist/selftest/repair.test.js +42 -0
  371. package/dist/selftest/repo-guard.test.d.ts +2 -0
  372. package/dist/selftest/repo-guard.test.js +121 -0
  373. package/dist/selftest/secrets.test.d.ts +2 -0
  374. package/dist/selftest/secrets.test.js +71 -0
  375. package/dist/selftest/setup.test.d.ts +2 -2
  376. package/dist/selftest/setup.test.js +45 -22
  377. package/dist/selftest/suite.d.ts +13 -0
  378. package/dist/selftest/suite.js +1 -0
  379. package/dist/selftest/sync.test.d.ts +2 -3
  380. package/dist/selftest/sync.test.js +130 -78
  381. package/dist/selftest/test-guard.test.d.ts +2 -0
  382. package/dist/selftest/test-guard.test.js +49 -0
  383. package/dist/selftest/toolchains.test.d.ts +2 -0
  384. package/dist/selftest/toolchains.test.js +133 -0
  385. package/dist/selftest/workspace-view.test.d.ts +2 -0
  386. package/dist/selftest/workspace-view.test.js +89 -0
  387. package/dist/selftest/workspace.test.d.ts +2 -0
  388. package/dist/selftest/workspace.test.js +53 -0
  389. package/dist/shared/apps.d.ts +49 -0
  390. package/dist/shared/apps.js +114 -0
  391. package/dist/shared/download.d.ts +3 -1
  392. package/dist/shared/download.js +19 -4
  393. package/dist/shared/netpolicy.d.ts +27 -0
  394. package/dist/shared/netpolicy.js +51 -0
  395. package/dist/shared/paths.d.ts +5 -0
  396. package/dist/shared/paths.js +6 -0
  397. package/dist/shared/projects.d.ts +53 -0
  398. package/dist/shared/projects.js +67 -0
  399. package/dist/shared/pty-client.d.ts +9 -4
  400. package/dist/shared/pty-client.js +139 -17
  401. package/dist/shared/rundir.d.ts +12 -1
  402. package/dist/shared/rundir.js +14 -4
  403. package/dist/shared/safe-remove.js +11 -0
  404. package/dist/shared/schema.d.ts +453 -0
  405. package/dist/shared/schema.js +43 -2
  406. package/dist/shared/system.d.ts +1 -0
  407. package/dist/shared/system.js +1 -0
  408. package/dist/shared/test-guard.d.ts +4 -0
  409. package/dist/shared/test-guard.js +37 -0
  410. package/dist/sync/claude-mem.js +6 -3
  411. package/dist/sync/history.d.ts +29 -12
  412. package/dist/sync/history.js +72 -25
  413. package/dist/sync/watcher.d.ts +8 -0
  414. package/dist/sync/watcher.js +99 -8
  415. package/package.json +7 -6
  416. package/sandbox/bootstrap.ps1 +258 -258
  417. package/sandbox/cargoshim.cs +56 -0
  418. package/sandbox/cpucap.cs +33 -0
  419. package/sandbox/cpucap.ps1 +20 -0
  420. package/sandbox/logon.cmd +24 -16
  421. package/sandbox/memlog.ps1 +34 -0
  422. package/sandbox/openssl.cs +42 -0
  423. package/sandbox/pagefile.cs +31 -0
  424. package/sandbox/pagefile.ps1 +21 -0
  425. package/sandbox/ptyhost.cs +120 -11
  426. package/sandbox/trust.cmd +35 -35
  427. package/schemas/config.schema.json +403 -3
  428. package/schemas/sandbox-config.schema.json +143 -13
  429. package/dist/cli/commands/data.d.ts +0 -46
  430. package/dist/cli/commands/data.js +0 -249
  431. package/dist/cli/commands/setup.js +0 -152
  432. package/dist/cli/menu.d.ts +0 -22
  433. package/dist/cli/menu.js +0 -187
  434. package/dist/cli/settings.d.ts +0 -11
  435. package/dist/cli/settings.js +0 -156
  436. package/dist/guest/toolchains/databases.js +0 -226
  437. package/dist/guest/toolchains/msvc.d.ts +0 -10
  438. package/dist/guest/toolchains/msvc.js +0 -216
  439. package/dist/guest/toolchains/native.d.ts +0 -12
  440. package/dist/guest/toolchains/native.js +0 -220
  441. package/dist/guest/toolchains/scripting.d.ts +0 -10
  442. package/dist/guest/toolchains/scripting.js +0 -137
  443. package/dist/selftest/guest.test.d.ts +0 -3
  444. package/dist/selftest/guest.test.js +0 -61
  445. package/dist/selftest/helpers.test.d.ts +0 -2
  446. package/dist/selftest/helpers.test.js +0 -125
  447. package/dist/selftest/host.test.d.ts +0 -2
  448. package/dist/selftest/host.test.js +0 -71
  449. package/dist/selftest/live.test.d.ts +0 -7
  450. package/dist/selftest/live.test.js +0 -127
  451. package/dist/selftest/security.test.d.ts +0 -3
  452. package/dist/selftest/security.test.js +0 -143
  453. /package/dist/guest/toolchains/{binstall.d.ts → native/binstall.d.ts} +0 -0
@@ -1,74 +1,38 @@
1
- // Config loading, argument parsing, workspace resolution and .sandbox-config.json trust.
2
- import fs from 'node:fs';
1
+ // Loading the settings: built-in defaults, a user config file, and the example config that ships with the tool.
3
2
  import path from 'node:path';
4
- import { parseCli as parseArgs } from '../cli/args.js';
5
- import { loadConfig } from '../core/config.js';
6
- import { ensureDir } from '../core/util.js';
7
- import { autoGuestWorkspace } from '../core/workspace.js';
8
- export const configTests = async (t, f) => {
9
- const { check, rejects, write, temp } = t;
10
- t.section('config');
11
- const builtIn = loadConfig();
12
- check(builtIn.configPath === null && builtIn.config.guestWorkspace === 'auto' && !builtIn.config.sandbox.headless && !builtIn.config.sandbox.vGpu && builtIn.config.provisioning.git.access === 'full', 'no user config: built-in defaults (auto guest folder, headless off, vGPU off, git full)');
13
- const custom = loadConfig(write(path.join(temp, 'cfg.json'), JSON.stringify({ sandbox: { headless: true }, toolchains: { rust: { enabled: false } }, mappings: [{ host: 'rel-folder', guest: 'C:\\Rel', readOnly: true }] })));
14
- check(custom.config.sandbox.headless && custom.config.toolchains.rust.enabled === false && custom.config.toolchains.python.enabled === 'auto', 'a config file only needs the keys it changes (deep merge over defaults)');
15
- check(await rejects(() => loadConfig(path.join(temp, 'missing.json')), 'Config file not found'), 'an explicit --config that does not exist is an error');
16
- check(loadConfig(path.join(t.packageRoot, 'config.example.json')).config.defaultHarness === 'claude', 'config.example.json is valid');
17
- ensureDir(path.join(temp, 'rel-folder'));
18
- const relRun = await f.run({}, { ...custom.config, storage: f.baseConfig.storage });
19
- check(relRun.config.mappings.some((m) => m.host === path.join(temp, 'rel-folder')), 'relative mappings in the user config resolve against the config file\'s folder');
20
- t.section('arguments');
21
- const wrapped = parseArgs(['--terminal', '--continue', '--headless', '--node', '20', '--', '--terminal'], true);
22
- check(wrapped.command === 'launch' && wrapped.terminal && wrapped.headless === true && wrapped.node === '20' && wrapped.passthrough.join(' ') === '--continue --terminal', 'claude-sbx: sandbox flags are consumed, everything else goes to the agent');
23
- check(await rejects(() => parseArgs(['status', '--bogus']), 'unknown option') && parseArgs(['account', 'use', 'work', '--no-restart']).positionals.join() === 'use,work' && parseArgs(['clean', '--days=3']).days === 3, 'agent-sandbox: unknown options are errors; positionals and --opt=value work');
24
- const agentFlags = parseArgs(['--model', 'opus', '-p', 'fix it', '--json', '-f', 'x', '--timeout', '5', '--resume', 'abc'], true);
25
- check(agentFlags.passthrough.join(' ') === '--model opus -p fix it --json -f x --timeout 5 --resume abc' && !agentFlags.json && !agentFlags.follow && agentFlags.timeout === 600, 'claude-sbx: only launch options are ours; any other flag (even ones agent-sandbox uses elsewhere) goes to the agent, in order');
26
- const reserved = parseArgs(['--sbx-terminal', '--terminal', '--sbx-node=18'], true);
27
- check(reserved.terminal && reserved.node === '18' && reserved.passthrough.join(' ') === '' && parseArgs(['launch', '--continue', '--sbx-headless']).passthrough.join() === '--continue' && await rejects(() => parseArgs(['--sbx-nope'], true), 'Unknown option: --sbx-nope'), '--sbx-<option> always means agent-sandbox; agent-sandbox launch passes agent flags too');
28
- t.section('workspace');
29
- const rA = await f.run();
30
- check(rA.workspaceHost === f.workA && rA.config.guestWorkspace === 'C:\\Workspace\\work-a' && rA.harnessName === 'claude' && rA.dataPath.endsWith(path.join('data', 'testdata')), 'any folder is the workspace; guest path is C:\\Workspace\\<folder name>');
31
- check(autoGuestWorkspace('C:\\x\\My Proj (2)') === 'C:\\Workspace\\My Proj _2_' && autoGuestWorkspace('D:\\...') === 'C:\\Workspace\\project', 'auto guest folder names are sanitised');
32
- check(rA.versions.node.spec === '22.3.0' && rA.versions.node.source === 'this PC' && rA.versions.python.spec === '3', 'default Node version is this PC\'s; Python defaults to latest 3.x');
33
- write(path.join(f.workA, '.nvmrc'), 'lts/iron\n');
34
- write(path.join(f.workA, '.python-version'), '3.11\n');
35
- const rV = await f.run();
36
- check(rV.versions.node.spec === 'lts/iron' && rV.versions.node.source === '.nvmrc' && rV.versions.python.spec === '3.11', '.nvmrc and .python-version are respected');
37
- check((await f.run({ nodeVersion: '18' })).versions.node.source === 'command line', '--node beats .nvmrc');
38
- fs.rmSync(path.join(f.workA, '.nvmrc'));
39
- fs.rmSync(path.join(f.workA, '.python-version'));
40
- write(path.join(f.workA, '.node-version'), 'v20.11.1');
41
- check((await f.run()).versions.node.spec === 'v20.11.1', '.node-version is respected');
42
- fs.rmSync(path.join(f.workA, '.node-version'));
43
- check(await rejects(() => f.run({ cwd: f.install }), 'install folder') && await rejects(() => f.run({ cwd: temp }), 'must not be able to change') && await rejects(() => f.run({ cwd: path.join(temp, 'storage') }), 'storage folder'), 'refuses the install folder, its parents and the storage folder');
44
- const originalWindir = process.env.WINDIR;
45
- ensureDir(path.join(temp, 'winroot', 'sys'));
46
- process.env.WINDIR = path.join(temp, 'winroot');
47
- const sysRefused = await rejects(() => f.run({ cwd: path.join(temp, 'winroot', 'sys') }), 'system folder');
48
- process.env.WINDIR = originalWindir;
49
- check(sysRefused && await rejects(() => f.run({ cwd: path.parse(temp).root }), 'whole drive'), 'refuses system folders and drive roots');
50
- t.section('.sandbox-config.json');
51
- const localB = { harness: 'codex', sandbox: { memoryMb: 4096, networking: false, headless: true }, toolchains: { node: { version: '18' }, rust: { enabled: false } }, mappings: [{ host: './assets', guest: 'C:\\Assets', readOnly: true }, { host: '../shared', guest: 'C:\\Shared', readOnly: false }], scripts: ['./setup.mjs'] };
52
- f.writeLocal(f.workB, localB);
53
- check(await rejects(() => f.run({ cwd: f.workB }), '--trust-config') && await rejects(() => f.run({ cwd: f.workB }), 'READ/WRITE'), 'an untrusted .sandbox-config.json is refused and its effects are listed');
54
- check(await rejects(() => f.run({ cwd: f.workB, ask: async () => false }), 'has not been approved'), 'answering "no" refuses it');
55
- const rB = await f.run({ cwd: f.workB, ask: async () => true });
56
- check(rB.harnessName === 'codex' && rB.config.sandbox.memoryMb === 4096 && rB.config.sandbox.headless && !rB.config.toolchains.rust.enabled && rB.config.toolchains.python.enabled && rB.versions.node.source === '.sandbox-config.json', 'approved local settings apply over the defaults (incl. toolchains, headless)');
57
- check(rB.config.mappings.some((m) => m.host === f.shared && !m.readOnly) && rB.scripts.includes(path.join(f.workB, 'setup.mjs')), 'relative paths resolve against the folder of .sandbox-config.json');
58
- check((await f.run({ cwd: f.workB })).harnessName === 'codex' && (await f.run({ cwd: f.workB, noLocalConfig: true })).harnessName === 'claude' && (await f.run({ cwd: f.workB, harness: 'gemini' })).harnessName === 'gemini', 'approval is remembered; --no-local-config and --harness win');
59
- check((await f.run({ cwd: f.workB, headless: false })).config.sandbox.headless === false, '--window beats a local headless setting');
60
- f.writeLocal(f.workB, { ...localB, sandbox: { memoryMb: 6144 } });
61
- check(await rejects(() => f.run({ cwd: f.workB }), 'has not been approved'), 'changing the file requires approval again');
62
- const dirC = path.join(temp, 'work-c');
63
- ensureDir(dirC);
64
- const bad = async (content, needle) => { f.writeLocal(dirC, content); return rejects(() => f.run({ cwd: dirC, trustConfig: true }), needle); };
65
- check(await bad({ storage: { root: 'C:\\x' } }, 'storage is not allowed') && await bad({ harnesses: {} }, 'harnesses is not allowed'), 'local file cannot change storage or agent executables');
66
- check(await bad({ claudeProfile: { hostDir: 'C:\\x' } }, 'claudeProfile.hostDir is not allowed') && await bad({ toolchains: { node: { hostDir: 'x' } } }, 'toolchains.node.hostDir is not allowed'), 'local file cannot point at host folders');
67
- check(await bad({ sandbox: { memoryMb: 12 } }, 'sandbox.memoryMb:') && await bad({ mappings: [{ host: 'x', guest: 'C:\\Windows\\x', readOnly: true }] }, 'reserved or invalid guest path') && await bad({ provisioning: { git: { access: 'admin' } } }, 'provisioning.git.access:'), 'values are type-checked; reserved guest paths and unknown git modes are rejected');
68
- check(await bad('{\n // a comment\n "readOnly": tru\n}', 'line 3') && await bad({ _comment: 'old style' }, '_comment is not allowed'), 'broken JSON is reported with line and column; unknown keys are rejected');
69
- f.writeLocal(dirC, '// comments and trailing commas are fine\n{\n "readOnly": true, /* inline */\n}\n');
70
- check((await f.run({ cwd: dirC, trustConfig: true })).config.readOnly === true, '.sandbox-config.json may contain comments and trailing commas');
71
- f.writeLocal(dirC, { claudeProfile: { copyCredentials: true }, network: { blockPrivateRanges: false }, provisioning: { git: { access: 'full' } } });
72
- check(await rejects(() => f.run({ cwd: dirC }), 'copies your Claude LOGIN') && await rejects(() => f.run({ cwd: dirC }), 'turns OFF the LAN block') && await rejects(() => f.run({ cwd: dirC }), 'commit and push'), 'risky settings are called out before approval');
73
- fs.rmSync(path.join(dirC, '.sandbox-config.json'));
3
+ import _ from 'lodash';
4
+ import { DEFAULTS, loadConfig } from '../core/config.js';
5
+ import { readJsonc } from '../core/util.js';
6
+ export const configSuite = {
7
+ name: 'config',
8
+ title: 'built-in defaults, user config file, config.example.json',
9
+ run: async (t, f) => {
10
+ t.section('built-in defaults');
11
+ const builtIn = loadConfig();
12
+ t.equal({
13
+ configPath: builtIn.configPath,
14
+ guestWorkspace: builtIn.config.guestWorkspace,
15
+ headless: builtIn.config.sandbox.headless,
16
+ vGpu: builtIn.config.sandbox.vGpu,
17
+ gitAccess: builtIn.config.provisioning.git.access,
18
+ }, { configPath: null, guestWorkspace: 'auto', headless: false, vGpu: false, gitAccess: 'full' }, 'with no user config the built-in defaults apply (auto guest folder, headless off, vGPU off, git full)');
19
+ t.section('a user config file');
20
+ const customFile = t.writeJson(t.at('cfg.json'), {
21
+ sandbox: { headless: true },
22
+ toolchains: { rust: { enabled: false } },
23
+ mappings: [{ host: 'rel-folder', guest: 'C:\\Rel', readOnly: true }],
24
+ });
25
+ const custom = loadConfig(customFile);
26
+ t.equal({ headless: custom.config.sandbox.headless, rust: custom.config.toolchains.rust.enabled, python: custom.config.toolchains.python.enabled }, { headless: true, rust: false, python: 'auto' }, 'a config file only needs the keys it changes (deep merge over the defaults)');
27
+ await t.throws(() => loadConfig(t.at('missing.json')), 'Config file not found', 'an explicit --config that does not exist is an error');
28
+ t.dir('rel-folder');
29
+ const relativeRun = await f.run({}, { ...custom.config, storage: f.baseConfig.storage });
30
+ const mappedHosts = relativeRun.config.mappings.map((m) => m.host);
31
+ t.check(mappedHosts.includes(t.at('rel-folder')), 'relative mappings in the user config resolve against the config file\'s folder', 'mapped hosts: ' + JSON.stringify(mappedHosts));
32
+ t.section('config.example.json');
33
+ const examplePath = path.join(t.packageRoot, 'config.example.json');
34
+ t.equal(loadConfig(examplePath).config.defaultHarness, 'claude', 'config.example.json is valid');
35
+ const example = _.omit(readJsonc(examplePath), '$schema');
36
+ t.check(_.isEqual(example, DEFAULTS), 'config.example.json lists exactly the built-in defaults', 'the differences are in: ' + Object.keys(DEFAULTS).filter((key) => !_.isEqual(example[key], DEFAULTS[key])).join(', '));
37
+ },
74
38
  };
@@ -1,2 +1,2 @@
1
- import type { Harness } from './harness.js';
2
- export declare const dataTests: (t: Harness) => void;
1
+ import type { Suite } from './suite.js';
2
+ export declare const dataSuite: Suite;
@@ -1,73 +1,80 @@
1
- // `sandbox data` target resolution and `sandbox repair` detection (half-installed toolchains, leftovers, links).
1
+ // `sandbox data` and `sandbox clean`: what a clean target may name, how sizes and removal treat links, and which old
2
+ // launch folders, logs and downloads are pruned.
2
3
  import fs from 'node:fs';
3
4
  import path from 'node:path';
4
- import { humanBytes, removeTree, resolveCleanTargets, sizeOf } from '../cli/commands/data.js';
5
- import { deadPidFiles, findHalfInstalled, findLinks, findStagingLeftovers, globalPackages, resolvedVersionsValid } from '../cli/commands/repair.js';
6
- import { ensureDir, errMsg } from '../core/util.js';
7
- export const dataTests = (t) => {
8
- const { check, write } = t;
9
- t.section('data: clean targets');
10
- const storage = path.join(t.temp, 'data-store');
11
- const dataPath = path.join(storage, 'data', 'default');
12
- const resolve = (...inputs) => resolveCleanTargets(inputs, dataPath, storage).map((r) => path.relative(storage, r.path));
13
- const rejected = (...inputs) => { try {
14
- resolveCleanTargets(inputs, dataPath, storage);
15
- return false;
16
- }
17
- catch (e) {
18
- return errMsg(e).length > 0;
19
- } };
20
- const d = (...parts) => path.join('data', 'default', ...parts);
21
- check(resolve('tools/go').join() === d('tools', 'go') && resolve('tools\\go').join() === d('tools', 'go') && resolve('Tools/agents/claude').join() === d('tools', 'agents', 'claude'), 'clean: tools/<name> and tools/agents/<name> resolve inside the data folder (either slash)');
22
- check(resolve('databases/postgres-18').join() === d('databases', 'postgres-18') && resolve('logs').join() === d('logs') && resolve('launches').join() === 'launches' && resolve('state/claude').join() === d('state', 'claude'), 'clean: databases/<name>, logs, launches, state/<agent>');
23
- check(resolve('tools', 'tools/go', 'tools/go').join() === d('tools') && resolve('all', 'logs').join() === [d(), 'launches'].join(), 'clean: nested and repeated targets collapse; all = the data folder + launches');
24
- check(Boolean(resolveCleanTargets(['state/claude'], dataPath, storage)[0].warning?.includes('login')) && Boolean(resolveCleanTargets(['all'], dataPath, storage)[0].warning), 'clean: state and all carry a warning');
25
- const bad = ['', '..', 'tools/..', 'tools/../../x', '../data', 'C:\\Windows', 'C:tools', '/etc', '\\\\server\\share', 'tools/*', 'tools/go:stream', 'tools/a/b', 'tools/agents/a/b', 'logs/guest.log', 'accounts', 'sync', 'windows', 'tools/.', 'tools/go.'];
26
- const accepted = bad.filter((b) => !rejected(b));
27
- check(accepted.length === 0 && rejected(), 'clean: rejects .., absolute paths, drive letters, UNC, wildcards, streams, too deep, unknown areas, nothing (accepted: ' + JSON.stringify(accepted) + ')');
28
- t.section('data: sizes and removal never follow links');
29
- const sized = path.join(t.temp, 'sized');
30
- write(path.join(sized, 'a.bin'), 'x'.repeat(1000));
31
- write(path.join(sized, 'sub', 'b.bin'), 'y'.repeat(24));
32
- const outside = path.join(t.temp, 'outside');
33
- write(path.join(outside, 'keep.txt'), 'z'.repeat(5000));
34
- let linked = false;
35
- try {
36
- fs.symlinkSync(outside, path.join(sized, 'sub', 'escape'), 'junction');
37
- linked = true;
38
- }
39
- catch { /* no junctions here */ }
40
- check(sizeOf(sized) === 1024 && humanBytes(1536) === '1.5 KB' && humanBytes(0) === '0 B' && humanBytes(3 * 1024 ** 3) === '3.0 GB', 'data: sizes count real files only' + (linked ? ' (a junction counts 0)' : ''));
41
- if (linked)
42
- check(findLinks(sized).length === 1, 'repair: finds a junction planted in the data folder');
43
- removeTree(sized, t.temp);
44
- check(!fs.existsSync(sized) && fs.existsSync(path.join(outside, 'keep.txt')), 'data: removing a folder never deletes what a link inside points to');
45
- t.section('repair: detection');
46
- const tools = path.join(t.temp, 'repair', 'tools');
47
- write(path.join(tools, 'go', '1.23.2', 'bin', 'go.exe'), 'x');
48
- ensureDir(path.join(tools, 'go', '1.22.8', 'src'));
49
- write(path.join(tools, 'java', 'jdk-21', 'bin', 'java.exe'), 'x');
50
- ensureDir(path.join(tools, 'java', 'jdk-17', 'lib'));
51
- write(path.join(tools, 'python', 'python-3.12.7', 'python.exe'), 'x');
52
- write(path.join(tools, 'nvm', 'settings.txt'), 'x');
53
- write(path.join(tools, 'nvm', 'v22.11.0', 'node.exe'), 'x');
54
- ensureDir(path.join(tools, 'nvm', 'v20.18.0'));
55
- write(path.join(tools, 'git', 'bin', 'bash.exe'), 'x');
56
- ensureDir(path.join(tools, 'dotnet', 'sdk'));
57
- ensureDir(path.join(tools, 'go', '1.24.0.staging-ab12cd'));
58
- ensureDir(path.join(tools, 'node-staging'));
59
- ensureDir(path.join(tools, 'rust.staging-0f'));
60
- const half = findHalfInstalled(tools).map((h) => path.relative(tools, h.path)).sort();
61
- check(half.join() === ['dotnet', 'java\\jdk-17', 'go\\1.22.8', 'nvm', 'nvm\\v20.18.0'].map((p) => p.split('\\').join(path.sep)).sort().join(), 'repair: half-installed toolchains = folder without its program (got ' + half.join(', ') + ')');
62
- const staging = findStagingLeftovers(tools).map((p) => path.relative(tools, p)).sort();
63
- check(staging.join() === [path.join('go', '1.24.0.staging-ab12cd'), 'node-staging', 'rust.staging-0f'].sort().join(), 'repair: interrupted downloads in tools and one level down (got ' + staging.join(', ') + ')');
64
- check(resolvedVersionsValid('{"go":"1.23.2"}') && resolvedVersionsValid('\uFEFF{}') && !resolvedVersionsValid('{"go":') && !resolvedVersionsValid('[1]'), 'repair: resolved-versions.json must be a JSON object');
65
- check(globalPackages('{"dependencies":{"agent-sandbox":{"version":"1.0.0"},"npm":{}}}').join() === 'agent-sandbox,npm' && globalPackages('nope').length === 0 && globalPackages('{"dependencies":3}').length === 0, 'repair: reads npm ls -g --json');
66
- const run = path.join(t.temp, 'repair', 'run');
67
- write(path.join(run, 'watcher-default.pid'), '4242');
68
- write(path.join(run, 'watcher-other.pid'), String(process.pid));
69
- write(path.join(run, 'watcher-junk.pid'), 'not a pid');
70
- write(path.join(run, 'hidden-windows.json'), '[]');
71
- const dead = deadPidFiles(run, (pid) => pid === process.pid).map((f) => path.basename(f)).sort();
72
- check(dead.join() === 'watcher-default.pid,watcher-junk.pid', 'repair: stale watcher pid files (dead process or junk) are found; a live one is kept');
5
+ import { resolveCleanTargets } from '../cli/commands/data/clean.js';
6
+ import { removeTree } from '../cli/commands/data/remove.js';
7
+ import { findLinks } from '../cli/commands/repair/finders.js';
8
+ import { humanBytes, sizeOf } from '../core/disk.js';
9
+ import { pruneStorage } from '../host/cleanup.js';
10
+ export const dataSuite = {
11
+ name: 'data',
12
+ title: 'data clean targets, sizes and removal with links, storage cleanup',
13
+ run: async (t) => {
14
+ t.section('clean targets');
15
+ const storage = t.at('data-store');
16
+ const dataPath = path.join(storage, 'data', 'default');
17
+ const resolve = (...inputs) => resolveCleanTargets(inputs, dataPath, storage).map((r) => path.relative(storage, r.path));
18
+ const inData = (...parts) => path.join('data', 'default', ...parts);
19
+ t.equal(resolve('tools/go'), [inData('tools', 'go')], 'tools/<name> resolves inside the data folder');
20
+ t.equal(resolve('tools\\go'), [inData('tools', 'go')], 'either slash works');
21
+ t.equal(resolve('Tools/agents/claude'), [inData('tools', 'agents', 'claude')], 'tools/agents/<name> resolves, whatever the case');
22
+ t.equal(resolve('databases/postgres-18'), [inData('databases', 'postgres-18')], 'databases/<name> resolves');
23
+ t.equal(resolve('logs'), [inData('logs')], 'logs resolves');
24
+ t.equal(resolve('launches'), ['launches'], 'launches resolves to the storage folder\'s launches');
25
+ t.equal(resolve('state/claude'), [inData('state', 'claude')], 'state/<agent> resolves');
26
+ t.equal(resolve('tools', 'tools/go', 'tools/go'), [inData('tools')], 'nested and repeated targets collapse');
27
+ t.equal(resolve('all', 'logs'), [inData(), 'launches'], '`all` is the data folder plus launches');
28
+ t.check(Boolean(resolveCleanTargets(['state/claude'], dataPath, storage)[0].warning?.includes('login')), 'cleaning state warns about the login');
29
+ t.check(Boolean(resolveCleanTargets(['all'], dataPath, storage)[0].warning), 'cleaning everything carries a warning');
30
+ const badTargets = ['', '..', 'tools/..', 'tools/../../x', '../data', 'C:\\Windows', 'C:tools', '/etc', '\\\\server\\share', 'tools/*', 'tools/go:stream', 'tools/a/b', 'tools/agents/a/b', 'logs/guest.log', 'accounts', 'sync', 'windows', 'tools/.', 'tools/go.'];
31
+ const accepted = badTargets.filter((target) => {
32
+ try {
33
+ resolveCleanTargets([target], dataPath, storage);
34
+ return true;
35
+ }
36
+ catch {
37
+ return false;
38
+ }
39
+ });
40
+ t.equal(accepted, [], 'rejected: .., absolute paths, drive letters, UNC, wildcards, streams, too deep, unknown areas');
41
+ await t.throws(() => resolveCleanTargets([], dataPath, storage), '', 'rejected: no target at all (that is not "everything")');
42
+ t.section('sizes and removal never follow links');
43
+ const sized = t.at('sized');
44
+ t.write(path.join(sized, 'a.bin'), 'x'.repeat(1000));
45
+ t.write(path.join(sized, 'sub', 'b.bin'), 'y'.repeat(24));
46
+ const outside = t.at('outside');
47
+ t.write(path.join(outside, 'keep.txt'), 'z'.repeat(5000));
48
+ let linked = false;
49
+ try {
50
+ fs.symlinkSync(outside, path.join(sized, 'sub', 'escape'), 'junction');
51
+ linked = true;
52
+ }
53
+ catch { /* no junctions here */ }
54
+ t.equal(sizeOf(sized), 1024, 'sizes count real files only' + (linked ? ' (a junction counts 0)' : ''));
55
+ t.equal([humanBytes(1536), humanBytes(0), humanBytes(3 * 1024 ** 3)], ['1.5 KB', '0 B', '3.0 GB'], 'sizes are shown in readable units');
56
+ if (linked)
57
+ t.equal(findLinks(sized).length, 1, 'repair finds a junction planted in the data folder');
58
+ removeTree(sized, t.temp);
59
+ t.check(!fs.existsSync(sized), 'a folder is removed');
60
+ t.check(fs.existsSync(path.join(outside, 'keep.txt')), 'removing a folder never deletes what a link inside points to');
61
+ t.section('storage cleanup');
62
+ const store = t.at('prune');
63
+ const aged = (p, days) => { const d = new Date(Date.now() - days * 86400000); fs.utimesSync(p, d, d); };
64
+ for (const [name, days] of [['old', 9], ['kept', 30], ['new', 1]]) {
65
+ aged(t.write(path.join(store, 'launches', name, 'agent.wsb'), 'x'), days);
66
+ aged(path.join(store, 'launches', name), days);
67
+ }
68
+ aged(t.write(path.join(store, 'data', 'd', 'logs', 'guest-old.log'), 'x'), 20);
69
+ t.write(path.join(store, 'data', 'd', 'logs', 'guest-new.log'), 'x');
70
+ const staging = t.dir('prune', 'data', 'd', 'tools', 'git.staging-ab12');
71
+ aged(staging, 2);
72
+ t.dir('prune', 'data', 'd', 'tools', 'node');
73
+ const pruned = pruneStorage(store, { keep: ['kept'] });
74
+ const left = (p) => fs.existsSync(path.join(store, p));
75
+ t.equal(pruned.launches, 1, 'cleanup removes one launch folder');
76
+ t.equal({ old: left('launches/old'), kept: left('launches/kept'), new: left('launches/new') }, { old: false, kept: true, new: true }, 'cleanup: launch folders older than 7 days go; the running launch and recent ones stay');
77
+ t.equal({ oldLog: left('data/d/logs/guest-old.log'), newLog: left('data/d/logs/guest-new.log') }, { oldLog: false, newLog: true }, 'cleanup: logs older than 14 days go');
78
+ t.equal({ interrupted: left('data/d/tools/git.staging-ab12'), realTool: left('data/d/tools/node') }, { interrupted: false, realTool: true }, 'cleanup: interrupted downloads go; real tools stay');
79
+ },
73
80
  };
@@ -1,2 +1,2 @@
1
- import type { Harness } from './harness.js';
2
- export declare const detectTests: (t: Harness) => void;
1
+ import type { Suite } from './suite.js';
2
+ export declare const detectSuite: Suite;
@@ -1,42 +1,58 @@
1
- // "Which databases does this project need?": detection from real-world file shapes, and the plan the host builds.
1
+ // "What does this project need?": databases and toolchains detected from project files, and the plans built from them.
2
2
  import { databasesFromServices, detectDatabases } from '../core/detect-databases.js';
3
3
  import { DEFAULTS } from '../core/config.js';
4
- import { databaseCatalog, planDatabases } from '../host/project-scan.js';
5
- import { voltaNode } from '../core/workspace.js';
4
+ import { databaseCatalog, planDatabases } from '../host/database-plan.js';
6
5
  import { decideToolchains, resolveToolchains } from '../core/detect-toolchains.js';
7
- import { parseReleasePage } from '../shared/download.js';
8
- const found = (files, host = []) => detectDatabases(files, host).map((d) => d.database).join(',');
9
- export const detectTests = (t) => {
10
- const { check } = t;
11
- t.section('database detection');
12
- const compose = { path: 'docker-compose.yml', text: 'services:\n db:\n image: postgres:16-alpine\n cache:\n image: "redis:7"\n dynamo:\n image: amazon/dynamodb-local:latest\n' };
13
- check(found([compose]) === 'postgres,redis,dynamodb', 'compose services: postgres, redis and DynamoDB Local images');
14
- check(found([{ path: 'package.json', text: '{"dependencies":{"pg":"^8.11.0","ioredis":"5","@aws-sdk/client-dynamodb":"3"}}' }]) === 'postgres,redis,dynamodb', 'package.json: pg, ioredis, @aws-sdk/client-dynamodb');
15
- check(found([{ path: 'package.json', text: '{"dependencies":{"pgadmin-ui":"1","mysql-logger":"2","express":"4"}}' }]) === '', 'package.json: names that only contain a driver name do not count');
16
- check(found([{ path: 'prisma/schema.prisma', text: 'datasource db {\n provider = "mysql"\n url = env("DATABASE_URL")\n}' }]) === 'mysql', 'Prisma: the datasource provider decides');
17
- check(found([{ path: 'requirements.txt', text: 'django==5.0\npsycopg2-binary==2.9\npymongo>=4\n' }]) === 'postgres,mongodb', 'Python requirements: psycopg2 and pymongo');
18
- check(found([{ path: 'Api/Api.csproj', text: '<PackageReference Include="Microsoft.EntityFrameworkCore.SqlServer" Version="8.0.0" />' }]) === 'sqlserver', '.NET: EF Core SQL Server provider');
19
- check(found([{ path: '.env.example', text: 'DATABASE_URL=postgresql://user:pass@localhost:5432/app\nMONGO_URL=mongodb+srv://cluster/x\n' }]) === 'postgres,mongodb', '.env templates: connection-string schemes');
20
- check(found([{ path: 'template.yaml', text: 'Resources:\n Table:\n Type: AWS::DynamoDB::Table\n' }]) === 'dynamodb', 'SAM/CloudFormation: a DynamoDB table resource');
21
- check(found([{ path: 'go.mod', text: 'require (\n\tgithub.com/jackc/pgx/v5 v5.5.0\n\tgithub.com/aws/aws-sdk-go-v2/service/dynamodb v1.0.0\n)' }]) === 'postgres,dynamodb', 'go.mod: pgx and the DynamoDB SDK');
22
- check(found([{ path: 'package.json', text: '{"dependencies":{"knex":"3"}}' }]) === '' && found([{ path: 'package.json', text: '{"dependencies":{"knex":"3"}}' }], ['mysql']) === 'mysql', 'an ORM that names no database uses the one installed on this PC (and nothing without one)');
23
- check(databasesFromServices(['postgresql-x64-16', 'MySQL80', 'MSSQL$SQLEXPRESS', 'Spooler', 'Redis']).join() === 'postgres,mysql,sqlserver,redis', 'database services on this PC are recognised by name');
24
- const plan = planDatabases({ ...DEFAULTS.databases, mongodb: { ...DEFAULTS.databases.mongodb, enabled: true }, redis: { ...DEFAULTS.databases.redis, enabled: false } }, 'unused', { files: [compose], host: [] });
25
- check(plan.map((p) => p.database + '@' + p.version + ':' + p.port).join() === 'postgres@16:5432,mongodb@latest:27017,dynamodb@latest:8000' && plan.find((p) => p.database === 'dynamodb')?.adminPort === 8001, 'plan: "auto" from the project (compose pins Postgres 16), true always, false never; DynamoDB gets its admin UI');
26
- const catalog = databaseCatalog({ ...DEFAULTS.databases, postgres: { ...DEFAULTS.databases.postgres, enabled: false, port: 6543 } });
27
- check(catalog.map((p) => p.database).join() === 'postgres,mysql,mongodb,redis,dynamodb' && catalog[0].port === 6543, 'sbx-db catalog: every database that can run (not SQL Server), even ones off at boot, with their configured ports');
28
- const volta = t.write(t.temp + '\\volta-ws\\package.json', '{"name":"x","volta":{"node":"20.11.1","pnpm":"9.1.0"}}');
29
- t.write(t.temp + '\\no-volta-ws\\package.json', '{"name":"y","volta":{"node":"latest; rm -rf"}}');
30
- check(voltaNode(volta.replace(/\\package\.json$/, '')) === '20.11.1' && voltaNode(t.temp + '\\no-volta-ws') === null && voltaNode(t.temp + '\\missing') === null, 'Volta: package.json "volta.node" picks the Node version (only a plain version is accepted)');
31
- const tc = DEFAULTS.toolchains;
32
- const goApi = decideToolchains(tc, ['README.md', 'go.mod', 'api/main.go'], ['python', 'rust']);
33
- check(goApi.go.on && goApi.go.reason === 'go.mod' && !goApi.python.on && !goApi.rust.on && !goApi.dotnet.on && !goApi.java.on, 'toolchains: a Go project gets Go and nothing it does not use, whatever this PC has');
34
- const fresh = decideToolchains(tc, ['notes.txt'], ['python', 'dotnet']);
35
- check(fresh.python.on && fresh.dotnet.on && !fresh.go.on && fresh.python.reason.includes('this PC'), 'toolchains: a folder that is no project yet gets the languages this PC has');
36
- const rusty = decideToolchains({ ...tc, python: { ...tc.python, enabled: true } }, ['Cargo.toml'], []);
37
- check(rusty.rust.on && rusty.cpp.on && rusty.python.on && rusty.python.reason === 'enabled in config', 'toolchains: Rust brings its C/C++ linker; true in config always wins');
38
- check(resolveToolchains(tc, goApi).go.enabled === true && resolveToolchains(tc, goApi).python.enabled === false, 'the sandbox gets plain decisions instead of "auto"');
39
- const page = '<a href="/o/r/releases/download/v1/tool-win-x64.zip">x</a><span>sha256:' + 'a'.repeat(64) + '</span><a href="/o/r/releases/download/v1/tool-linux.tar.gz">y</a>';
40
- const assets = parseReleasePage(page);
41
- check(assets.length === 2 && assets[0].url === 'https://github.com/o/r/releases/download/v1/tool-win-x64.zip' && assets[0].sha256 === 'a'.repeat(64) && assets[1].sha256 === null, 'GitHub release page fallback (API rate limit): assets with their own sha256 only');
6
+ const found = (files, host = []) => detectDatabases(files, host).map((d) => d.database);
7
+ const file = (path, text) => ({ path, text });
8
+ export const detectSuite = {
9
+ name: 'detect',
10
+ title: 'database and toolchain detection from project files',
11
+ run: (t) => {
12
+ t.section('databases from project files');
13
+ const compose = file('docker-compose.yml', 'services:\n db:\n image: postgres:16-alpine\n cache:\n image: "redis:7"\n dynamo:\n image: amazon/dynamodb-local:latest\n');
14
+ t.equal(found([compose]), ['postgres', 'redis', 'dynamodb'], 'compose services: postgres, redis and DynamoDB Local images');
15
+ t.equal(found([file('package.json', '{"dependencies":{"pg":"^8.11.0","ioredis":"5","@aws-sdk/client-dynamodb":"3"}}')]), ['postgres', 'redis', 'dynamodb'], 'package.json: pg, ioredis, @aws-sdk/client-dynamodb');
16
+ t.equal(found([file('package.json', '{"dependencies":{"pgadmin-ui":"1","mysql-logger":"2","express":"4"}}')]), [], 'package.json: names that only contain a driver name do not count');
17
+ t.equal(found([file('prisma/schema.prisma', 'datasource db {\n provider = "mysql"\n url = env("DATABASE_URL")\n}')]), ['mysql'], 'Prisma: the datasource provider decides');
18
+ t.equal(found([file('requirements.txt', 'django==5.0\npsycopg2-binary==2.9\npymongo>=4\n')]), ['postgres', 'mongodb'], 'Python requirements: psycopg2 and pymongo');
19
+ t.equal(found([file('Api/Api.csproj', '<PackageReference Include="Microsoft.EntityFrameworkCore.SqlServer" Version="8.0.0" />')]), ['sqlserver'], '.NET: EF Core SQL Server provider');
20
+ t.equal(found([file('.env.example', 'DATABASE_URL=postgresql://user:pass@localhost:5432/app\nMONGO_URL=mongodb+srv://cluster/x\n')]), ['postgres', 'mongodb'], '.env templates: connection-string schemes');
21
+ t.equal(found([file('template.yaml', 'Resources:\n Table:\n Type: AWS::DynamoDB::Table\n')]), ['dynamodb'], 'SAM/CloudFormation: a DynamoDB table resource');
22
+ t.equal(found([file('go.mod', 'require (\n\tgithub.com/jackc/pgx/v5 v5.5.0\n\tgithub.com/aws/aws-sdk-go-v2/service/dynamodb v1.0.0\n)')]), ['postgres', 'dynamodb'], 'go.mod: pgx and the DynamoDB SDK');
23
+ const knex = [file('package.json', '{"dependencies":{"knex":"3"}}')];
24
+ t.equal(found(knex), [], 'an ORM that names no database uses nothing when this PC has none');
25
+ t.equal(found(knex, ['mysql']), ['mysql'], 'an ORM that names no database uses the one installed on this PC');
26
+ t.equal(databasesFromServices(['postgresql-x64-16', 'MySQL80', 'MSSQL$SQLEXPRESS', 'Spooler', 'Redis']), ['postgres', 'mysql', 'sqlserver', 'redis'], 'database services on this PC are recognised by name');
27
+ // Hostile workspace files (a cloned repository is untrusted): detection must stay linear.
28
+ const hostile = [
29
+ file('.env', 'Server=a;Database=a;'.repeat(12000)),
30
+ file('compose.yaml', 'services:\n x:\n image: ' + 'postgres'.repeat(20000) + '!\n'),
31
+ file('appsettings.json', 'Host=a;'.repeat(30000)),
32
+ ];
33
+ const started = Date.now();
34
+ detectDatabases(hostile);
35
+ const took = Date.now() - started;
36
+ t.check(took < 2000, 'database detection stays fast on hostile workspace files (no catastrophic backtracking)', 'took ' + took + ' ms');
37
+ t.section('the database plan');
38
+ const plan = planDatabases({ ...DEFAULTS.databases, mongodb: { ...DEFAULTS.databases.mongodb, enabled: true }, redis: { ...DEFAULTS.databases.redis, enabled: false } }, 'unused', { files: [compose], host: [] });
39
+ t.equal(plan.map((p) => p.database + '@' + p.version + ':' + p.port), ['postgres@16:5432', 'mongodb@latest:27017', 'dynamodb@latest:8000'], 'plan: "auto" from the project (compose pins Postgres 16), true always, false never');
40
+ t.equal(plan.find((p) => p.database === 'dynamodb')?.adminPort, 8001, 'plan: DynamoDB gets its admin UI port');
41
+ const catalog = databaseCatalog({ ...DEFAULTS.databases, postgres: { ...DEFAULTS.databases.postgres, enabled: false, port: 6543 } });
42
+ t.equal(catalog.map((p) => p.database), ['postgres', 'mysql', 'mongodb', 'redis', 'dynamodb'], 'sbx-db catalog: every database that can run (not SQL Server), even ones off at boot');
43
+ t.equal(catalog[0].port, 6543, 'sbx-db catalog: configured ports are used');
44
+ t.section('toolchains from project files');
45
+ const tc = DEFAULTS.toolchains;
46
+ const goProject = decideToolchains(tc, ['README.md', 'go.mod', 'api/main.go'], ['python', 'rust']);
47
+ t.equal({ go: goProject.go.on, reason: goProject.go.reason }, { go: true, reason: 'go.mod' }, 'toolchains: a Go project gets Go (and says why)');
48
+ t.equal({ python: goProject.python.on, rust: goProject.rust.on, dotnet: goProject.dotnet.on, java: goProject.java.on }, { python: false, rust: false, dotnet: false, java: false }, 'toolchains: a Go project gets nothing it does not use, whatever this PC has');
49
+ const fresh = decideToolchains(tc, ['notes.txt'], ['python', 'dotnet']);
50
+ t.equal({ python: fresh.python.on, dotnet: fresh.dotnet.on, go: fresh.go.on }, { python: true, dotnet: true, go: false }, 'toolchains: a folder that is no project yet gets the languages this PC has');
51
+ t.includes(fresh.python.reason, 'this PC', 'toolchains: the reason names this PC');
52
+ const rusty = decideToolchains({ ...tc, python: { ...tc.python, enabled: true } }, ['Cargo.toml'], []);
53
+ t.equal({ rust: rusty.rust.on, cpp: rusty.cpp.on }, { rust: true, cpp: true }, 'toolchains: Rust brings its C/C++ linker');
54
+ t.equal({ python: rusty.python.on, reason: rusty.python.reason }, { python: true, reason: 'enabled in config' }, 'toolchains: true in config always wins');
55
+ const resolved = resolveToolchains(tc, goProject);
56
+ t.equal({ go: resolved.go.enabled, python: resolved.python.enabled }, { go: true, python: false }, 'the sandbox gets plain decisions instead of "auto"');
57
+ },
42
58
  };