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,63 @@
1
+ // Which project of the running sandbox a command is run from. Every command that works on a running sandbox uses
2
+ // these, so they all agree: `locateHere` only looks (attach, sessions, status, restart), `joinHere` also makes the
3
+ // folder a project of the running sandbox when it can without a restart (launch, start, exec, db).
4
+ import path from 'node:path';
5
+ import { askYesNo } from '../../core/prompt.js';
6
+ import { isInsideOrEqual } from '../../core/util.js';
7
+ import { resolveRun } from '../../core/workspace.js';
8
+ import { prepareProject } from '../../host/channel.js';
9
+ import { projectHere, readRegistry } from '../../host/projects.js';
10
+ import { joinRunning } from '../../host/join.js';
11
+ import { working } from '../progress.js';
12
+ import { guestPathFor, sandboxAlive } from './context.js';
13
+ /** The folder the command is about: --workspace, else the current one. */
14
+ export const hereDir = (ctx) => path.resolve(ctx.options.workspace ?? process.cwd());
15
+ /** Where this folder is in the running sandbox, if it is in it at all (nothing is registered). */
16
+ export const locateHere = (ctx, status) => {
17
+ const dir = hereDir(ctx);
18
+ const here = projectHere(ctx.storageRoot, dir);
19
+ if (here)
20
+ return here;
21
+ // A sandbox started before projects existed: only its launch folder.
22
+ if (!readRegistry(ctx.storageRoot) && status && isInsideOrEqual(status.workspaceHost, dir))
23
+ return { project: null, guestDir: guestPathFor(dir, status.workspaceHost, status.guestWorkspace) };
24
+ return { project: null, guestDir: null };
25
+ };
26
+ /**
27
+ * Makes this folder a project of the running sandbox (nothing to do for the launch folder and projects already in
28
+ * it): its .sandbox-config.json is approved like at a launch, its protections are checked, and it is made visible.
29
+ * Throws NeedsRestart (host/projects.ts) when that is impossible without a restart.
30
+ */
31
+ export const joinHere = async (ctx, status) => {
32
+ const { options, loaded, packageRoot, storageRoot, dataPath } = ctx;
33
+ const dir = hereDir(ctx);
34
+ // A sandbox started before projects existed: only its launch folder (guestPathFor explains why otherwise).
35
+ if (!readRegistry(storageRoot))
36
+ return { project: null, guestDir: guestPathFor(dir, status.workspaceHost, status.guestWorkspace), spec: null, harness: null, notes: [] };
37
+ const joined = await joinRunning({
38
+ storageRoot, dataPath, packageRoot, hostDir: dir, wsbShare: loaded.config.projects.wsbShare,
39
+ resolve: (root) => resolveRun({
40
+ packageRoot, loaded, cwd: root, workspace: root, harness: options.harness, noAgent: options.noAgent,
41
+ noLocalConfig: options.noLocalConfig, trustConfig: options.trustConfig, scripts: options.scripts, databases: options.databases,
42
+ nodeVersion: options.node, pythonVersion: options.python, ask: askYesNo,
43
+ }),
44
+ });
45
+ const harness = joined.run && joined.run.harnessName !== 'none' ? joined.run.harnessName : null;
46
+ return { project: joined.project, guestDir: joined.guestDir, spec: joined.spec, harness, notes: joined.notes };
47
+ };
48
+ /** Waits until the sandbox has set a project up, with its progress on a spinner (plain lines without a terminal). */
49
+ export const provisionJoined = async (ctx, joined) => {
50
+ if (!joined.spec)
51
+ return;
52
+ const spec = joined.spec;
53
+ let step = 'starting';
54
+ await working(() => 'Setting up ' + spec.name + ' in the running sandbox: ' + step, () => prepareProject(ctx.dataPath, spec, sandboxAlive, (m) => { step = m; }));
55
+ };
56
+ /** joinHere, then (for another project) waits until the sandbox has set it up. */
57
+ export const readyHere = async (ctx, status) => {
58
+ const joined = await joinHere(ctx, status);
59
+ for (const note of joined.notes)
60
+ console.log(note);
61
+ await provisionJoined(ctx, joined);
62
+ return joined;
63
+ };
@@ -0,0 +1,14 @@
1
+ import { type Context } from './context.js';
2
+ interface Row {
3
+ folder: string;
4
+ here: boolean;
5
+ known: boolean;
6
+ root: string | null;
7
+ excluded: boolean;
8
+ mappedAtStart: boolean;
9
+ running: string | null;
10
+ }
11
+ /** Every project worth listing: known ones, the running sandbox's, and this folder. */
12
+ export declare const projectRows: (ctx: Context) => Row[];
13
+ export declare const projects: (ctx: Context) => Promise<number>;
14
+ export {};
@@ -0,0 +1,146 @@
1
+ // `sandbox projects`: which projects a sandbox can open without a restart. Project roots (whole folders of projects),
2
+ // the projects this tool has started before, and projects.all / projects.exclude. Changes to what is mapped take
3
+ // effect at the next start; a project you start while a sandbox runs joins it when it can (projects.ts in host/).
4
+ import path from 'node:path';
5
+ import { checkbox } from '@inquirer/prompts';
6
+ import { assertSafeMapping, assertSafeWorkspace } from '../../core/safe-paths.js';
7
+ import { dirExists, expandPath, fail, isRootPath } from '../../core/util.js';
8
+ import { isSandboxRunning } from '../../host/platform.js';
9
+ import { forgetProject, projectMapper, readKnown, readRegistry, rememberProject } from '../../host/projects.js';
10
+ import { isWithin } from '../../shared/projects.js';
11
+ import { c, pageSizeFor, shortenMiddle, termWidth } from '../colors.js';
12
+ import { printJson, table } from './context.js';
13
+ import { hereDir } from './project.js';
14
+ import { addToList, removeFromList, setSetting } from './user-config.js';
15
+ const settings = (ctx) => {
16
+ const p = ctx.loaded.config.projects;
17
+ const expand = (list) => list.map((x) => expandPath(x, ctx.loaded.base));
18
+ return { all: p.all, roots: expand(p.roots), exclude: expand(p.exclude) };
19
+ };
20
+ /** Every project worth listing: known ones, the running sandbox's, and this folder. */
21
+ export const projectRows = (ctx) => {
22
+ const s = settings(ctx);
23
+ const registry = isSandboxRunning() ? readRegistry(ctx.storageRoot) : null;
24
+ const here = hereDir(ctx);
25
+ const folders = [...readKnown(ctx.storageRoot).map((k) => k.host), ...(registry?.projects.map((p) => p.host) ?? [])];
26
+ const unique = folders.filter((f, i) => folders.findIndex((g) => g.toLowerCase() === f.toLowerCase()) === i);
27
+ return unique.map((folder) => {
28
+ const root = s.roots.find((r) => isWithin(r, folder)) ?? null;
29
+ const excluded = s.exclude.some((e) => isWithin(e, folder));
30
+ const inSandbox = registry?.projects.find((p) => p.host.toLowerCase() === folder.toLowerCase());
31
+ return {
32
+ folder, here: isWithin(folder, here), known: readKnown(ctx.storageRoot).some((k) => k.host.toLowerCase() === folder.toLowerCase()), root, excluded,
33
+ mappedAtStart: root !== null || (s.all && !excluded && dirExists(folder)), running: inSandbox ? inSandbox.name + ' (' + inSandbox.mechanism + ')' : null,
34
+ };
35
+ });
36
+ };
37
+ const NEXT_START = 'This changes what is mapped from the next start.';
38
+ const runningNote = () => (isSandboxRunning() ? ' The running sandbox keeps what it has; start a project in it (sandbox claude there) and it joins without a restart when it can, or you are offered one.' : '');
39
+ /** A folder for roots: it holds projects, so it must pass the same checks as a workspace and must not be a drive. */
40
+ const checkFolder = (ctx, folder, what) => {
41
+ const dir = path.resolve(folder);
42
+ if (!dirExists(dir))
43
+ fail('The folder ' + dir + ' does not exist.', 'Check the path.');
44
+ if (isRootPath(dir))
45
+ fail('A whole drive cannot be ' + what + ': ' + dir, 'Use the folder that holds your projects, e.g. F:\\Code.');
46
+ assertSafeWorkspace(dir, ctx.packageRoot, ctx.storageRoot);
47
+ assertSafeMapping(dir, ctx.packageRoot, ctx.storageRoot);
48
+ return dir;
49
+ };
50
+ const list = (ctx) => {
51
+ const rows = projectRows(ctx);
52
+ const s = settings(ctx);
53
+ if (printJson(ctx, { all: s.all, roots: s.roots, exclude: s.exclude, projects: rows }))
54
+ return 0;
55
+ console.log(c.bold('projects.all') + ': ' + (s.all ? 'on (every project below is mapped at every start, except excluded ones)' : 'off') + c.dim(' sandbox projects all on|off'));
56
+ console.log(c.bold('Roots') + ': ' + (s.roots.length ? s.roots.join(', ') : c.dim('none (sandbox projects roots add <folder>)')));
57
+ if (!rows.length) {
58
+ console.log('No projects yet: start one with sandbox claude in its folder.');
59
+ return 0;
60
+ }
61
+ const width = Math.max(20, termWidth() - 46);
62
+ table([['', 'FOLDER', 'AT START', 'RUNNING SANDBOX'], ...rows.map((r) => [
63
+ r.here ? '*' : '', shortenMiddle(r.folder, width), r.root ? 'root' : r.excluded ? 'excluded' : r.mappedAtStart ? 'mapped' : '-', r.running ?? '-',
64
+ ])]);
65
+ console.log(c.dim('* = this folder. Choose with: sandbox projects choose'));
66
+ return 0;
67
+ };
68
+ /** Ticks the projects mapped at every start (turns projects.all on when any is ticked); the rest are excluded. */
69
+ const choose = async (ctx) => {
70
+ const rows = projectRows(ctx).filter((r) => !r.root);
71
+ if (!rows.length) {
72
+ console.log('No projects to choose from yet (projects in a root are always mapped with it). Start one with sandbox claude in its folder.');
73
+ return 0;
74
+ }
75
+ if (!process.stdin.isTTY)
76
+ fail('sandbox projects choose needs an interactive terminal.', 'Use sandbox projects include|exclude <folder> instead.');
77
+ const s = settings(ctx);
78
+ const picked = await checkbox({
79
+ message: 'Projects mapped at every start, so they open without a restart (every agent in the sandbox can see them):',
80
+ pageSize: pageSizeFor(rows.length),
81
+ choices: rows.map((r) => ({ value: r.folder, name: shortenMiddle(r.folder, termWidth() - 12) + (r.here ? c.dim(' (this folder)') : ''), checked: s.all && !r.excluded })),
82
+ });
83
+ const exclude = rows.filter((r) => !picked.includes(r.folder)).map((r) => r.folder);
84
+ setSetting(ctx, ['projects', 'exclude'], [...s.exclude.filter((e) => !rows.some((r) => r.folder.toLowerCase() === e.toLowerCase())), ...exclude]);
85
+ setSetting(ctx, ['projects', 'all'], picked.length > 0);
86
+ console.log(picked.length + ' project(s) mapped at every start, ' + exclude.length + ' excluded. ' + NEXT_START + runningNote());
87
+ return 0;
88
+ };
89
+ export const projects = async (ctx) => {
90
+ const [action = 'list', value, folder] = ctx.options.positionals;
91
+ switch (action) {
92
+ case 'list': return list(ctx);
93
+ case 'choose': return choose(ctx);
94
+ case 'add': {
95
+ const dir = checkFolder(ctx, value, 'a project');
96
+ rememberProject(ctx.storageRoot, dir);
97
+ console.log('Added ' + dir + ' to your projects.' + (settings(ctx).all ? ' ' + NEXT_START : ' It is mapped at every start once projects.all is on (sandbox projects all on).'));
98
+ return 0;
99
+ }
100
+ case 'remove': {
101
+ const dir = path.resolve(value);
102
+ const forgot = forgetProject(ctx.storageRoot, dir);
103
+ // The running sandbox forgets it too (its folder stays visible until the sandbox stops, if it was mapped).
104
+ const registered = isSandboxRunning() ? readRegistry(ctx.storageRoot)?.projects.find((p) => !p.primary && p.host.toLowerCase() === dir.toLowerCase()) : undefined;
105
+ if (registered)
106
+ projectMapper({ storageRoot: ctx.storageRoot, packageRoot: ctx.packageRoot, wsbShare: false }).unregister(registered.name);
107
+ console.log(forgot || registered ? 'Removed ' + dir + ' from your projects (nothing in the folder was touched).' : dir + ' was not in your projects.');
108
+ return 0;
109
+ }
110
+ case 'include':
111
+ case 'exclude': {
112
+ const dir = path.resolve(value);
113
+ const changed = action === 'exclude' ? addToList(ctx, ['projects', 'exclude'], dir) : removeFromList(ctx, ['projects', 'exclude'], dir);
114
+ if (action === 'include')
115
+ rememberProject(ctx.storageRoot, dir);
116
+ console.log((changed ? (action === 'exclude' ? 'Excluded ' : 'Included ') + dir + '. ' + NEXT_START : 'Nothing changed.') + (action === 'include' && !settings(ctx).all ? ' projects.all is off: sandbox projects all on maps your projects at every start.' : '') + (changed ? runningNote() : ''));
117
+ return 0;
118
+ }
119
+ case 'all': {
120
+ if (value === undefined) {
121
+ console.log('projects.all is ' + (settings(ctx).all ? 'on' : 'off') + '.');
122
+ return 0;
123
+ }
124
+ setSetting(ctx, ['projects', 'all'], value === 'on');
125
+ console.log('projects.all is ' + value + '. ' + NEXT_START + (value === 'on' ? ' Every agent in the sandbox can then see all of those projects.' : '') + runningNote());
126
+ return 0;
127
+ }
128
+ case 'roots': {
129
+ if (!value || value === 'list') {
130
+ const roots = settings(ctx).roots;
131
+ console.log(roots.length ? roots.join('\n') : 'No project roots. Add one: sandbox projects roots add <folder>');
132
+ return 0;
133
+ }
134
+ if (value === 'add') {
135
+ const dir = checkFolder(ctx, folder, 'a project root');
136
+ const added = addToList(ctx, ['projects', 'roots'], dir);
137
+ console.log(added ? 'Added the project root ' + dir + ': every project in it (also new ones) opens without a restart. Every agent in the sandbox can see all of them. ' + NEXT_START + runningNote() : dir + ' is a root already.');
138
+ return 0;
139
+ }
140
+ const dir = path.resolve(folder);
141
+ console.log(removeFromList(ctx, ['projects', 'roots'], dir) ? 'Removed the project root ' + dir + '. ' + NEXT_START : dir + ' was not a root.');
142
+ return 0;
143
+ }
144
+ default: return fail('Unknown projects action "' + action + '".');
145
+ }
146
+ };
@@ -0,0 +1,2 @@
1
+ import { type RepairCheck } from './types.js';
2
+ export declare const CHECKS: RepairCheck[];
@@ -1,131 +1,22 @@
1
- // `sandbox repair`: finds and fixes common problems. Each check is a small function in CHECKS; it either confirms
2
- // all is well (OK), fixes the problem itself (FIXED), or says what you need to do (NEEDS YOU). Nothing under the data
3
- // folder is followed through a link, and toolchain/run-state fixes wait until no sandbox is running.
1
+ // The repair checks, in the order they run. Each either confirms all is well, fixes the problem itself, or says what
2
+ // you need to do. Nothing under the data folder is followed through a link.
4
3
  import fs from 'node:fs';
5
4
  import path from 'node:path';
6
5
  import { spawnSync } from 'node:child_process';
7
6
  import _ from 'lodash';
8
- import { z } from 'zod';
9
- import { DEFAULTS, loadConfig, userDir } from '../../core/config.js';
10
- import { assertNoLinks, dirExists, errMsg, expandPath, fileExists, isLink, isWin, removeLink, UserError } from '../../core/util.js';
11
- import { resetRunDir } from '../../host/channel.js';
12
- import { isSandboxRunning, sandboxFeatureInstalled } from '../../host/platform.js';
13
- import { listSessions, runPaths, scanRequests } from '../../shared/rundir.js';
14
- import { createContext, printJson, table } from './context.js';
15
- import { humanBytes, removeTree } from './data.js';
16
- const GB = 1024 ** 3;
17
- const LABEL = { ok: 'OK', fixed: 'FIXED', 'needs-you': 'NEEDS YOU', warn: 'WARNING', skipped: 'SKIPPED' };
18
- const ok = (detail) => ({ status: 'ok', detail });
19
- const whileRunning = { status: 'skipped', detail: 'a sandbox is running and may be using it; run repair again once it is closed' };
7
+ import { GB, freeBytesAt, humanBytes } from '../../../core/disk.js';
8
+ import { assertTestWritable } from '../../../shared/test-guard.js';
9
+ import { assertNoLinks, fileExists, isLink, isWin, removeLink } from '../../../core/util.js';
10
+ import { resetRunDir } from '../../../host/channel.js';
11
+ import { sandboxFeatureInstalled } from '../../../host/platform.js';
12
+ import { listSessions, runPaths, scanRequests } from '../../../shared/rundir.js';
13
+ import { removeTree } from '../data/remove.js';
14
+ import { deadPidFiles, findHalfInstalled, findLinks, findStagingLeftovers, globalPackages, resolvedVersionsValid } from './finders.js';
15
+ import { ok, whileRunning } from './types.js';
20
16
  const rel = (root, p) => path.relative(root, p) || '.';
21
- // ---- pure helpers (exported for tests) ----
22
- /** Interrupted downloads and copies: `<name>.staging-<hex>` and `node-staging`, in tools\ and one folder down (tools\go\1.23.staging-ab12). */
23
- export const findStagingLeftovers = (toolsDir) => {
24
- const isStaging = (name) => /\.staging-[0-9a-f]+$/i.test(name) || name.toLowerCase() === 'node-staging';
25
- const realChildren = (dir) => (dirExists(dir) && !isLink(dir) ? fs.readdirSync(dir).map((n) => path.join(dir, n)) : []);
26
- return realChildren(toolsDir).flatMap((p) => (isStaging(path.basename(p)) ? [p] : isLink(p) ? [] : realChildren(p).filter((c) => isStaging(path.basename(c)))));
27
- };
28
- export const TOOL_RULES = [
29
- { folder: 'nvm', exe: ['nvm.exe'] },
30
- { folder: 'nvm', exe: ['node.exe'], versions: true, match: /^v\d+\.\d+\.\d+$/ },
31
- { folder: 'node', exe: ['node.exe'] },
32
- { folder: 'git', exe: ['bin', 'bash.exe'] },
33
- { folder: 'dotnet', exe: ['dotnet.exe'] },
34
- { folder: 'go', exe: ['bin', 'go.exe'], versions: true },
35
- { folder: 'java', exe: ['bin', 'java.exe'], versions: true },
36
- { folder: 'python', exe: ['python.exe'], versions: true },
37
- ];
38
- /** Toolchain folders that exist but lack their main program (an install that stopped half way). Never enters links. */
39
- export const findHalfInstalled = (toolsDir, rules = TOOL_RULES) => {
40
- const realDir = (p) => dirExists(p) && !isLink(p);
41
- const staging = (name) => /\.staging-[0-9a-f]+$/i.test(name) || name.toLowerCase() === 'node-staging';
42
- return rules.flatMap((rule) => {
43
- const base = path.join(toolsDir, rule.folder);
44
- if (!realDir(base))
45
- return [];
46
- const folders = rule.versions
47
- ? fs.readdirSync(base).filter((n) => !staging(n) && (!rule.match || rule.match.test(n))).map((n) => path.join(base, n)).filter(realDir)
48
- : [base];
49
- return folders.filter((f) => !fileExists(path.join(f, ...rule.exe))).map((f) => ({ path: f, missing: path.join(...rule.exe) }));
50
- });
51
- };
52
- /**
53
- * Every link or junction under `root` (the links themselves; nothing behind them is visited). Before descending into
54
- * a folder its real path must still be inside root: a folder swapped for a junction between two looks is not followed.
55
- */
56
- export const findLinks = (root) => {
57
- if (isLink(root))
58
- return [root];
59
- const real = (p) => { try {
60
- return fs.realpathSync.native(p).toLowerCase();
61
- }
62
- catch {
63
- return '';
64
- } };
65
- const top = real(root);
66
- const inside = (p) => { const r = real(p); return r !== '' && (r === top || r.startsWith(top + path.sep)); };
67
- const walk = (dir) => {
68
- if (!inside(dir))
69
- return [];
70
- let entries;
71
- try {
72
- entries = fs.readdirSync(dir, { withFileTypes: true });
73
- }
74
- catch {
75
- return [];
76
- }
77
- return entries.flatMap((d) => {
78
- const p = path.join(dir, d.name);
79
- if (d.isSymbolicLink() || isLink(p))
80
- return [p];
81
- return d.isDirectory() ? walk(p) : [];
82
- });
83
- };
84
- return dirExists(root) ? walk(root) : [];
85
- };
86
- const ResolvedVersions = z.record(z.string(), z.unknown());
87
- /** tools\resolved-versions.json is a JSON object (anything else makes the guest stumble). */
88
- export const resolvedVersionsValid = (text) => {
89
- try {
90
- return ResolvedVersions.safeParse(JSON.parse(text.replace(/^/, ''))).success;
91
- }
92
- catch {
93
- return false;
94
- }
95
- };
96
- const NpmList = z.object({ dependencies: z.record(z.string(), z.unknown()).optional() });
97
- /** Package names from `npm ls -g --json`; [] when the output is not what npm prints. */
98
- export const globalPackages = (json) => {
99
- try {
100
- return Object.keys(NpmList.parse(JSON.parse(json)).dependencies ?? {});
101
- }
102
- catch {
103
- return [];
104
- }
105
- };
106
- const Pid = z.coerce.number().int().positive();
107
- export const pidAlive = (pid) => { try {
108
- process.kill(pid, 0);
109
- return true;
110
- }
111
- catch (e) {
112
- return e.code === 'EPERM';
113
- } };
114
- /** Sync-watcher pid files (storage\run\watcher-*.pid) whose process is gone (or whose content is not a pid). */
115
- export const deadPidFiles = (runDir, alive = pidAlive) => (dirExists(runDir) ? fs.readdirSync(runDir) : [])
116
- .filter((n) => /^watcher-.*\.pid$/i.test(n))
117
- .map((n) => path.join(runDir, n))
118
- .filter((f) => { try {
119
- const pid = Pid.safeParse(fs.readFileSync(f, 'utf8').trim());
120
- return !pid.success || !alive(pid.data);
121
- }
122
- catch {
123
- return false;
124
- } });
125
- // ---- checks ----
126
17
  /** Fails (throws) unless every folder from the data folder to `p` is real. */
127
18
  const guard = (env, p) => assertNoLinks(path.dirname(env.dataPath), p);
128
- const CHECKS = [
19
+ export const CHECKS = [
129
20
  {
130
21
  name: 'Config file',
131
22
  run: (env) => (env.configError
@@ -183,6 +74,7 @@ const CHECKS = [
183
74
  return ok('clean.');
184
75
  if (env.running)
185
76
  return ok('a sandbox is running; kept.');
77
+ assertTestWritable(file);
186
78
  fs.rmSync(file, { force: true });
187
79
  return { status: 'fixed', detail: 'removed the saved hidden-window list from a sandbox that is no longer running.' };
188
80
  },
@@ -193,8 +85,10 @@ const CHECKS = [
193
85
  const dead = deadPidFiles(path.join(env.storageRoot, 'run'));
194
86
  if (!dead.length)
195
87
  return ok('no stale watcher records.');
196
- for (const f of dead)
88
+ for (const f of dead) {
89
+ assertTestWritable(f);
197
90
  fs.rmSync(f, { force: true });
91
+ }
198
92
  return { status: 'fixed', detail: 'removed ' + dead.length + ' record(s) of a history sync watcher that is no longer running (the next launch starts a new one).' };
199
93
  },
200
94
  },
@@ -241,6 +135,7 @@ const CHECKS = [
241
135
  }
242
136
  if (fs.statSync(file).size < 4 * 1024 * 1024 && resolvedVersionsValid(fs.readFileSync(file, 'utf8')))
243
137
  return ok('valid.');
138
+ assertTestWritable(file);
244
139
  fs.rmSync(file, { force: true });
245
140
  return { status: 'fixed', detail: 'tools\\resolved-versions.json was damaged; removed it (versions are looked up again on the next start).' };
246
141
  },
@@ -264,69 +159,12 @@ const CHECKS = [
264
159
  {
265
160
  name: 'Free disk',
266
161
  run: (env) => {
267
- let at = path.resolve(env.storageRoot);
268
- while (!fs.existsSync(at) && path.dirname(at) !== at)
269
- at = path.dirname(at);
270
- let free;
271
- try {
272
- const s = fs.statfsSync(at);
273
- free = s.bavail * s.bsize;
274
- }
275
- catch {
162
+ const free = freeBytesAt(env.storageRoot);
163
+ if (free === null)
276
164
  return { status: 'skipped', detail: 'could not check the drive of ' + env.storageRoot };
277
- }
278
165
  if (free >= 20 * GB)
279
166
  return ok(humanBytes(free) + ' free.');
280
167
  return { status: 'warn', detail: humanBytes(free) + ' free on the drive of ' + env.storageRoot + (free < 5 * GB ? ': toolchains need several GB.' : ': getting low.') + ' See what is stored with: sandbox data, and free space with: sandbox data clean <target>' };
281
168
  },
282
169
  },
283
170
  ];
284
- /** Runs every check in order; a check that throws is reported, never stops the rest. */
285
- export const runChecks = (env, checks = CHECKS) => checks.map((check) => {
286
- try {
287
- return { name: check.name, ...check.run(env) };
288
- }
289
- catch (e) {
290
- return { name: check.name, status: 'needs-you', detail: 'check failed: ' + errMsg(e) + (e instanceof UserError && e.fix ? ' ' + e.fix : '') };
291
- }
292
- });
293
- export const repair = (ctx, configError = null) => {
294
- const env = { ctx, running: isSandboxRunning(), dataPath: ctx.dataPath, storageRoot: ctx.storageRoot, toolsDir: path.join(ctx.dataPath, 'tools'), configError };
295
- // Re-check the config file even when the context loaded (it may have been edited since, or come from --config).
296
- if (!env.configError) {
297
- try {
298
- loadConfig(ctx.options.config);
299
- }
300
- catch (e) {
301
- if (e instanceof UserError)
302
- env.configError = e;
303
- else
304
- throw e;
305
- }
306
- }
307
- const results = runChecks(env);
308
- if (printJson(ctx, results))
309
- return results.some((r) => r.status === 'needs-you') ? 1 : 0;
310
- table(results.map((r) => [LABEL[r.status], r.name, r.detail]));
311
- const counts = _.countBy(results, 'status');
312
- 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).' : '.')));
313
- return counts['needs-you'] ? 1 : 0;
314
- };
315
- /**
316
- * Entry point that works even when the config file is broken (createContext would throw): falls back to the built-in
317
- * defaults so the other checks still run, and reports the config problem as the first check.
318
- */
319
- export const repairCommand = (packageRoot, entry, options) => {
320
- let ctx;
321
- try {
322
- ctx = createContext(packageRoot, entry, options);
323
- }
324
- catch (e) {
325
- if (!(e instanceof UserError))
326
- throw e;
327
- const loaded = { config: structuredClone(DEFAULTS), configPath: null, base: userDir() };
328
- const storageRoot = expandPath(loaded.config.storage.root, loaded.base);
329
- return repair({ packageRoot, entry, options, loaded, storageRoot, dataPath: path.join(storageRoot, 'data', loaded.config.storage.dataName) }, e);
330
- }
331
- return repair(ctx);
332
- };
@@ -1,24 +1,3 @@
1
- import { UserError } from '../../core/util.js';
2
- import type { CliOptions } from '../args.js';
3
- import { type Context } from './context.js';
4
- export type RepairStatus = 'ok' | 'fixed' | 'needs-you' | 'warn' | 'skipped';
5
- export interface RepairResult {
6
- name: string;
7
- status: RepairStatus;
8
- detail: string;
9
- }
10
- export interface RepairEnv {
11
- ctx: Context;
12
- running: boolean;
13
- dataPath: string;
14
- storageRoot: string;
15
- toolsDir: string;
16
- configError: UserError | null;
17
- }
18
- export interface RepairCheck {
19
- name: string;
20
- run: (env: RepairEnv) => Omit<RepairResult, 'name'>;
21
- }
22
1
  /** Interrupted downloads and copies: `<name>.staging-<hex>` and `node-staging`, in tools\ and one folder down (tools\go\1.23.staging-ab12). */
23
2
  export declare const findStagingLeftovers: (toolsDir: string) => string[];
24
3
  /**
@@ -49,11 +28,3 @@ export declare const globalPackages: (json: string) => string[];
49
28
  export declare const pidAlive: (pid: number) => boolean;
50
29
  /** Sync-watcher pid files (storage\run\watcher-*.pid) whose process is gone (or whose content is not a pid). */
51
30
  export declare const deadPidFiles: (runDir: string, alive?: (pid: number) => boolean) => string[];
52
- /** Runs every check in order; a check that throws is reported, never stops the rest. */
53
- export declare const runChecks: (env: RepairEnv, checks?: RepairCheck[]) => RepairResult[];
54
- export declare const repair: (ctx: Context, configError?: UserError | null) => number;
55
- /**
56
- * Entry point that works even when the config file is broken (createContext would throw): falls back to the built-in
57
- * defaults so the other checks still run, and reports the config problem as the first check.
58
- */
59
- export declare const repairCommand: (packageRoot: string, entry: string, options: CliOptions) => number;
@@ -0,0 +1,108 @@
1
+ // Pure finders the repair checks use (exported for tests): they look, they never change anything.
2
+ import fs from 'node:fs';
3
+ import path from 'node:path';
4
+ import { z } from 'zod';
5
+ import { dirExists, fileExists, isLink } from '../../../core/util.js';
6
+ /** Interrupted downloads and copies: `<name>.staging-<hex>` and `node-staging`, in tools\ and one folder down (tools\go\1.23.staging-ab12). */
7
+ export const findStagingLeftovers = (toolsDir) => {
8
+ const isStaging = (name) => /\.staging-[0-9a-f]+$/i.test(name) || name.toLowerCase() === 'node-staging';
9
+ const realChildren = (dir) => (dirExists(dir) && !isLink(dir) ? fs.readdirSync(dir).map((n) => path.join(dir, n)) : []);
10
+ return realChildren(toolsDir).flatMap((p) => (isStaging(path.basename(p)) ? [p] : isLink(p) ? [] : realChildren(p).filter((c) => isStaging(path.basename(c)))));
11
+ };
12
+ export const TOOL_RULES = [
13
+ { folder: 'nvm', exe: ['nvm.exe'] },
14
+ { folder: 'nvm', exe: ['node.exe'], versions: true, match: /^v\d+\.\d+\.\d+$/ },
15
+ { folder: 'node', exe: ['node.exe'] },
16
+ { folder: 'git', exe: ['bin', 'bash.exe'] },
17
+ { folder: 'dotnet', exe: ['dotnet.exe'] },
18
+ { folder: 'go', exe: ['bin', 'go.exe'], versions: true },
19
+ { folder: 'java', exe: ['bin', 'java.exe'], versions: true },
20
+ { folder: 'python', exe: ['python.exe'], versions: true },
21
+ ];
22
+ /** Toolchain folders that exist but lack their main program (an install that stopped half way). Never enters links. */
23
+ export const findHalfInstalled = (toolsDir, rules = TOOL_RULES) => {
24
+ const realDir = (p) => dirExists(p) && !isLink(p);
25
+ const staging = (name) => /\.staging-[0-9a-f]+$/i.test(name) || name.toLowerCase() === 'node-staging';
26
+ return rules.flatMap((rule) => {
27
+ const base = path.join(toolsDir, rule.folder);
28
+ if (!realDir(base))
29
+ return [];
30
+ const folders = rule.versions
31
+ ? fs.readdirSync(base).filter((n) => !staging(n) && (!rule.match || rule.match.test(n))).map((n) => path.join(base, n)).filter(realDir)
32
+ : [base];
33
+ return folders.filter((f) => !fileExists(path.join(f, ...rule.exe))).map((f) => ({ path: f, missing: path.join(...rule.exe) }));
34
+ });
35
+ };
36
+ /**
37
+ * Every link or junction under `root` (the links themselves; nothing behind them is visited). Before descending into
38
+ * a folder its real path must still be inside root: a folder swapped for a junction between two looks is not followed.
39
+ */
40
+ export const findLinks = (root) => {
41
+ if (isLink(root))
42
+ return [root];
43
+ const real = (p) => { try {
44
+ return fs.realpathSync.native(p).toLowerCase();
45
+ }
46
+ catch {
47
+ return '';
48
+ } };
49
+ const top = real(root);
50
+ const inside = (p) => { const r = real(p); return r !== '' && (r === top || r.startsWith(top + path.sep)); };
51
+ const walk = (dir) => {
52
+ if (!inside(dir))
53
+ return [];
54
+ let entries;
55
+ try {
56
+ entries = fs.readdirSync(dir, { withFileTypes: true });
57
+ }
58
+ catch {
59
+ return [];
60
+ }
61
+ return entries.flatMap((d) => {
62
+ const p = path.join(dir, d.name);
63
+ if (d.isSymbolicLink() || isLink(p))
64
+ return [p];
65
+ return d.isDirectory() ? walk(p) : [];
66
+ });
67
+ };
68
+ return dirExists(root) ? walk(root) : [];
69
+ };
70
+ const ResolvedVersions = z.record(z.string(), z.unknown());
71
+ /** tools\resolved-versions.json is a JSON object (anything else makes the guest stumble). */
72
+ export const resolvedVersionsValid = (text) => {
73
+ try {
74
+ return ResolvedVersions.safeParse(JSON.parse(text.replace(/^/, ''))).success;
75
+ }
76
+ catch {
77
+ return false;
78
+ }
79
+ };
80
+ const NpmList = z.object({ dependencies: z.record(z.string(), z.unknown()).optional() });
81
+ /** Package names from `npm ls -g --json`; [] when the output is not what npm prints. */
82
+ export const globalPackages = (json) => {
83
+ try {
84
+ return Object.keys(NpmList.parse(JSON.parse(json)).dependencies ?? {});
85
+ }
86
+ catch {
87
+ return [];
88
+ }
89
+ };
90
+ const Pid = z.coerce.number().int().positive();
91
+ export const pidAlive = (pid) => { try {
92
+ process.kill(pid, 0);
93
+ return true;
94
+ }
95
+ catch (e) {
96
+ return e.code === 'EPERM';
97
+ } };
98
+ /** Sync-watcher pid files (storage\run\watcher-*.pid) whose process is gone (or whose content is not a pid). */
99
+ export const deadPidFiles = (runDir, alive = pidAlive) => (dirExists(runDir) ? fs.readdirSync(runDir) : [])
100
+ .filter((n) => /^watcher-.*\.pid$/i.test(n))
101
+ .map((n) => path.join(runDir, n))
102
+ .filter((f) => { try {
103
+ const pid = Pid.safeParse(fs.readFileSync(f, 'utf8').trim());
104
+ return !pid.success || !alive(pid.data);
105
+ }
106
+ catch {
107
+ return false;
108
+ } });
@@ -0,0 +1,12 @@
1
+ import { UserError } from '../../../core/util.js';
2
+ import type { CliOptions } from '../../args.js';
3
+ import { type Context } from '../context.js';
4
+ import type { RepairCheck, RepairEnv, RepairResult } from './types.js';
5
+ /** Runs every check in order; a check that throws is reported, never stops the rest. */
6
+ export declare const runChecks: (env: RepairEnv, checks?: RepairCheck[]) => RepairResult[];
7
+ export declare const repair: (ctx: Context, configError?: UserError | null) => number;
8
+ /**
9
+ * Entry point that works even when the config file is broken (createContext would throw): falls back to the built-in
10
+ * defaults so the other checks still run, and reports the config problem as the first check.
11
+ */
12
+ export declare const repairCommand: (packageRoot: string, entry: string, options: CliOptions) => number;