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,45 @@
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 _ from 'lodash';
5
+ import { diskUsage, humanBytes } from '../../../core/disk.js';
6
+ import { fail } from '../../../core/util.js';
7
+ import { printJson, table } from '../context.js';
8
+ import { working } from '../../progress.js';
9
+ import { dataClean } from './clean.js';
10
+ import { collectDataEntries } from './collect.js';
11
+ const AREA_LABEL = { tools: 'Toolchains', databases: 'Databases', state: 'Agent state', logs: 'Logs', data: 'Other', host: 'On this PC' };
12
+ export const dataInfo = async (ctx) => {
13
+ const entries = await working('Measuring the saved data (big folders take a moment)', () => collectDataEntries(ctx.dataPath, ctx.storageRoot), !ctx.options.json);
14
+ const total = _.sumBy(entries, 'bytes');
15
+ const disks = diskUsage(ctx.storageRoot);
16
+ if (printJson(ctx, { dataPath: ctx.dataPath, storageRoot: ctx.storageRoot, totalBytes: total, entries, disks }))
17
+ return 0;
18
+ console.log('Disks\n');
19
+ 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)' : ''])]);
20
+ console.log('');
21
+ console.log('Persistent data for the sandbox: ' + ctx.dataPath + ' (C:\\AgentData inside)\n');
22
+ if (!entries.length) {
23
+ console.log('Nothing stored yet. Toolchains, databases and agent logins appear here after the first start.');
24
+ return 0;
25
+ }
26
+ const rows = _.flatMap(_.groupBy(entries, 'area'), (group, area) => [
27
+ [AREA_LABEL[area], humanBytes(_.sumBy(group, 'bytes')), ''],
28
+ ...group.map((e) => [' ' + e.name, humanBytes(e.bytes), e.path]),
29
+ ]);
30
+ table([...rows, ['', '', ''], ['Total', humanBytes(total), '']]);
31
+ console.log('\nFree space with: sandbox data clean <target...> (e.g. tools/go, databases/postgres-18, logs, launches)');
32
+ return 0;
33
+ };
34
+ /** `sandbox data [info|clean] [targets...]`. */
35
+ export const data = (ctx) => {
36
+ const [action = 'info'] = ctx.options.positionals;
37
+ switch (action) {
38
+ case 'info':
39
+ case 'show':
40
+ case 'list': return dataInfo(ctx);
41
+ case 'clean':
42
+ case 'remove': return dataClean(ctx);
43
+ default: return fail('Unknown data action "' + action + '".', 'Use: sandbox data (what is stored) or sandbox data clean <target...>');
44
+ }
45
+ };
@@ -0,0 +1,2 @@
1
+ /** Deletes p (strictly inside root): links inside are removed first and never followed; see shared/safe-remove.ts. */
2
+ export declare const removeTree: (p: string, root: string) => void;
@@ -0,0 +1,30 @@
1
+ // Deleting inside the data folder. The sandbox can write there, so links are removed themselves and never followed.
2
+ import fs from 'node:fs';
3
+ import path from 'node:path';
4
+ import { dirExists, isLink, removeLink } from '../../../core/util.js';
5
+ import { removeRefusal, safeRemove } from '../../../shared/safe-remove.js';
6
+ /** Deletes p (strictly inside root): links inside are removed first and never followed; see shared/safe-remove.ts. */
7
+ export const removeTree = (p, root) => {
8
+ const unlinkInside = (dir) => {
9
+ for (const d of fs.readdirSync(dir, { withFileTypes: true })) {
10
+ const child = path.join(dir, d.name);
11
+ if (d.isSymbolicLink() || isLink(child))
12
+ removeLink(child);
13
+ else if (d.isDirectory())
14
+ unlinkInside(child);
15
+ }
16
+ };
17
+ if (isLink(p)) {
18
+ removeLink(p);
19
+ return;
20
+ }
21
+ if (!fs.existsSync(p))
22
+ return;
23
+ // Refused before anything is touched (safeRemove below checks again).
24
+ const refusal = removeRefusal(p, root);
25
+ if (refusal)
26
+ throw new Error('Refusing to delete ' + p + ': ' + refusal);
27
+ if (dirExists(p))
28
+ unlinkInside(p);
29
+ safeRemove(p, root);
30
+ };
@@ -1,8 +1,10 @@
1
1
  export { createContext, type Context } from './context.js';
2
2
  export { launch } from './launch.js';
3
- export { setup } from './setup.js';
4
- export { data } from './data.js';
5
- export { repair, repairCommand } from './repair.js';
3
+ export { setup } from './setup/index.js';
4
+ export { data } from './data/index.js';
5
+ export { repairCommand } from './repair/index.js';
6
6
  export { attach, db, exec, hide, restart, sessions, show, status, stop } from './sessions.js';
7
7
  export { account } from './accounts.js';
8
+ export { projects } from './projects.js';
8
9
  export { claudeHost, clean, config, doctor, logs, sync, watcher } from './maintenance.js';
10
+ export { agentCommandsCommand, syncAgentCommands } from './agent-commands.js';
@@ -1,8 +1,10 @@
1
1
  export { createContext } from './context.js';
2
2
  export { launch } from './launch.js';
3
- export { setup } from './setup.js';
4
- export { data } from './data.js';
5
- export { repair, repairCommand } from './repair.js';
3
+ export { setup } from './setup/index.js';
4
+ export { data } from './data/index.js';
5
+ export { repairCommand } from './repair/index.js';
6
6
  export { attach, db, exec, hide, restart, sessions, show, status, stop } from './sessions.js';
7
7
  export { account } from './accounts.js';
8
+ export { projects } from './projects.js';
8
9
  export { claudeHost, clean, config, doctor, logs, sync, watcher } from './maintenance.js';
10
+ export { agentCommandsCommand, syncAgentCommands } from './agent-commands.js';
@@ -1,5 +1,29 @@
1
- import { attachTerminal } from '../../host/channel.js';
1
+ import { attachTerminal } from '../../host/attach.js';
2
+ import type { SandboxStatus } from '../../core/types.js';
2
3
  import { type Context } from './context.js';
4
+ import { type Joined } from './project.js';
3
5
  /** Attaches this terminal; with attach.autoHide the sandbox window hides meanwhile (best effort, never fails the attach). */
4
6
  export declare const attachHere: (ctx: Context, session: Parameters<typeof attachTerminal>[0]) => Promise<number>;
5
- export declare const launch: (ctx: Context, generateOnly?: boolean) => Promise<number>;
7
+ /** The sandbox writes sandbox.json a few seconds into its boot; a second launch right after the first waits for it. */
8
+ export declare const runningSandboxStatus: (dataPath: string) => Promise<SandboxStatus>;
9
+ /** Waits for the boot with its current step on a spinner (plain lines without a terminal). */
10
+ export declare const waitForBoot: (dataPath: string, launchId: string | null) => Promise<SandboxStatus>;
11
+ /** The running sandbox, once it is ready. */
12
+ export declare const readySandbox: (dataPath: string) => Promise<SandboxStatus>;
13
+ /**
14
+ * Starts (or picks back up) a session in the folder `joined` describes. attach: show it in this terminal.
15
+ * Exported for restart.ts, which reopens the sessions a restart ended.
16
+ */
17
+ export declare const openSession: (ctx: Context, ready: SandboxStatus, joined: Joined, o: {
18
+ attach: boolean;
19
+ harness?: string;
20
+ args?: string[];
21
+ reuse?: boolean;
22
+ }) => Promise<number>;
23
+ /** A restart that adds a project: map these too, start no session at boot (restart.ts opens them), wait until ready. */
24
+ export interface LaunchExtras {
25
+ extraProjects?: string[];
26
+ autoSession?: boolean;
27
+ waitUntilReady?: boolean;
28
+ }
29
+ export declare const launch: (ctx: Context, generateOnly?: boolean, extras?: LaunchExtras) => Promise<number>;
@@ -1,20 +1,36 @@
1
- // `launch` / `generate`, and what sandbox claude runs: start a sandbox, or another session in the running one.
1
+ // `launch` / `generate`, and what sandbox claude runs: start a sandbox, or another session in the running one. From a
2
+ // folder the running sandbox does not have yet, the folder joins it (no restart) when it can, and otherwise a
3
+ // restart that adds it is offered (restart.ts).
4
+ import path from 'node:path';
5
+ import fs from 'node:fs';
6
+ import { select } from '@inquirer/prompts';
2
7
  import { activateAccount, activeAccount, defaultAccount } from '../../host/accounts.js';
3
- import { attachTerminal, findUnwatchedSession, requestSession, resetRunDir, waitForReady, waitForSession } from '../../host/channel.js';
8
+ import { attachTerminal, findUnwatchedSession } from '../../host/attach.js';
9
+ import { requestSession, resetRunDir, waitForReady, waitForSession } from '../../host/channel.js';
4
10
  import { describePrune, pruneStorage } from '../../host/cleanup.js';
5
11
  import { generateSandbox } from '../../host/generate.js';
6
12
  import { findSecretFiles } from '../../host/secrets.js';
7
13
  import { checkRepo, saveRepoSnapshot } from '../../host/repo-guard.js';
8
14
  import { assertCanLaunch, insideSandbox, isSandboxRunning, probeHost, spawnDetached, startSandbox } from '../../host/platform.js';
15
+ import { NeedsRestart, rememberProject, startRegistry } from '../../host/projects.js';
9
16
  import { ensureTerminal } from '../../host/terminal.js';
10
17
  import { hideSandboxWindow, showSandboxWindow } from '../../host/window.js';
11
18
  import { readSandboxStatus } from '../../shared/rundir.js';
12
- import path from 'node:path';
13
- import fs from 'node:fs';
19
+ import { isWithin } from '../../shared/projects.js';
14
20
  import { syncBeforeLaunch, watcherRunning } from '../../sync/watcher.js';
15
- import { errMsg, fail, sleep } from '../../core/util.js';
16
- import { askYesNo, resolveRun } from '../../core/workspace.js';
17
- import { currentGuestDir, sandboxAlive, table } from './context.js';
21
+ import { protectionsOf } from '../../core/project-policy.js';
22
+ import { assertSafeMapping, assertSafeWorkspace } from '../../core/safe-paths.js';
23
+ import { errMsg, fail, isRootPath, sleep } from '../../core/util.js';
24
+ import { syncAgentCommands } from './agent-commands.js';
25
+ import { askYesNo } from '../../core/prompt.js';
26
+ import { resolveRun } from '../../core/workspace.js';
27
+ import { c } from '../colors.js';
28
+ import { working } from '../progress.js';
29
+ import { requireOnPc, sandboxAlive, table } from './context.js';
30
+ import _ from 'lodash';
31
+ import { hereDir, joinHere, readyHere } from './project.js';
32
+ import { offerRestart } from './restart.js';
33
+ import { addToList, setSetting } from './user-config.js';
18
34
  /** Attaches this terminal; with attach.autoHide the sandbox window hides meanwhile (best effort, never fails the attach). */
19
35
  export const attachHere = (ctx, session) => {
20
36
  if (ctx.loaded.config.attach.autoHide && !insideSandbox())
@@ -24,9 +40,9 @@ export const attachHere = (ctx, session) => {
24
40
  /** Nothing has been installed into this data set yet: the first start downloads every toolchain once. */
25
41
  const firstBoot = (dataPath) => !fs.existsSync(path.join(dataPath, 'tools', 'resolved-versions.json'));
26
42
  const FIRST_BOOT = 'First start for this storage: the sandbox downloads and installs the toolchains, databases and agent this project needs.\n' +
27
- 'That takes a few minutes, once. Everything is kept, so every later start reuses it and takes seconds. Progress:';
43
+ 'That takes a few minutes, once. Everything is kept, so every later start reuses it and takes seconds.';
28
44
  /** The sandbox writes sandbox.json a few seconds into its boot; a second launch right after the first waits for it. */
29
- const runningSandboxStatus = async (dataPath) => {
45
+ export const runningSandboxStatus = async (dataPath) => {
30
46
  for (let i = 0; i < 60; i++) {
31
47
  const status = readSandboxStatus(dataPath);
32
48
  if (status)
@@ -35,28 +51,124 @@ const runningSandboxStatus = async (dataPath) => {
35
51
  }
36
52
  return fail('A Windows Sandbox is running that this tool did not start (or that uses another storage.dataName).', 'Close that sandbox window first, then run sandbox claude again.');
37
53
  };
38
- /** A sandbox is already running: open another agent session in it (new tab inside; optionally attach here). */
39
- const launchIntoRunning = async (ctx) => {
40
- const { options, loaded, dataPath } = ctx;
54
+ /** Waits for the boot with its current step on a spinner (plain lines without a terminal). */
55
+ export const waitForBoot = (dataPath, launchId) => {
56
+ let step = 'starting Windows Sandbox';
57
+ return working(() => 'Waiting for the sandbox: ' + step, () => waitForReady(dataPath, launchId, sandboxAlive, (m) => { step = m; }));
58
+ };
59
+ /** The running sandbox, once it is ready. */
60
+ export const readySandbox = async (dataPath) => {
41
61
  const status = await runningSandboxStatus(dataPath);
42
- const ready = status.phase === 'ready' ? status : await waitForReady(dataPath, status.launchId, sandboxAlive);
43
- const cwd = currentGuestDir(ctx, ready.workspaceHost, ready.guestWorkspace);
44
- const harness = options.harness ?? loaded.config.defaultHarness;
62
+ return status.phase === 'ready' ? status : waitForBoot(dataPath, status.launchId);
63
+ };
64
+ /** Waits for a session to start (its project's setup, the agent's install) with its progress on a spinner. */
65
+ const waitForStart = (ctx, requestId, what) => {
66
+ let step = 'starting';
67
+ return working(() => what + ': ' + step, () => waitForSession(ctx.dataPath, requestId, sandboxAlive, 60 * 60 * 1000, (m) => { step = m; }));
68
+ };
69
+ /**
70
+ * Starts (or picks back up) a session in the folder `joined` describes. attach: show it in this terminal.
71
+ * Exported for restart.ts, which reopens the sessions a restart ended.
72
+ */
73
+ export const openSession = async (ctx, ready, joined, o) => {
74
+ const { options, loaded, dataPath } = ctx;
75
+ const cwd = joined.guestDir;
76
+ const harness = o.harness ?? options.harness ?? joined.harness ?? loaded.config.defaultHarness;
77
+ const args = o.args ?? options.passthrough;
45
78
  // -t with nothing new for the agent: pick up this folder's session if no terminal on this PC shows it (its tab was
46
79
  // closed, or you detached). Someone already attached, --new, or agent arguments: a new session, as before.
47
- const reuse = options.terminal && !options.newSession && options.passthrough.length === 0 && loaded.config.attach.reuseSession;
80
+ const reuse = (o.reuse ?? true) && o.attach && !options.newSession && args.length === 0 && loaded.config.attach.reuseSession;
48
81
  const existing = reuse ? await findUnwatchedSession(dataPath, harness, cwd) : null;
49
82
  if (existing) {
50
83
  console.log('Reattaching to ' + harness + ' session ' + existing.id + ' in ' + cwd + ' (no other terminal is attached; --new starts another).');
51
84
  return attachHere(ctx, existing);
52
85
  }
53
- console.log('Sandbox already running: starting another ' + harness + ' session in ' + cwd);
54
- const session = await waitForSession(dataPath, requestSession(dataPath, harness, options.passthrough, cwd), sandboxAlive);
55
- if (options.terminal)
86
+ console.log(joined.spec
87
+ ? 'Starting ' + harness + ' for ' + joined.project?.host + ' in the running sandbox (' + cwd + '; the first time sets up what it needs).'
88
+ : 'Sandbox already running: starting another ' + harness + ' session in ' + cwd);
89
+ // The agent's definition goes along: the sandbox may have booted with another agent.
90
+ const harnessSpec = loaded.config.harnesses[harness];
91
+ const requestId = requestSession(dataPath, harness, args, cwd, { ...(joined.spec ? { project: joined.spec } : {}), ...(harnessSpec ? { harnessSpec } : {}) });
92
+ const session = await waitForStart(ctx, requestId, 'Starting ' + harness + ' in ' + cwd);
93
+ if (o.attach)
56
94
  return attachHere(ctx, session);
57
95
  console.log('Session ' + session.id + ' opened in a new tab inside the sandbox. Attach here with: sandbox attach ' + session.id + (ready.headless ? ' (headless: sbx-show opens the window)' : ''));
58
96
  return 0;
59
97
  };
98
+ /** The sessions a launch asks for: each --session, else one (the agent, in this folder). */
99
+ const wantedSessions = (ctx) => ctx.options.sessions.length ? ctx.options.sessions : [{ harness: ctx.options.harness, folder: hereDir(ctx) }];
100
+ /** The same command, for one of its sessions (its folder, agent, and the arguments after -- plus its own). */
101
+ const contextFor = (ctx, want) => ({ ...ctx, options: { ...ctx.options, workspace: want.folder, harness: want.harness, sessions: [], passthrough: [...ctx.options.passthrough, ...(want.args ?? [])] } });
102
+ /** Several sessions at once: all requested together (projects set up in parallel), then listed; -t attaches the first. */
103
+ const openSessions = async (ctx, ready, list) => {
104
+ const { dataPath, loaded, options } = ctx;
105
+ const requests = list.map(({ ctx: at, joined }) => {
106
+ const harness = at.options.harness ?? joined.harness ?? loaded.config.defaultHarness;
107
+ const harnessSpec = loaded.config.harnesses[harness];
108
+ const id = requestSession(dataPath, harness, at.options.passthrough, joined.guestDir, { ...(joined.spec ? { project: joined.spec } : {}), ...(harnessSpec ? { harnessSpec } : {}) });
109
+ return { id, harness, folder: at.options.workspace ?? joined.guestDir, cwd: joined.guestDir };
110
+ });
111
+ let started = 0;
112
+ const results = await working(() => 'Starting ' + requests.length + ' sessions (' + started + ' running)', () => Promise.allSettled(requests.map(async (r) => {
113
+ const s = await waitForSession(dataPath, r.id, sandboxAlive, 60 * 60 * 1000, () => undefined);
114
+ started++;
115
+ return s;
116
+ })));
117
+ table([['SESSION', 'AGENT', 'FOLDER', ''], ...requests.map((r, i) => {
118
+ const result = results[i];
119
+ return [r.id, r.harness, r.folder, result.status === 'fulfilled' ? 'running sandbox attach ' + r.id : c.red('FAILED: ' + errMsg(result.reason))];
120
+ })]);
121
+ const first = results.find((r) => r.status === 'fulfilled');
122
+ if (options.terminal && first)
123
+ return attachHere(ctx, first.value);
124
+ if (!first)
125
+ return 1;
126
+ console.log('Each has its own tab inside the sandbox' + (ready.headless ? ' (headless: sbx-show opens the window)' : '') + '. Attach to any of them, from any terminal: sandbox attach');
127
+ return results.every((r) => r.status === 'fulfilled') ? 0 : 1;
128
+ };
129
+ /** A sandbox is already running: open the sessions in it (new tabs inside; optionally attach here). */
130
+ const launchIntoRunning = async (ctx) => {
131
+ const ready = await readySandbox(ctx.dataPath);
132
+ const wants = wantedSessions(ctx);
133
+ // Every folder joins first: when one needs a restart, nothing has been started that the restart would end.
134
+ const joined = [];
135
+ const needs = [];
136
+ for (const want of wants) {
137
+ const at = ctx.options.sessions.length ? contextFor(ctx, want) : ctx;
138
+ try {
139
+ joined.push({ ctx: at, joined: await joinHere(at, ready) });
140
+ }
141
+ catch (e) {
142
+ if (!(e instanceof NeedsRestart))
143
+ throw e;
144
+ if (!needs.some((n) => n.hostDir.toLowerCase() === e.hostDir.toLowerCase()))
145
+ needs.push(e);
146
+ }
147
+ }
148
+ if (needs.length)
149
+ return offerRestart(ctx, needs, () => launchIntoRunning(ctx));
150
+ for (const note of _.uniq(joined.flatMap((j) => j.joined.notes)))
151
+ console.log(note);
152
+ if (joined.length === 1)
153
+ return openSession(joined[0].ctx, ready, joined[0].joined, { attach: ctx.options.terminal });
154
+ return openSessions(ctx, ready, joined);
155
+ };
156
+ /** `sandbox start` while one runs: this folder joins it (set up, no agent), or a restart that adds it is offered. */
157
+ const startIntoRunning = async (ctx) => {
158
+ const ready = await readySandbox(ctx.dataPath);
159
+ let joined;
160
+ try {
161
+ joined = await readyHere(ctx, ready);
162
+ }
163
+ catch (e) {
164
+ if (e instanceof NeedsRestart)
165
+ return offerRestart(ctx, [e], () => startIntoRunning(ctx));
166
+ throw e;
167
+ }
168
+ const where = joined.spec ? ' This folder is ready in it as ' + joined.guestDir + ' (sbx-shell ' + joined.spec.name + ' opens a terminal there inside).' : '';
169
+ console.log('A sandbox is already running' + (showSandboxWindow(ctx.storageRoot) ? '; its window is in front now.' : '.') + where + ' Start an agent in it with sandbox claude (or sandbox <agent>); stop it with sandbox stop.');
170
+ return 0;
171
+ };
60
172
  const syncSummary = (config, dataPath, workspaceHost) => {
61
173
  try {
62
174
  const s = syncBeforeLaunch(config, dataPath, workspaceHost);
@@ -66,6 +178,44 @@ const syncSummary = (config, dataPath, workspaceHost) => {
66
178
  return 'FAILED: ' + errMsg(e);
67
179
  }
68
180
  };
181
+ /**
182
+ * Asked once per parent folder: map it at every start (a project root), so the projects next to this one open in the
183
+ * running sandbox without a restart. The answer is saved to your config.
184
+ */
185
+ const maybeAddRoot = async (ctx, run) => {
186
+ const { projects } = run.config;
187
+ const parent = path.dirname(run.workspaceHost);
188
+ if (!projects.askToAddRoot || ctx.options.yes || !process.stdin.isTTY || !process.stdout.isTTY)
189
+ return;
190
+ if (projects.roots.some((r) => isWithin(r, run.workspaceHost)) || projects.declinedRoots.some((d) => isWithin(d, parent) && isWithin(parent, d)))
191
+ return;
192
+ if (isRootPath(parent))
193
+ return;
194
+ try {
195
+ assertSafeWorkspace(parent, ctx.packageRoot, run.storageRoot);
196
+ assertSafeMapping(parent, ctx.packageRoot, run.storageRoot);
197
+ }
198
+ catch {
199
+ return;
200
+ }
201
+ const answer = await select({
202
+ message: 'Map ' + parent + ' at every start, so other projects there (also new ones) open in the running sandbox without a restart? Agents in the sandbox can then see every project in it.',
203
+ choices: [
204
+ { name: 'Yes, from now on', value: 'yes' },
205
+ { name: 'No (and do not ask again for this folder)', value: 'no' },
206
+ { name: 'Never ask this again', value: 'never' },
207
+ ],
208
+ });
209
+ if (answer === 'yes') {
210
+ addToList(ctx, ['projects', 'roots'], parent);
211
+ run.config.projects.roots.push(parent);
212
+ console.log('Added ' + parent + ' to projects.roots (sandbox projects roots remove ' + parent + ' undoes it).');
213
+ }
214
+ if (answer === 'no')
215
+ addToList(ctx, ['projects', 'declinedRoots'], parent);
216
+ if (answer === 'never')
217
+ setSetting(ctx, ['projects', 'askToAddRoot'], false);
218
+ };
69
219
  /** Everything a real launch does before the sandbox starts (skipped by `generate`, which only writes files). */
70
220
  const prepare = async (ctx, run) => {
71
221
  const { options, dataPath, storageRoot } = ctx;
@@ -77,22 +227,38 @@ const prepare = async (ctx, run) => {
77
227
  const history = syncSummary(config, dataPath, run.workspaceHost);
78
228
  const guarded = checkRepo(dataPath);
79
229
  if (guarded.length)
80
- console.warn('\x1b[33mSECURITY: neutralised ' + guarded.length + ' change(s) the last sandbox made that could run code on this PC (see sandbox status).\x1b[0m');
81
- saveRepoSnapshot(dataPath, run.workspaceHost);
230
+ console.warn(c.yellow('SECURITY: neutralised ' + guarded.length + ' change(s) the last sandbox made that could run code on this PC (see sandbox status).'));
82
231
  resetRunDir(dataPath);
83
232
  return { account, terminal: terminal.message, history, pruned: describePrune(pruneStorage(storageRoot)) };
84
233
  };
85
- export const launch = async (ctx, generateOnly = false) => {
234
+ export const launch = async (ctx, generateOnly = false, extras = {}) => {
86
235
  const { options, loaded, packageRoot, dataPath, storageRoot } = ctx;
87
236
  // attach.auto: an agent started from an interactive terminal attaches it, as with -t.
88
237
  if (loaded.config.attach.auto && !options.noAgent && process.stdin.isTTY && process.stdout.isTTY)
89
238
  options.terminal = true;
90
- if (!generateOnly && isSandboxRunning()) {
91
- if (!options.noAgent)
92
- return launchIntoRunning(ctx);
93
- // Only one Windows Sandbox can run: "start" on a running one brings its window up instead.
94
- console.log('A sandbox is already running' + (showSandboxWindow(storageRoot) ? '; its window is in front now.' : '.') + ' Start an agent in it with sandbox claude (or sandbox <agent>); stop it with sandbox stop.');
95
- return 0;
239
+ if (!generateOnly)
240
+ requireOnPc('launch');
241
+ // agentCommands (Beta) follows the config: a setting changed in the file takes effect here too. Never blocks a launch.
242
+ if (!generateOnly) {
243
+ try {
244
+ syncAgentCommands(ctx, loaded.config);
245
+ }
246
+ catch (e) {
247
+ console.warn('Agent commands (Beta): ' + errMsg(e));
248
+ }
249
+ }
250
+ if (!generateOnly && isSandboxRunning())
251
+ return options.noAgent ? startIntoRunning(ctx) : launchIntoRunning(ctx);
252
+ // Several sessions: the sandbox starts from the first one's folder with the others' folders mapped too, then
253
+ // they all open in it (each project is set up on demand, in parallel). One --session is an ordinary launch.
254
+ const wants = options.sessions;
255
+ if (wants.length === 1)
256
+ return launch(contextFor(ctx, wants[0]), generateOnly, extras);
257
+ if (wants.length > 1) {
258
+ const first = contextFor(ctx, wants[0]);
259
+ const others = _.uniqBy(wants.slice(1).map((w) => w.folder).filter((f) => !isWithin(wants[0].folder, f)), (f) => f.toLowerCase());
260
+ const started = await launch({ ...first, options: { ...first.options, terminal: false } }, generateOnly, { ...extras, extraProjects: [...(extras.extraProjects ?? []), ...others], autoSession: false, waitUntilReady: true });
261
+ return generateOnly || started !== 0 ? started : launchIntoRunning(ctx);
96
262
  }
97
263
  const run = await resolveRun({
98
264
  packageRoot, loaded, cwd: process.cwd(), workspace: options.workspace, harness: options.harness, noAgent: options.noAgent,
@@ -102,45 +268,60 @@ export const launch = async (ctx, generateOnly = false) => {
102
268
  const { config } = run;
103
269
  if (!generateOnly)
104
270
  assertCanLaunch();
271
+ if (!generateOnly && !extras.extraProjects)
272
+ await maybeAddRoot(ctx, run);
105
273
  const prepared = generateOnly ? null : await prepare(ctx, run);
106
- const generated = generateSandbox({ packageRoot, run, harnessArgs: options.passthrough, probe: probeHost(config, run.harnessName) });
107
- for (const note of run.notes)
274
+ const generated = generateSandbox({ packageRoot, run, harnessArgs: options.passthrough, probe: probeHost(config, run.harnessName), extraProjects: extras.extraProjects, autoSession: extras.autoSession });
275
+ for (const note of [...run.notes, ...generated.projects.notes])
108
276
  console.log(note);
109
277
  const secretFiles = config.secrets.warnFiles ? findSecretFiles(run.workspaceHost) : [];
110
- if (secretFiles.length)
111
- console.warn('\x1b[33mHeads-up: the agent can read these files in your workspace, which often hold secrets: ' + secretFiles.slice(0, 12).join(', ') + (secretFiles.length > 12 ? ', ...' : '') + '. Move real credentials out of the folder (or use test ones). Silence with secrets.warnFiles = false.\x1b[0m');
278
+ if (secretFiles.length) {
279
+ const shown = secretFiles.slice(0, 12).join(', ') + (secretFiles.length > 12 ? ', ...' : '');
280
+ console.warn(c.yellow('Heads-up: the agent can read these files in your workspace, which often hold secrets: ' + shown + '. Move real credentials out of the folder (or use test ones). Silence with secrets.warnFiles = false.'));
281
+ }
282
+ const { roots, projects } = generated.projects;
112
283
  table([
113
284
  ['Workspace', run.workspaceHost + ' -> ' + config.guestWorkspace + (config.readOnly ? ' (read-only)' : '')],
114
- ['Agent', run.harnessName + (prepared?.account ? ' (account: ' + prepared.account + ')' : '') + ' Memory: ' + generated.memoryMb + ' MB Network: ' + config.sandbox.networking + (config.sandbox.headless ? ' HEADLESS' : '')],
285
+ ['Agent', run.harnessName + (prepared?.account ? ' (account: ' + prepared.account + ')' : '') + ' Memory: ' + generated.memoryMb + ' MB CPU: ' + (config.sandbox.cpuLimit === 'none' ? 'all' : 'max ' + config.sandbox.cpuLimit + '%') + ' Network: ' + config.sandbox.networking + (config.sandbox.headless ? ' HEADLESS' : '')],
115
286
  ['Node', run.versions.node.spec + ' (' + run.versions.node.source + ') Python: ' + run.versions.python.spec + ' (' + run.versions.python.source + ')'],
116
287
  ...(prepared ? [['Terminal', prepared.terminal], ['History', prepared.history], ['Cleanup', prepared.pruned]] : []),
117
288
  ['Toolchains', Object.entries(generated.toolchains).filter(([, d]) => d.on).map(([name, d]) => name + ' (' + d.reason + ')').join(', ') || 'Node only (nothing else used by this project)'],
118
289
  ...(generated.databases.length
119
- ? generated.databases.map((d, i) => [i ? '' : 'Databases', d.database + (d.database === 'sqlserver' ? ' (cannot run in the sandbox; see hostAccess.forwards)' : ' on port ' + d.port) + ' because ' + d.reasons[0] + (d.reasons.length > 1 ? ' (+' + (d.reasons.length - 1) + ' more)' : '')])
290
+ ? generated.databases.map((d, i) => [i ? '' : 'Databases', d.database + (d.database === 'sqlserver' ? ' (cannot run in the sandbox; use Postgres or SQLite for local work)' : ' on port ' + d.port) + ' because ' + d.reasons[0] + (d.reasons.length > 1 ? ' (+' + (d.reasons.length - 1) + ' more)' : '')])
120
291
  : [['Databases', 'none needed (databases.<name>.enabled = true adds one)']]),
121
292
  ['Desktop', generated.personalization],
122
293
  ['Claude', generated.profile.staged.length ? generated.profile.staged.join(', ') + (generated.profile.credentials ? ' (INCLUDING YOUR LOGIN)' : '') : 'no settings copied'],
123
294
  ['Secrets', !config.secrets.strip ? 'NOT stripped (secrets.strip is off)' : generated.profile.secretsRemoved.length ? 'kept on this PC: ' + generated.profile.secretsRemoved.join(', ') + ' (secrets.allow lets one through)' : 'none found in what is copied'],
124
- ...generated.mappings.slice(3).map((m) => [' map ' + (m.readOnly ? 'ro' : 'rw'), m.host + ' -> ' + m.guest]),
295
+ ...roots.map((r, i) => [i ? '' : 'Project roots', r.host + ' -> ' + r.guest + ' (every project in it opens without a restart)']),
296
+ ...projects.map((p, i) => [i ? '' : 'Projects', p.host + ' -> ' + p.guest]),
297
+ ...generated.mappings.slice(3).filter((m) => ![...roots, ...projects].some((p) => p.guest === m.guest)).map((m) => [' map ' + (m.readOnly ? 'ro' : 'rw'), m.host + ' -> ' + m.guest]),
125
298
  ['Generated', generated.wsbPath],
126
299
  ]);
127
300
  if (generateOnly)
128
301
  return 0;
302
+ // Every folder the sandbox can write is snapshotted before it starts (the guard compares against this), and the
303
+ // projects it starts with are recorded host-side for the commands that run later.
304
+ saveRepoSnapshot(dataPath, run.workspaceHost, projects.map((p) => p.host), roots.map((r) => r.host));
305
+ startRegistry(storageRoot, { launchId: generated.launchId, protections: protectionsOf(config), primary: { host: run.workspaceHost, guest: config.guestWorkspace }, roots, boot: projects });
306
+ rememberProject(storageRoot, run.workspaceHost);
129
307
  startSandbox(generated.wsbPath);
130
308
  // Absolute: the watcher runs from another folder, and a relative --config there would make it exit silently.
131
309
  if (!watcherRunning(dataPath))
132
310
  spawnDetached(ctx.entry, ['_watch', ...(options.config ? ['--config', path.resolve(options.config)] : [])]);
133
311
  // Hiding runs alongside; a failure to find the window must never break the launch or an attach.
134
312
  const hidden = config.sandbox.headless ? hideSandboxWindow(storageRoot).catch(() => false) : null;
135
- if (options.terminal || options.noAgent) {
136
- console.log(firstBoot(dataPath) ? FIRST_BOOT : 'Waiting for the sandbox (everything it needs is installed already; this takes seconds) ...');
137
- await waitForReady(dataPath, generated.launchId, sandboxAlive);
313
+ if (options.terminal || options.noAgent || extras.waitUntilReady) {
314
+ if (firstBoot(dataPath))
315
+ console.log(FIRST_BOOT);
316
+ await waitForBoot(dataPath, generated.launchId);
317
+ if (extras.waitUntilReady)
318
+ return 0;
138
319
  // Without an agent there is no session to attach to: the sandbox itself is the result.
139
320
  if (options.noAgent) {
140
321
  console.log('Sandbox ready. Its window has a terminal in ' + config.guestWorkspace + '; from here: sandbox exec -- <command>, sandbox stop.');
141
322
  return 0;
142
323
  }
143
- return attachHere(ctx, await waitForSession(dataPath, generated.launchId, sandboxAlive));
324
+ return attachHere(ctx, await waitForStart(ctx, generated.launchId, 'Starting ' + run.harnessName));
144
325
  }
145
326
  if (firstBoot(dataPath))
146
327
  console.log(FIRST_BOOT);
@@ -5,9 +5,11 @@ import { pathToFileURL } from 'node:url';
5
5
  import _ from 'lodash';
6
6
  import { DEFAULTS, userConfigPath, userDir } from '../../core/config.js';
7
7
  import { describeOverrides, isTrusted, LOCAL_CONFIG_NAME, normalizeOverrides, readLocalConfig, trustStorePath } from '../../core/local-config.js';
8
- import { fail, safeText, sleep, writeJson } from '../../core/util.js';
8
+ import { expandPath, fail, safeText, sleep, writeJson } from '../../core/util.js';
9
+ import { readKnown, readRegistry } from '../../host/projects.js';
10
+ import { detectWsb } from '../../host/wsb.js';
9
11
  import { describePrune, pruneStorage } from '../../host/cleanup.js';
10
- import { doctorRows, hostClaude, isSandboxRunning } from '../../host/platform.js';
12
+ import { doctorRows, hostClaude, insideSandbox, isSandboxRunning } from '../../host/platform.js';
11
13
  import { readSandboxStatus } from '../../shared/rundir.js';
12
14
  import { syncBack, watch } from '../../sync/watcher.js';
13
15
  import { table } from './context.js';
@@ -44,6 +46,8 @@ export const logs = async (ctx) => {
44
46
  printed = from + buffer.length;
45
47
  };
46
48
  print();
49
+ if (ctx.options.follow)
50
+ console.error('(following; Ctrl+C to stop)');
47
51
  while (ctx.options.follow) {
48
52
  await sleep(1000);
49
53
  print();
@@ -103,8 +107,17 @@ const checkLocalConfig = (ctx) => {
103
107
  return 0;
104
108
  };
105
109
  export const config = (ctx) => {
106
- const [action = 'show'] = ctx.options.positionals;
110
+ const [action = 'summary'] = ctx.options.positionals;
107
111
  switch (action) {
112
+ case 'summary':
113
+ table([
114
+ ['Settings file', ctx.loaded.configPath ?? userConfigPath() + ' (does not exist yet; sandbox config init creates it)'],
115
+ ['Default agent', ctx.loaded.config.defaultHarness],
116
+ ['Storage', ctx.storageRoot],
117
+ ['Memory', String(ctx.loaded.config.sandbox.memoryMb) + (ctx.loaded.config.sandbox.memoryMb === 'auto' ? '' : ' MB')],
118
+ ]);
119
+ console.log('\nChange settings in the menu (sandbox > Settings). All of them as JSON: sandbox config show');
120
+ return 0;
108
121
  case 'path':
109
122
  console.log(ctx.loaded.configPath ?? userConfigPath() + ' (does not exist yet)');
110
123
  return 0;
@@ -118,7 +131,23 @@ export const config = (ctx) => {
118
131
  }
119
132
  };
120
133
  export const doctor = (ctx) => {
121
- table(doctorRows(ctx.loaded.config, ctx.options.harness ?? ctx.loaded.config.defaultHarness, ctx.storageRoot, ctx.loaded.configPath));
134
+ const rows = doctorRows(ctx.loaded.config, ctx.options.harness ?? ctx.loaded.config.defaultHarness, ctx.storageRoot, ctx.loaded.configPath);
135
+ // Inside a sandbox the feature and "running" rows describe the sandbox itself, and the host rows describe nothing useful.
136
+ if (insideSandbox()) {
137
+ table([['Where', 'inside a Windows Sandbox started by this tool (run doctor on your PC to check the PC)'], ...rows.filter(([name]) => name === 'Config file')]);
138
+ return 0;
139
+ }
140
+ // Several projects: the roots mapped at every start, what the running sandbox has, and whether wsb share is there.
141
+ const p = ctx.loaded.config.projects;
142
+ const registry = isSandboxRunning() ? readRegistry(ctx.storageRoot) : null;
143
+ const wsb = p.wsbShare ? detectWsb() : null;
144
+ table([
145
+ ...rows,
146
+ ['Project roots', p.roots.length ? p.roots.map((r) => expandPath(r, ctx.loaded.base)).join(', ') : 'none (sandbox projects roots add <folder>)'],
147
+ ['All projects', p.all ? 'on: ' + readKnown(ctx.storageRoot).length + ' known project(s) mapped at every start (' + p.exclude.length + ' excluded)' : 'off'],
148
+ ['Projects running', registry ? registry.projects.map((x) => x.name + ' (' + x.mechanism + ')').join(', ') : '-'],
149
+ ['wsb share', !p.wsbShare ? 'off (projects.wsbShare)' : wsb ? 'available: ' + wsb.exe + (wsb.version ? ' ' + wsb.version : '') + ' (untested; a failure offers a restart instead)' : 'not on this Windows (needs Windows 11 24H2+ with the updated Windows Sandbox): folders outside your roots need a restart'],
150
+ ]);
122
151
  return 0;
123
152
  };
124
153
  export const claudeHost = (ctx) => hostClaude(ctx.storageRoot, ctx.options.personal, ctx.options.passthrough);
@@ -0,0 +1,29 @@
1
+ import type { SandboxStatus } from '../../core/types.js';
2
+ import { type RegistryEntry } from '../../host/projects.js';
3
+ import type { ProjectSpecT } from '../../shared/schema.js';
4
+ import { type Context } from './context.js';
5
+ /** The folder the command is about: --workspace, else the current one. */
6
+ export declare const hereDir: (ctx: Context) => string;
7
+ export interface Located {
8
+ project: RegistryEntry | null;
9
+ guestDir: string | null;
10
+ }
11
+ /** Where this folder is in the running sandbox, if it is in it at all (nothing is registered). */
12
+ export declare const locateHere: (ctx: Context, status: SandboxStatus | null) => Located;
13
+ export interface Joined {
14
+ project: RegistryEntry | null;
15
+ guestDir: string;
16
+ spec: ProjectSpecT | null;
17
+ harness: string | null;
18
+ notes: string[];
19
+ }
20
+ /**
21
+ * Makes this folder a project of the running sandbox (nothing to do for the launch folder and projects already in
22
+ * it): its .sandbox-config.json is approved like at a launch, its protections are checked, and it is made visible.
23
+ * Throws NeedsRestart (host/projects.ts) when that is impossible without a restart.
24
+ */
25
+ export declare const joinHere: (ctx: Context, status: SandboxStatus) => Promise<Joined>;
26
+ /** Waits until the sandbox has set a project up, with its progress on a spinner (plain lines without a terminal). */
27
+ export declare const provisionJoined: (ctx: Context, joined: Joined) => Promise<void>;
28
+ /** joinHere, then (for another project) waits until the sandbox has set it up. */
29
+ export declare const readyHere: (ctx: Context, status: SandboxStatus) => Promise<Joined>;