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,61 @@
1
+ // `sandbox repair`: finds and fixes common problems. Each check (checks.ts) either confirms all is well (OK), fixes the
2
+ // problem itself (FIXED), or says what you need to do (NEEDS YOU). Toolchain and run-state fixes wait until no
3
+ // sandbox is running.
4
+ import path from 'node:path';
5
+ import _ from 'lodash';
6
+ import { DEFAULTS, loadConfig, userDir } from '../../../core/config.js';
7
+ import { errMsg, expandPath, UserError } from '../../../core/util.js';
8
+ import { isSandboxRunning } from '../../../host/platform.js';
9
+ import { createContext, printJson, table } from '../context.js';
10
+ import { CHECKS } from './checks.js';
11
+ const LABEL = { ok: 'OK', fixed: 'FIXED', 'needs-you': 'NEEDS YOU', warn: 'WARNING', skipped: 'SKIPPED' };
12
+ /** Runs every check in order; a check that throws is reported, never stops the rest. */
13
+ export const runChecks = (env, checks = CHECKS) => checks.map((check) => {
14
+ try {
15
+ return { name: check.name, ...check.run(env) };
16
+ }
17
+ catch (e) {
18
+ const fix = e instanceof UserError && e.fix ? ' ' + e.fix : '';
19
+ return { name: check.name, status: 'needs-you', detail: 'check failed: ' + errMsg(e) + fix };
20
+ }
21
+ });
22
+ export const repair = (ctx, configError = null) => {
23
+ const env = { ctx, running: isSandboxRunning(), dataPath: ctx.dataPath, storageRoot: ctx.storageRoot, toolsDir: path.join(ctx.dataPath, 'tools'), configError };
24
+ // Re-check the config file even when the context loaded (it may have been edited since, or come from --config).
25
+ if (!env.configError) {
26
+ try {
27
+ loadConfig(ctx.options.config);
28
+ }
29
+ catch (e) {
30
+ if (e instanceof UserError)
31
+ env.configError = e;
32
+ else
33
+ throw e;
34
+ }
35
+ }
36
+ const results = runChecks(env);
37
+ if (printJson(ctx, results))
38
+ return results.some((r) => r.status === 'needs-you') ? 1 : 0;
39
+ table(results.map((r) => [LABEL[r.status], r.name, r.detail]));
40
+ const counts = _.countBy(results, 'status');
41
+ console.log('\n' + (counts['needs-you'] ? counts['needs-you'] + ' thing(s) need you (see NEEDS YOU above).' : 'All good' + (counts.fixed ? ': fixed ' + counts.fixed + ' problem(s).' : '.')));
42
+ return counts['needs-you'] ? 1 : 0;
43
+ };
44
+ /**
45
+ * Entry point that works even when the config file is broken (createContext would throw): falls back to the built-in
46
+ * defaults so the other checks still run, and reports the config problem as the first check.
47
+ */
48
+ export const repairCommand = (packageRoot, entry, options) => {
49
+ let ctx;
50
+ try {
51
+ ctx = createContext(packageRoot, entry, options);
52
+ }
53
+ catch (e) {
54
+ if (!(e instanceof UserError))
55
+ throw e;
56
+ const loaded = { config: structuredClone(DEFAULTS), configPath: null, base: userDir() };
57
+ const storageRoot = expandPath(loaded.config.storage.root, loaded.base);
58
+ return repair({ packageRoot, entry, options, loaded, storageRoot, dataPath: path.join(storageRoot, 'data', loaded.config.storage.dataName) }, e);
59
+ }
60
+ return repair(ctx);
61
+ };
@@ -0,0 +1,26 @@
1
+ import type { UserError } from '../../../core/util.js';
2
+ import type { Context } from '../context.js';
3
+ export type RepairStatus = 'ok' | 'fixed' | 'needs-you' | 'warn' | 'skipped';
4
+ export interface RepairResult {
5
+ name: string;
6
+ status: RepairStatus;
7
+ detail: string;
8
+ }
9
+ export interface RepairEnv {
10
+ ctx: Context;
11
+ running: boolean;
12
+ dataPath: string;
13
+ storageRoot: string;
14
+ toolsDir: string;
15
+ configError: UserError | null;
16
+ }
17
+ export interface RepairCheck {
18
+ name: string;
19
+ run: (env: RepairEnv) => Omit<RepairResult, 'name'>;
20
+ }
21
+ export declare const ok: (detail: string) => Omit<RepairResult, "name">;
22
+ /** For fixes that must not touch folders a running sandbox may be using. */
23
+ export declare const whileRunning: {
24
+ status: "skipped";
25
+ detail: string;
26
+ };
@@ -0,0 +1,3 @@
1
+ export const ok = (detail) => ({ status: 'ok', detail });
2
+ /** For fixes that must not touch folders a running sandbox may be using. */
3
+ export const whileRunning = { status: 'skipped', detail: 'a sandbox is running and may be using it; run repair again once it is closed' };
@@ -0,0 +1,6 @@
1
+ import type { SessionStatus } from '../../core/types.js';
2
+ import { type NeedsRestart } from '../../host/projects.js';
3
+ import { type Context } from './context.js';
4
+ /** "2 sessions (claude in api, codex in web)". */
5
+ export declare const describeSessions: (sessions: Pick<SessionStatus, "harness" | "cwd" | "project">[]) => string;
6
+ export declare const offerRestart: (ctx: Context, needs: NeedsRestart[], then: () => Promise<number>) => Promise<number>;
@@ -0,0 +1,85 @@
1
+ // The last way in for a folder the running sandbox cannot show (not in a project root, no `wsb share`): restart it
2
+ // with that folder mapped as well. Only ever with your consent (a prompt, or --yes): it lists what will end, stops
3
+ // the sandbox cleanly (history and claude-mem sync back as on any close), starts it again from the same launch
4
+ // folder with the projects it had plus the new one, reopens the sessions it ended (continuing their conversations
5
+ // where the agent supports it) and then starts the new project's session.
6
+ import path from 'node:path';
7
+ import _ from 'lodash';
8
+ import { askYesNo } from '../../core/prompt.js';
9
+ import { fail, safeText, sleep } from '../../core/util.js';
10
+ import { currentSessions, execInSandbox } from '../../host/channel.js';
11
+ import { isSandboxRunning } from '../../host/platform.js';
12
+ import { readRegistry } from '../../host/projects.js';
13
+ import { hostDirFor, projectForGuestDir } from '../../shared/projects.js';
14
+ import { readSandboxStatus } from '../../shared/rundir.js';
15
+ import { watcherRunning } from '../../sync/watcher.js';
16
+ import { c } from '../colors.js';
17
+ import { working } from '../progress.js';
18
+ import { sandboxAlive } from './context.js';
19
+ /** "2 sessions (claude in api, codex in web)". */
20
+ export const describeSessions = (sessions) => sessions.length === 0 ? 'no sessions' : sessions.length + ' session' + (sessions.length === 1 ? '' : 's') + ' (' + sessions.map((s) => safeText(s.harness) + ' in ' + safeText(s.project ?? path.win32.basename(s.cwd))).join(', ') + ')';
21
+ /** The command to run again with consent, for a non-interactive terminal. */
22
+ const againWithYes = (ctx) => 'sandbox ' + (ctx.options.noAgent ? 'start' : ctx.options.harness ?? ctx.loaded.config.defaultHarness) + (ctx.options.terminal ? ' -t' : '') + ' --yes';
23
+ /** Where each running session is on this PC, so it can be reopened after the restart. */
24
+ const sessionsToReopen = (registry, sessions) => sessions.flatMap((s) => {
25
+ const project = projectForGuestDir([...registry.projects, ...registry.roots.map((r) => ({ ...r }))], s.cwd);
26
+ return project ? [{ harness: s.harness, hostDir: hostDirFor(project, s.cwd) }] : [];
27
+ });
28
+ /** Shuts the sandbox down and waits until it (and the background sync's final pass) is gone. */
29
+ const stopAndWait = async (ctx) => {
30
+ const status = readSandboxStatus(ctx.dataPath);
31
+ await working('Stopping the sandbox (history syncs back)', async () => {
32
+ await execInSandbox(ctx.dataPath, ['C:\\Windows\\System32\\shutdown.exe', '/s', '/t', '0'], status?.guestWorkspace ?? 'C:\\', 30, sandboxAlive).catch(() => null);
33
+ for (let i = 0; i < 180 && isSandboxRunning(); i++)
34
+ await sleep(1000);
35
+ if (isSandboxRunning())
36
+ fail('The sandbox did not shut down within 3 minutes.', 'Close its window yourself, then run the command again.');
37
+ // The watcher syncs history back a few seconds after the window closes; the next start needs it gone.
38
+ for (let i = 0; i < 60 && watcherRunning(ctx.dataPath); i++)
39
+ await sleep(1000);
40
+ });
41
+ };
42
+ export const offerRestart = async (ctx, needs, then) => {
43
+ const folders = needs.map((n) => n.hostDir);
44
+ const registry = readRegistry(ctx.storageRoot) ?? fail('The running sandbox was started by an older version, so it cannot take another folder.', 'Stop it (sandbox stop) and start it again from ' + folders[0] + '.');
45
+ const primary = registry.projects.find((p) => p.primary) ?? fail('The running sandbox has no launch folder on record.', 'Stop it (sandbox stop) and start it again.');
46
+ const sessions = currentSessions(ctx.dataPath);
47
+ for (const need of needs)
48
+ console.log(c.yellow('Windows Sandbox cannot add ' + need.hostDir + ' while it runs: ' + need.reason + '.'));
49
+ console.log('A restart maps ' + (needs.length === 1 ? 'it' : 'them') + ' as well (and keeps ' + registry.projects.map((p) => p.host).join(', ') + '). ' +
50
+ (sessions.length ? describeSessions(sessions) + ' will end' : 'No session is running') + '; your files, tools, databases and logins are kept. It takes about a minute.');
51
+ for (const parent of _.uniq(folders.map((f) => path.dirname(f))))
52
+ console.log(c.dim('No restart next time for folders in ' + parent + ': sandbox projects roots add "' + parent + '"'));
53
+ const interactive = Boolean(process.stdin.isTTY && process.stdout.isTTY);
54
+ if (!ctx.options.yes) {
55
+ if (!interactive)
56
+ fail('Not restarting without your consent.', 'Run it again with --yes to restart: ' + againWithYes(ctx));
57
+ if (!(await askYesNo('Restart the sandbox now?'))) {
58
+ console.log('Nothing changed.');
59
+ return 1;
60
+ }
61
+ }
62
+ const reopen = sessionsToReopen(registry, sessions);
63
+ const resume = reopen.length > 0 && (ctx.options.yes || (await askYesNo('Reopen the ' + reopen.length + ' session(s) afterwards (continuing their conversations where the agent supports it)?', true)));
64
+ await stopAndWait(ctx);
65
+ // Imported here: launch.ts imports this file.
66
+ const { launch, openSession, readySandbox } = await import('./launch.js');
67
+ const { joinHere } = await import('./project.js');
68
+ const keep = registry.projects.filter((p) => !p.primary && p.mechanism !== 'root').map((p) => p.host);
69
+ const relaunch = { ...ctx, options: { ...ctx.options, workspace: primary.host, harness: ctx.options.harness ?? ctx.options.sessions[0]?.harness, terminal: false, noAgent: false, newSession: true, passthrough: [], sessions: [] } };
70
+ await launch(relaunch, false, { extraProjects: [...keep, ...folders], autoSession: false, waitUntilReady: true });
71
+ const ready = await readySandbox(ctx.dataPath);
72
+ if (resume) {
73
+ for (const s of reopen) {
74
+ const at = { ...ctx, options: { ...ctx.options, workspace: s.hostDir, harness: s.harness, terminal: false, newSession: true, passthrough: [], sessions: [] } };
75
+ try {
76
+ const joined = await joinHere(at, ready);
77
+ await openSession(at, ready, joined, { attach: false, harness: s.harness, args: ctx.loaded.config.harnesses[s.harness]?.resumeArguments ?? [], reuse: false });
78
+ }
79
+ catch (e) {
80
+ console.warn(c.yellow('Could not reopen ' + s.harness + ' in ' + s.hostDir + ': ' + (e instanceof Error ? e.message : String(e))));
81
+ }
82
+ }
83
+ }
84
+ return then();
85
+ };
@@ -3,8 +3,9 @@ export declare const attach: (ctx: Context) => Promise<number>;
3
3
  export declare const sessions: (ctx: Context) => Promise<number>;
4
4
  export declare const status: (ctx: Context) => Promise<number>;
5
5
  export declare const exec: (ctx: Context) => Promise<number>;
6
- /** `sandbox db`: runs the sandbox's own `sbx-db`, which knows this project's database versions and ports. */
6
+ /** `sandbox db`: runs the sandbox's own `sbx-db` in this folder's project, which knows its database versions and ports. */
7
7
  export declare const db: (ctx: Context) => Promise<number>;
8
+ /** Restarts this folder's project's sessions (--all: every session). */
8
9
  export declare const restart: (ctx: Context) => Promise<number>;
9
10
  export declare const stop: (ctx: Context) => Promise<number>;
10
11
  export declare const show: (ctx: Context) => number;
@@ -1,110 +1,254 @@
1
- // Working with a running sandbox: attach, sessions, status, exec, restart, stop, show/hide.
1
+ // Working with a running sandbox: attach, sessions, status, exec, restart, stop, show/hide. Each knows which project
2
+ // you run it from (project.ts): attach and sessions put that project's sessions first, exec and db run in it, restart
3
+ // restarts its sessions, stop warns about the other projects' sessions.
4
+ import path from 'node:path';
5
+ import { spawnSync } from 'node:child_process';
6
+ import { select } from '@inquirer/prompts';
2
7
  import _ from 'lodash';
8
+ import { askYesNo } from '../../core/prompt.js';
3
9
  import { activeAccount } from '../../host/accounts.js';
4
- import { execInSandbox, pickSession, pingSandbox, restartSessions, currentSessions, runningSessions, sessionViewers, waitForReady } from '../../host/channel.js';
5
- import { isSandboxRunning } from '../../host/platform.js';
6
- import { repoFindings } from '../../host/repo-guard.js';
10
+ import { execInSandbox, pickSession, pingSandbox, restartSessions, currentSessions, runningSessions, sessionsFirstIn } from '../../host/channel.js';
11
+ import { sessionViewers } from '../../host/attach.js';
12
+ import { insideSandbox, isSandboxRunning } from '../../host/platform.js';
13
+ import { NeedsRestart, readRegistry } from '../../host/projects.js';
14
+ import { guardedProjects, repoFindings } from '../../host/repo-guard.js';
7
15
  import { hideSandboxWindow, showSandboxWindow } from '../../host/window.js';
8
- import { listSessions, readSandboxStatus } from '../../shared/rundir.js';
16
+ import { isWithin, projectForGuestDir } from '../../shared/projects.js';
17
+ import { listProjectStatus, listSessions, readSandboxStatus } from '../../shared/rundir.js';
9
18
  import { watcherRunning } from '../../sync/watcher.js';
10
19
  import { errMsg, fail, safeText, sleep } from '../../core/util.js';
11
- import { currentGuestDir, printJson, requireRunning, sandboxAlive, table } from './context.js';
12
- import { attachHere } from './launch.js';
20
+ import { printJson, requireOnPc, requireRunning, sandboxAlive, table } from './context.js';
21
+ import { attachHere, readySandbox } from './launch.js';
22
+ import { hereDir, joinHere, locateHere, provisionJoined } from './project.js';
23
+ import { working } from '../progress.js';
24
+ import { ago, c, shortenMiddle, termWidth } from '../colors.js';
25
+ const viewerText = (v) => (v === undefined ? '-' : v === null ? 'unknown (older session)' : v.remote + ' here, ' + (v.viewers - v.remote) + ' in sandbox');
26
+ const projectOf = (projects, s) => s.project ?? projectForGuestDir(projects, s.cwd)?.name ?? '-';
13
27
  export const attach = async (ctx) => {
28
+ requireOnPc('attach');
14
29
  requireRunning('Start one with: sandbox claude --terminal');
15
- await waitForReady(ctx.dataPath, null, sandboxAlive);
30
+ const ready = await readySandbox(ctx.dataPath);
16
31
  const id = ctx.options.positionals[0];
17
32
  if (!id && runningSessions(ctx.dataPath).length === 0)
18
33
  await sleep(1500);
19
- return attachHere(ctx, pickSession(ctx.dataPath, id));
34
+ const here = locateHere(ctx, ready);
35
+ if (id)
36
+ return attachHere(ctx, pickSession(ctx.dataPath, id));
37
+ // Several sessions: choose, this folder's project first (the newest is the default).
38
+ const running = sessionsFirstIn(runningSessions(ctx.dataPath), here.guestDir);
39
+ if (running.length > 1 && process.stdin.isTTY && process.stdout.isTTY) {
40
+ const projects = readRegistry(ctx.storageRoot)?.projects ?? [];
41
+ const chosen = await select({
42
+ message: 'Which session?' + (here.project ? ' (' + here.project.name + ' first)' : ''),
43
+ choices: running.map((s) => ({ value: s.id, name: (here.guestDir && isWithin(here.guestDir, s.cwd) ? '* ' : ' ') + safeText(s.harness).padEnd(9) + ' ' + projectOf(projects, s).padEnd(16) + ' ' + ago(s.startedAt).padEnd(11) + ' ' + s.id })),
44
+ });
45
+ return attachHere(ctx, pickSession(ctx.dataPath, chosen));
46
+ }
47
+ return attachHere(ctx, pickSession(ctx.dataPath, undefined, here.guestDir));
20
48
  };
21
- const viewerText = (v) => (v === undefined ? '-' : v === null ? 'unknown (older session)' : v.remote + ' here, ' + (v.viewers - v.remote) + ' in sandbox');
22
- // harness, cwd and times are written by the sandbox: printed through safeText.
23
- const sessionRow = (s) => [s.id, safeText(s.harness), s.phase + (s.exitCode !== undefined ? ' (' + s.exitCode + ')' : ''), viewerText(s.viewers), safeText(s.cwd), safeText(new Date(s.startedAt).toLocaleTimeString())];
24
- export const sessions = async (ctx) => {
49
+ /** Sessions with who is watching them; `here` marks the ones in the project of the current folder. */
50
+ const listedSessions = async (ctx) => {
25
51
  // Who is attached comes from each session itself, so a terminal that was closed is never counted.
26
52
  const live = isSandboxRunning();
27
53
  // Only this sandbox's sessions are asked (and at most MAX_QUERIED_SESSIONS): the sandbox writes these files.
28
54
  const ask = new Set(live ? currentSessions(ctx.dataPath).map((s) => s.id) : []);
29
- const list = await Promise.all(listSessions(ctx.dataPath).map(async (s) => (ask.has(s.id) ? { ...s, viewers: await sessionViewers(s) } : s)));
55
+ const here = live ? locateHere(ctx, readSandboxStatus(ctx.dataPath)) : { guestDir: null };
56
+ const projects = readRegistry(ctx.storageRoot)?.projects ?? [];
57
+ return Promise.all(listSessions(ctx.dataPath).map(async (s) => ({
58
+ ...s, projectName: projectOf(projects, s), here: Boolean(here.guestDir && isWithin(here.guestDir, s.cwd)),
59
+ ...(ask.has(s.id) ? { viewers: await sessionViewers(s) } : {}),
60
+ })));
61
+ };
62
+ export const sessions = async (ctx) => {
63
+ const list = await listedSessions(ctx);
30
64
  if (printJson(ctx, list.map((s) => _.omit(s, 'attach'))))
31
65
  return 0;
32
- if (list.length === 0)
33
- console.log('No sessions.');
34
- else
35
- table([['ID', 'AGENT', 'STATE', 'ATTACHED', 'FOLDER', 'STARTED'], ...list.map(sessionRow)]);
66
+ if (list.length === 0) {
67
+ console.log('No sessions. Start one with: sandbox claude');
68
+ return 0;
69
+ }
70
+ // This folder's project first, newest first within each group; harness, cwd and times are written by the sandbox.
71
+ const ordered = [..._.sortBy(list.filter((s) => s.here), (s) => s.startedAt).reverse(), ..._.sortBy(list.filter((s) => !s.here), (s) => s.startedAt).reverse()];
72
+ const folderWidth = Math.max(16, termWidth() - 80);
73
+ table([['', 'ID', 'AGENT', 'PROJECT', 'STATE', 'ATTACHED', 'FOLDER', 'STARTED'], ...ordered.map((s) => [
74
+ s.here ? '*' : '', s.id, safeText(s.harness), safeText(s.projectName ?? '-'), s.phase + (s.exitCode !== undefined ? ' (' + s.exitCode + ')' : ''), viewerText(s.viewers), shortenMiddle(safeText(s.cwd), folderWidth), ago(s.startedAt),
75
+ ])]);
76
+ if (list.some((s) => s.here))
77
+ console.log(c.dim('* = the project of this folder'));
36
78
  return 0;
37
79
  };
80
+ /** Running databases by port, from the sandbox's own `sbx-db list` (best effort: unknown when it does not answer). */
81
+ const runningPorts = async (ctx, cwd) => {
82
+ try {
83
+ const r = await execInSandbox(ctx.dataPath, ['sbx-db', 'list'], cwd, 30, sandboxAlive);
84
+ return new Set((r.stdout ?? '').split(/\r?\n/).filter((l) => /\brunning\b/.test(l)).map((l) => Number(/port (\d+)/.exec(l)?.[1])).filter((n) => n > 0));
85
+ }
86
+ catch {
87
+ return null;
88
+ }
89
+ };
38
90
  export const status = async (ctx) => {
39
91
  const running = isSandboxRunning();
40
92
  const sandbox = readSandboxStatus(ctx.dataPath);
93
+ const registry = running ? readRegistry(ctx.storageRoot) : null;
94
+ const ready = running && sandbox?.phase === 'ready';
95
+ const daemon = ready ? await pingSandbox(ctx.dataPath, sandboxAlive).then((r) => r.message, (e) => 'no answer (' + errMsg(e) + ')') : '-';
96
+ const here = running ? locateHere(ctx, sandbox) : { project: null, guestDir: null };
97
+ const sessionList = running ? await listedSessions(ctx) : [];
98
+ const provisioning = running ? listProjectStatus(ctx.dataPath) : [];
99
+ const ports = ready && sandbox ? await runningPorts(ctx, sandbox.guestWorkspace) : null;
41
100
  const findings = repoFindings(ctx.dataPath);
42
- const daemon = running && sandbox?.phase === 'ready' ? await pingSandbox(ctx.dataPath, sandboxAlive).then((r) => r.message, (e) => 'no answer (' + errMsg(e) + ')') : '-';
101
+ const startedAt = Number(/^(\d+)-/.exec(registry?.launchId ?? sandbox?.launchId ?? '')?.[1]) || null;
102
+ const projects = (registry?.projects ?? []).map((p) => {
103
+ const prov = provisioning.find((s) => s.name.toLowerCase() === p.name.toLowerCase());
104
+ const own = sessionList.filter((s) => s.phase === 'running' && projectForGuestDir(registry?.projects ?? [], s.cwd)?.name === p.name);
105
+ return {
106
+ name: p.name, host: p.host, guest: p.guest, mechanism: p.mechanism, here: here.project?.name === p.name,
107
+ setup: prov ? { phase: prov.phase, message: safeText(prov.message) } : { phase: 'not set up yet', message: 'set up when you first start a session or command there' },
108
+ databases: (prov?.databases ?? []).map((d) => ({ database: safeText(d.database), port: d.port, state: ports ? (ports.has(d.port) ? 'running' : 'stopped') : 'unknown' })),
109
+ sessions: own.map((s) => ({ id: s.id, harness: safeText(s.harness), viewers: viewerText(s.viewers), started: s.startedAt })),
110
+ // With several guarded folders each finding starts with its folder.
111
+ repoGuard: findings.filter((f) => f.detail.toLowerCase().startsWith(p.host.toLowerCase() + ':')).map((f) => f.kind + ': ' + safeText(f.detail)),
112
+ };
113
+ });
43
114
  const info = {
44
- running, phase: running ? sandbox?.phase ?? 'unknown' : 'stopped', message: sandbox?.message, workspace: sandbox?.workspaceHost, headless: sandbox?.headless,
45
- daemon, account: activeAccount(ctx.dataPath), sessions: running ? runningSessions(ctx.dataPath).length : 0, syncWatcher: watcherRunning(ctx.dataPath),
46
- repoGuard: findings.length ? findings.map((x) => x.kind + ': ' + x.detail).join('; ') : 'nothing from the sandbox needed neutralising',
115
+ sandbox: {
116
+ running, phase: running ? sandbox?.phase ?? 'unknown' : 'stopped', message: sandbox?.message, headless: sandbox?.headless,
117
+ uptime: running && startedAt ? ago(startedAt).replace(' ago', '') : null, daemon, account: activeAccount(ctx.dataPath), syncWatcher: watcherRunning(ctx.dataPath),
118
+ },
119
+ here: { folder: hereDir(ctx), project: here.project?.name ?? null, guest: here.guestDir },
120
+ roots: registry?.roots ?? [],
121
+ projects,
122
+ otherSessions: sessionList.filter((s) => s.phase === 'running' && !projects.some((p) => p.sessions.some((x) => x.id === s.id))).map((s) => ({ id: s.id, harness: safeText(s.harness), cwd: safeText(s.cwd) })),
123
+ repoGuard: { guarded: guardedProjects(ctx.dataPath).length, findings: findings.map((f) => f.kind + ': ' + f.detail) },
47
124
  };
48
- if (!printJson(ctx, info))
49
- table(Object.entries(info).map(([k, v]) => [_.startCase(k), safeText(String(v ?? '-'))]));
125
+ if (printJson(ctx, info))
126
+ return 0;
127
+ const yesNo = (v) => (typeof v === 'boolean' ? (v ? 'yes' : 'no') : String(v ?? '-'));
128
+ table(Object.entries(info.sandbox).map(([k, v]) => [_.startCase(k), safeText(yesNo(v))]));
129
+ if (!running)
130
+ return 0;
131
+ console.log('\n' + c.bold('Projects') + c.dim(' (* = this folder: ' + info.here.folder + (info.here.project ? '' : ', not in this sandbox') + ')'));
132
+ for (const p of projects) {
133
+ console.log((p.here ? '* ' : ' ') + c.bold(p.name) + ' ' + p.host + ' -> ' + p.guest + c.dim(' (' + (p.mechanism === 'launch' ? 'started from here' : p.mechanism === 'root' ? 'in a project root' : p.mechanism === 'boot' ? 'mapped at start' : 'wsb share') + ')'));
134
+ console.log(' setup: ' + p.setup.phase + (p.setup.phase === 'ready' ? '' : ' - ' + shortenMiddle(p.setup.message, termWidth() - 20)));
135
+ if (p.databases.length)
136
+ console.log(' databases: ' + p.databases.map((d) => d.database + ' :' + d.port + ' ' + d.state).join(', '));
137
+ for (const s of p.sessions)
138
+ console.log(' session ' + s.id + ' ' + s.harness + ' ' + s.viewers + ' ' + ago(s.started));
139
+ for (const f of p.repoGuard)
140
+ console.log(' ' + c.yellow('guard: ' + f));
141
+ }
142
+ for (const r of info.roots)
143
+ console.log(' root ' + r.host + ' -> ' + r.guest + c.dim(' (every project in it opens without a restart)'));
144
+ for (const s of info.otherSessions)
145
+ console.log(' session ' + s.id + ' ' + s.harness + ' ' + s.cwd);
146
+ console.log('\n' + c.bold('Repo guard') + ' ' + info.repoGuard.guarded + ' folder(s) guarded; ' + (findings.length ? c.yellow(findings.length + ' change(s) neutralised:') : 'nothing from the sandbox needed neutralising'));
147
+ for (const f of info.repoGuard.findings.slice(-10))
148
+ console.log(' ' + c.yellow(safeText(f)));
50
149
  return 0;
51
150
  };
151
+ /** What a command run in the sandbox printed (text from the sandbox: through safeText), or why it failed. */
152
+ const printExecResult = (result) => {
153
+ if (result.stdout)
154
+ process.stdout.write(safeText(result.stdout));
155
+ if (result.stderr)
156
+ process.stderr.write(safeText(result.stderr));
157
+ if (!result.ok && !result.stdout && !result.stderr)
158
+ console.error(result.message);
159
+ };
160
+ /** The guest folder for this folder, joining the running sandbox (and setting the project up) when needed. */
161
+ const guestFolderHere = async (ctx, what) => {
162
+ const ready = await readySandbox(ctx.dataPath);
163
+ try {
164
+ const joined = await joinHere(ctx, ready);
165
+ for (const note of joined.notes)
166
+ console.error(note);
167
+ await provisionJoined(ctx, joined);
168
+ return joined.guestDir;
169
+ }
170
+ catch (e) {
171
+ if (!(e instanceof NeedsRestart))
172
+ throw e;
173
+ return fail(hereDir(ctx) + ' is not in the running sandbox (' + e.reason + '), so "sandbox ' + what + '" cannot run there.', 'sandbox claude (or sandbox start) from that folder offers a restart that adds it; or add its parent folder as a root: sandbox projects roots add "' + path.dirname(e.hostDir) + '"');
174
+ }
175
+ };
52
176
  export const exec = async (ctx) => {
53
177
  const command = [...ctx.options.positionals, ...ctx.options.passthrough];
54
178
  if (command.length === 0)
55
179
  fail('No command given.', 'Usage: sandbox exec [--timeout <s>] -- <command> [args...]');
180
+ requireOnPc('exec');
56
181
  requireRunning();
57
- const ready = await waitForReady(ctx.dataPath, null, sandboxAlive);
58
- const result = await execInSandbox(ctx.dataPath, command, currentGuestDir(ctx, ready.workspaceHost, ready.guestWorkspace), ctx.options.timeout, sandboxAlive);
59
- if (!printJson(ctx, result)) {
60
- if (result.stdout)
61
- process.stdout.write(safeText(result.stdout));
62
- if (result.stderr)
63
- process.stderr.write(safeText(result.stderr));
64
- if (!result.ok && !result.stdout && !result.stderr)
65
- console.error(result.message);
66
- }
182
+ const cwd = await guestFolderHere(ctx, 'exec');
183
+ const result = await execInSandbox(ctx.dataPath, command, cwd, ctx.options.timeout, sandboxAlive);
184
+ if (!printJson(ctx, result))
185
+ printExecResult(result);
67
186
  return result.exitCode ?? 1;
68
187
  };
69
- /** `sandbox db`: runs the sandbox's own `sbx-db`, which knows this project's database versions and ports. */
188
+ /** `sandbox db`: runs the sandbox's own `sbx-db` in this folder's project, which knows its database versions and ports. */
70
189
  export const db = async (ctx) => {
71
190
  const [action = 'list', name] = ctx.options.positionals;
191
+ if (!['list', 'start', 'stop'].includes(action))
192
+ fail('Unknown db action "' + action + '".', 'Use: sandbox db (list) or sandbox db start|stop <postgres|mysql|mongodb|redis|dynamodb>');
72
193
  if (action !== 'list' && !name)
73
194
  fail('Which database?', 'Usage: sandbox db ' + action + ' <postgres|mysql|mongodb|redis|dynamodb>');
195
+ // Already inside the sandbox: run the database tool here instead of asking the sandbox from outside.
196
+ if (insideSandbox())
197
+ return spawnSync(process.execPath, [path.join(ctx.packageRoot, 'dist', 'runtime', 'db.js'), action, ...(name ? [name] : [])], { stdio: 'inherit', windowsHide: true }).status ?? 1;
74
198
  requireRunning('Start one with: sandbox start --database <name>');
75
- const ready = await waitForReady(ctx.dataPath, null, sandboxAlive);
199
+ const cwd = await guestFolderHere(ctx, 'db');
76
200
  // Starting can download the database the first time.
77
- const result = await execInSandbox(ctx.dataPath, ['sbx-db', action, ...(name ? [name] : [])], ready.guestWorkspace, 1800, sandboxAlive);
78
- if (result.stdout)
79
- process.stdout.write(safeText(result.stdout));
80
- if (result.stderr)
81
- process.stderr.write(safeText(result.stderr));
82
- if (!result.ok && !result.stdout && !result.stderr)
83
- console.error(result.message);
201
+ const result = await working((action === 'list' ? 'Asking' : action === 'start' ? 'Starting' : 'Stopping') + ' the database' + (name ? ' ' + name : 's') + ' in the sandbox', () => execInSandbox(ctx.dataPath, ['sbx-db', action, ...(name ? [name] : [])], cwd, 1800, sandboxAlive));
202
+ printExecResult(result);
84
203
  return result.exitCode ?? 1;
85
204
  };
205
+ /** Restarts this folder's project's sessions (--all: every session). */
86
206
  export const restart = async (ctx) => {
207
+ requireOnPc('restart');
87
208
  requireRunning();
88
- console.log((await restartSessions(ctx.dataPath, ctx.options.harness ?? null, sandboxAlive)).message);
209
+ const here = locateHere(ctx, readSandboxStatus(ctx.dataPath));
210
+ const scope = ctx.options.all || !here.project ? undefined : here.project.guest;
211
+ if (!scope && !ctx.options.all)
212
+ console.log(c.dim(hereDir(ctx) + ' is not a project of the running sandbox: restarting every session.'));
213
+ console.log((await working('Restarting the agents' + (scope ? ' of ' + here.project?.name : ''), () => restartSessions(ctx.dataPath, ctx.options.harness ?? null, sandboxAlive, scope))).message);
89
214
  return 0;
90
215
  };
91
216
  export const stop = async (ctx) => {
217
+ requireOnPc('stop');
92
218
  if (!isSandboxRunning()) {
93
219
  console.log('No Windows Sandbox is running.');
94
220
  return 0;
95
221
  }
96
222
  const sandbox = readSandboxStatus(ctx.dataPath) ?? fail('This sandbox was not started by this tool.', 'Close its window instead.');
97
- await execInSandbox(ctx.dataPath, ['C:\\Windows\\System32\\shutdown.exe', '/s', '/t', '2'], sandbox.guestWorkspace, 30, sandboxAlive);
223
+ // Stopping ends every project's sessions, not just this folder's: say which, and ask when others would end too.
224
+ const here = locateHere(ctx, sandbox);
225
+ const running = currentSessions(ctx.dataPath);
226
+ const others = running.filter((s) => !(here.guestDir && isWithin(here.project?.guest ?? here.guestDir, s.cwd)));
227
+ if (running.length)
228
+ console.log('Stopping ends ' + running.length + ' session(s): ' + running.map((s) => safeText(s.harness) + ' in ' + safeText(s.project ?? path.win32.basename(s.cwd))).join(', ') + '.');
229
+ if (others.length && !ctx.options.yes) {
230
+ // A script must never end other projects' sessions by accident: without a terminal to ask in, it needs --yes.
231
+ const interactive = process.stdin.isTTY && process.stdout.isTTY;
232
+ if (!interactive)
233
+ fail(others.length + ' of these sessions belong to other projects, so the sandbox was not stopped.', 'Stop it anyway with: sandbox stop --yes');
234
+ if (!(await askYesNo(others.length + ' of them belong to other projects. Stop the whole sandbox?'))) {
235
+ console.log('Nothing changed.');
236
+ return 1;
237
+ }
238
+ }
239
+ await working('Asking the sandbox to shut down', () => execInSandbox(ctx.dataPath, ['C:\\Windows\\System32\\shutdown.exe', '/s', '/t', '2'], sandbox.guestWorkspace, 30, sandboxAlive));
98
240
  console.log('Sandbox is shutting down; history and claude-mem sync back when it closes.');
99
241
  return 0;
100
242
  };
101
243
  export const show = (ctx) => {
244
+ requireOnPc('show');
102
245
  requireRunning();
103
246
  if (!showSandboxWindow(ctx.storageRoot))
104
247
  fail('Could not find the Windows Sandbox window.', 'It may still be starting; try again in a few seconds.');
105
248
  return 0;
106
249
  };
107
250
  export const hide = async (ctx) => {
251
+ requireOnPc('hide');
108
252
  requireRunning();
109
253
  if (!(await hideSandboxWindow(ctx.storageRoot, 5)))
110
254
  fail('Could not find a visible Windows Sandbox window.', 'It may already be hidden (sbx-show brings it back).');
@@ -0,0 +1,13 @@
1
+ import { type EditionSupport, type MachineFacts } from './facts.js';
2
+ export type CheckStatus = 'ok' | 'warn' | 'action';
3
+ export interface Check {
4
+ name: string;
5
+ status: CheckStatus;
6
+ detail: string;
7
+ }
8
+ /** Windows Sandbox needs Pro, Enterprise or Education (build 18305+); Home ("Core*") and Server cannot run it. */
9
+ export declare const editionSupport: (edition: string | null) => EditionSupport;
10
+ export declare const nodeMajor: (version: string) => number;
11
+ /** Offer to enable the feature only when it is off and this edition can have it. */
12
+ export declare const canEnableFeature: (f: MachineFacts) => boolean;
13
+ export declare const buildChecks: (f: MachineFacts) => Check[];
@@ -0,0 +1,71 @@
1
+ // Turns what `sandbox setup` learnt about the PC into a checklist. Pure, so it can be tested.
2
+ import { GB } from '../../../core/disk.js';
3
+ import { ENABLE_COMMAND } from './facts.js';
4
+ /** Windows Sandbox needs Pro, Enterprise or Education (build 18305+); Home ("Core*") and Server cannot run it. */
5
+ export const editionSupport = (edition) => {
6
+ if (!edition)
7
+ return 'unknown';
8
+ if (/^Core/i.test(edition))
9
+ return 'home';
10
+ if (/^Server/i.test(edition))
11
+ return 'unsupported';
12
+ return /^(Professional|Enterprise|Education|IoTEnterprise)/i.test(edition) ? 'supported' : 'unknown';
13
+ };
14
+ export const nodeMajor = (version) => Number(/^v?(\d+)/.exec(version.trim())?.[1] ?? 0);
15
+ /** Offer to enable the feature only when it is off and this edition can have it. */
16
+ export const canEnableFeature = (f) => f.feature !== 'enabled' && f.feature !== 'pending' && !f.sandboxExe && ['supported', 'unknown'].includes(editionSupport(f.edition));
17
+ const formatGb = (bytes) => (bytes / GB).toFixed(1) + ' GB';
18
+ const check = (name, status, detail) => ({ name, status, detail });
19
+ const editionCheck = (f) => {
20
+ const support = editionSupport(f.edition);
21
+ const edition = (f.edition ?? 'unknown edition') + (f.build ? ', build ' + f.build : '');
22
+ const name = 'Windows edition';
23
+ if (support === 'supported' && (f.build ?? 18305) >= 18305)
24
+ return check(name, 'ok', edition);
25
+ if (support === 'home')
26
+ return check(name, 'action', edition + ': Windows Home cannot run Windows Sandbox. Upgrade to Pro (Settings > System > Activation).');
27
+ if (support === 'unsupported')
28
+ return check(name, 'action', edition + ': Windows Sandbox needs Windows 10/11 Pro, Enterprise or Education.');
29
+ if (support === 'supported')
30
+ return check(name, 'action', edition + ': too old; Windows Sandbox needs Windows 10 version 1903 or newer.');
31
+ return check(name, 'warn', edition + ': not sure this edition has Windows Sandbox (it needs Pro, Enterprise or Education).');
32
+ };
33
+ const virtualizationCheck = (f) => {
34
+ const name = 'CPU virtualization';
35
+ const virt = f.hypervisorPresent || f.virtualizationFirmware;
36
+ if (virt)
37
+ return check(name, 'ok', f.hypervisorPresent ? 'on (hypervisor running)' : 'on');
38
+ if (virt === false)
39
+ return check(name, 'action', 'off. Turn on Intel VT-x / AMD-V (SVM) in your PC\'s BIOS/UEFI settings, then run this again.');
40
+ return check(name, 'warn', 'could not tell; if Windows Sandbox will not start, turn on VT-x / AMD-V in the BIOS/UEFI.');
41
+ };
42
+ const featureCheck = (f) => {
43
+ const name = 'Windows Sandbox feature';
44
+ if (f.feature === 'enabled' && f.sandboxExe)
45
+ return check(name, 'ok', 'enabled');
46
+ if (f.feature === 'pending' || f.feature === 'enabled')
47
+ return check(name, 'action', 'enabled, but Windows needs a restart to finish.');
48
+ if (f.sandboxExe)
49
+ return check(name, 'ok', 'installed');
50
+ return check(name, 'action', 'not enabled. In an admin PowerShell: ' + ENABLE_COMMAND + ', then restart.');
51
+ };
52
+ const memoryCheck = (f) => {
53
+ const gb = formatGb(f.totalRamBytes);
54
+ if (f.totalRamBytes < 4 * GB)
55
+ return check('Memory', 'action', gb + ': Windows Sandbox needs at least 4 GB.');
56
+ if (f.totalRamBytes < 8 * GB)
57
+ return check('Memory', 'warn', gb + ': works, but 8 GB or more is recommended.');
58
+ return check('Memory', 'ok', gb);
59
+ };
60
+ const diskCheck = (f) => {
61
+ if (f.freeDiskBytes === null)
62
+ return check('Free disk', 'warn', 'could not check the drive of ' + f.storageRoot);
63
+ const free = formatGb(f.freeDiskBytes) + ' free for ' + f.storageRoot;
64
+ if (f.freeDiskBytes < 5 * GB)
65
+ return check('Free disk', 'action', free + ': free up space (toolchains need several GB).');
66
+ if (f.freeDiskBytes < 20 * GB)
67
+ return check('Free disk', 'warn', free + ': getting low; toolchains and caches can use several GB.');
68
+ return check('Free disk', 'ok', free);
69
+ };
70
+ const nodeCheck = (f) => nodeMajor(f.nodeVersion) >= 20 ? check('Node.js', 'ok', f.nodeVersion) : check('Node.js', 'action', f.nodeVersion + ': needs 20 or newer (winget install OpenJS.NodeJS.LTS).');
71
+ export const buildChecks = (f) => [editionCheck(f), virtualizationCheck(f), featureCheck(f), memoryCheck(f), diskCheck(f), nodeCheck(f)];
@@ -1,11 +1,4 @@
1
1
  import { z } from 'zod';
2
- import { type Context } from './context.js';
3
- export type CheckStatus = 'ok' | 'warn' | 'action';
4
- export interface Check {
5
- name: string;
6
- status: CheckStatus;
7
- detail: string;
8
- }
9
2
  export type FeatureState = 'enabled' | 'pending' | 'disabled' | 'unknown';
10
3
  export type EditionSupport = 'supported' | 'home' | 'unsupported' | 'unknown';
11
4
  export interface MachineFacts {
@@ -24,22 +17,15 @@ export interface MachineFacts {
24
17
  export declare const ENABLE_COMMAND: string;
25
18
  /** `reg query <key>` output to { valueName: data }. */
26
19
  export declare const parseRegQuery: (text: string) => Record<string, string>;
27
- /** Windows Sandbox needs Pro, Enterprise or Education (build 18305+); Home ("Core*") and Server cannot run it. */
28
- export declare const editionSupport: (edition: string | null) => EditionSupport;
29
20
  /** The "State : ..." line of `Dism /Get-FeatureInfo` (English output only; anything else is unknown). */
30
21
  export declare const parseDismState: (text: string) => FeatureState;
31
22
  /** Win32_OptionalFeature.InstallState: 1 enabled, 2 disabled, 3 absent. */
32
23
  export declare const featureFromInstallState: (state: number | null | undefined) => FeatureState;
33
- export declare const nodeMajor: (version: string) => number;
34
- /** Offer to enable the feature only when it is off and this edition can have it. */
35
- export declare const canEnableFeature: (f: MachineFacts) => boolean;
36
- /** Turns what we learnt about the PC into the checklist (pure, so it can be tested). */
37
- export declare const buildChecks: (f: MachineFacts) => Check[];
38
24
  declare const Probe: z.ZodObject<{
39
25
  virtualizationFirmware: z.ZodOptional<z.ZodNullable<z.ZodBoolean>>;
40
26
  hypervisorPresent: z.ZodOptional<z.ZodNullable<z.ZodBoolean>>;
41
27
  installState: z.ZodOptional<z.ZodNullable<z.ZodNumber>>;
42
28
  }, z.core.$strip>;
43
29
  export declare const parseProbe: (text: string) => z.infer<typeof Probe>;
44
- export declare const setup: (ctx: Context) => Promise<number>;
30
+ export declare const gatherFacts: (storageRoot: string) => MachineFacts;
45
31
  export {};