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,52 @@
1
+ // What every database module shares: where data lives, starting a server detached, and safe first-time creation.
2
+ import fs from 'node:fs';
3
+ import path from 'node:path';
4
+ import { spawn } from 'node:child_process';
5
+ import { safeRemove } from '../../../shared/safe-remove.js';
6
+ import { dataDir, exists, logsDir, mkdirp } from '../../env.js';
7
+ import { portOpen, waitForPort } from '../../ports.js';
8
+ /** C:\AgentData\databases\<name>: the data survives the sandbox. */
9
+ export const dataFor = (name) => path.join(dataDir, 'databases', name);
10
+ /** Starts a server detached (it outlives the boot script) with its output in logs\<name>.log, and waits for its port. */
11
+ export const startServer = async (name, exe, args, port) => {
12
+ if (await portOpen(port))
13
+ throw new Error('port ' + port + ' is already in use (change databases.' + name + '.port)');
14
+ const logFile = path.join(logsDir, name + '.log');
15
+ const out = fs.openSync(logFile, 'a');
16
+ spawn(exe, args, { detached: true, stdio: ['ignore', out, out], windowsHide: true }).unref();
17
+ fs.closeSync(out);
18
+ if (!(await waitForPort(port, 60)))
19
+ throw new Error(name + ' did not start listening on port ' + port + '; see ' + logFile);
20
+ };
21
+ /**
22
+ * Lock files a sandbox that was closed without shutting the server down leaves behind. Only when nothing listens on
23
+ * the port: a server already running in this VM keeps its lock.
24
+ */
25
+ export const clearStaleLocks = async (dir, files, port) => {
26
+ if (await portOpen(port))
27
+ return;
28
+ for (const f of files)
29
+ fs.rmSync(path.join(dir, f), { force: true });
30
+ };
31
+ /**
32
+ * Creates a database folder with `create` (initdb, install-db) unless `marker` is there. A failed or interrupted run
33
+ * removes the folder again when this call made it, so the half-made one is not taken for a finished database next boot.
34
+ */
35
+ export const createOnce = async (data, marker, create) => {
36
+ if (exists(path.join(data, marker)))
37
+ return;
38
+ const existed = exists(data);
39
+ mkdirp(path.dirname(data));
40
+ try {
41
+ await create();
42
+ }
43
+ catch (e) {
44
+ if (!existed) {
45
+ try {
46
+ safeRemove(data, dataDir);
47
+ }
48
+ catch { /* the original error is the one to report */ }
49
+ }
50
+ throw e;
51
+ }
52
+ };
@@ -3,18 +3,24 @@
3
3
  import fs from 'node:fs';
4
4
  import path from 'node:path';
5
5
  import { download, githubAsset, moveIntoPlace, stagingFolder } from '../../shared/download.js';
6
- import { dataDir, dryRun, errMsg, execAsync, execOutput, exists, hostToolsDir, launchDir, log, mkdirp, robocopy, warn } from '../env.js';
7
- import { ensureCompiled } from '../ptyhost.js';
6
+ import { safeRemove } from '../../shared/safe-remove.js';
7
+ import { dataDir, dryRun, errMsg, execAsync, execOutput, exists, hostToolsDir, launchDir, log, mkdirp, robocopy, toolsDir, warn } from '../env.js';
8
+ import { ensureCompiled } from '../helpers.js';
8
9
  import { arch, tool } from './common.js';
10
+ import { duringSession } from '../project-context.js';
9
11
  const BLOCKED_HOOKS = ['pre-commit', 'commit-msg', 'pre-merge-commit', 'pre-push', 'pre-rebase', 'pre-applypatch', 'applypatch-msg'];
10
12
  const posix = (p) => p.replace(/\\/g, '/');
11
13
  const installPortableGit = async (root) => {
12
14
  const asset = await githubAsset('git-for-windows/git', arch === 'arm64' ? /^PortableGit-[\d.]+-arm64\.7z\.exe$/ : /^PortableGit-[\d.]+-64-bit\.7z\.exe$/);
13
15
  const sfx = tool(asset.name);
14
- log('Downloading ' + asset.name + ' ...');
15
- await download(asset.url, sfx, asset.sha256);
16
- await execAsync(sfx, ['-o' + root, '-y'], { stdio: 'ignore' });
17
- fs.rmSync(sfx, { force: true });
16
+ log('Downloading Git for Windows (' + asset.name + ', ~60 MB once) ...');
17
+ try {
18
+ await download(asset.url, sfx, asset.sha256);
19
+ await execAsync(sfx, ['-o' + root, '-y'], { stdio: 'ignore' });
20
+ }
21
+ finally {
22
+ fs.rmSync(sfx, { force: true });
23
+ }
18
24
  };
19
25
  /**
20
26
  * readonly / none: a `git` wrapper first on PATH and as a bash function (Git Bash would otherwise find the real
@@ -24,14 +30,17 @@ const installGuard = (env, access, realGit) => {
24
30
  const dir = mkdirp(tool('gitguard'));
25
31
  const guard = path.join(launchDir, 'runtime', 'gitguard.js');
26
32
  // A real git.exe (programs like Claude Code only look for .exe); a .cmd only if the C# compiler is unavailable.
27
- try {
28
- fs.copyFileSync(ensureCompiled('gitshim'), path.join(dir, 'git.exe'));
29
- fs.rmSync(path.join(dir, 'git.cmd'), { force: true });
30
- }
31
- catch (e) {
32
- warn('git guard: falling back to git.cmd (' + errMsg(e) + ')');
33
- fs.writeFileSync(path.join(dir, 'git.cmd'), '@"' + process.execPath + '" "' + guard + '" %*\r\n');
34
- }
33
+ // A project added later reuses the boot's: sessions may be running it, and Windows cannot replace a running exe.
34
+ if (duringSession() && (exists(path.join(dir, 'git.exe')) || exists(path.join(dir, 'git.cmd')))) { /* keep it */ }
35
+ else
36
+ try {
37
+ fs.copyFileSync(ensureCompiled('gitshim'), path.join(dir, 'git.exe'));
38
+ fs.rmSync(path.join(dir, 'git.cmd'), { force: true });
39
+ }
40
+ catch (e) {
41
+ warn('git guard: falling back to git.cmd (' + errMsg(e) + ')');
42
+ fs.writeFileSync(path.join(dir, 'git.cmd'), '@"' + process.execPath + '" "' + guard + '" %*\r\n');
43
+ }
35
44
  fs.writeFileSync(path.join(dir, 'bashenv.sh'), 'git() { "' + posix(process.execPath) + '" "' + posix(guard) + '" "$@"; }\nexport -f git\n');
36
45
  env.prepend.unshift(dir);
37
46
  Object.assign(env.vars, {
@@ -71,7 +80,7 @@ export const setupGit = async (env, hostCopy, refresh, access) => {
71
80
  const hostGit = path.join(hostToolsDir, 'git');
72
81
  if (exists(root) && !exists(bash)) {
73
82
  log('Replacing the old MinGit (no bash) with full Git for Windows.');
74
- fs.rmSync(root, { recursive: true, force: true });
83
+ safeRemove(root, toolsDir);
75
84
  }
76
85
  if (!exists(bash) || refresh) {
77
86
  if (dryRun) {
@@ -93,7 +102,7 @@ export const setupGit = async (env, hostCopy, refresh, access) => {
93
102
  moveIntoPlace(work, root);
94
103
  }
95
104
  finally {
96
- fs.rmSync(work, { recursive: true, force: true });
105
+ safeRemove(work, toolsDir);
97
106
  }
98
107
  }
99
108
  if (!exists(bash))
@@ -19,7 +19,7 @@ export const setupGo = async (env, config) => {
19
19
  const file = (await findRelease(version)).files.find((f) => f.os === 'windows' && f.arch === goArch && f.kind === 'archive');
20
20
  if (!file)
21
21
  throw new Error(version + ' has no Windows ' + goArch + ' zip');
22
- log('Downloading ' + file.filename + ' ...');
22
+ log('Downloading Go ' + version.replace(/^go/, '') + ' (~80 MB once) ...');
23
23
  await installArchive('https://go.dev/dl/' + file.filename, root, { sha256: file.sha256, locate: 'VERSION', fileName: file.filename });
24
24
  }
25
25
  const GOPATH = tool('go-path');
@@ -1,13 +1,17 @@
1
- import type { DatabasePlan, GitAccess, Toolchains, ToolVersions } from '../../core/types.js';
1
+ import type { Toolchains, ToolVersions } from '../../core/types.js';
2
2
  import { type ToolEnv } from './common.js';
3
+ import { type ToolchainOptions } from './plan.js';
3
4
  export type { ToolEnv } from './common.js';
4
- export { pickNodeVersion, pickPackageVersion } from './versions.js';
5
- export interface ToolchainOptions {
6
- gitEnabled: boolean;
7
- gitAccess: GitAccess;
8
- hostGit: boolean;
9
- refresh: boolean;
10
- databases: DatabasePlan[];
5
+ export type { ToolchainOptions } from './plan.js';
6
+ export interface RunOptions {
7
+ /** Progress lines ("Installing Go, Rust (3 of 9 done)"); default: the sandbox's boot status. */
8
+ report?: (message: string) => void;
9
+ /** What machine-wide (shared) steps contributed at boot: reused instead of run again. */
10
+ reuse?: Record<string, ToolEnv>;
11
+ }
12
+ /** The combined environment, plus what each step contributed (the boot keeps it so later projects can reuse shared steps). */
13
+ export interface ToolchainResult extends ToolEnv {
14
+ steps: Record<string, ToolEnv>;
11
15
  }
12
16
  /** Provisions every enabled toolchain and database, and returns the combined environment for the agent. */
13
- export declare const setupToolchains: (config: Toolchains, versions: ToolVersions, options: ToolchainOptions) => Promise<ToolEnv>;
17
+ export declare const setupToolchains: (config: Toolchains, versions: ToolVersions, options: ToolchainOptions, run?: RunOptions) => Promise<ToolchainResult>;
@@ -1,102 +1,54 @@
1
- import path from 'node:path';
2
- import { dataDir, execAsync, exists, localDir, log, mkdirp, setSandboxStatus, workspace } from '../env.js';
3
- import { arch, emptyEnv, installOnce, step, tool, wanted } from './common.js';
4
- import { DATABASE_NAMES, setupDatabase } from './databases.js';
5
- import { setupGit } from './git.js';
6
- import { setupGo } from './go.js';
7
- import { setupJava } from './java.js';
8
- import { setupMsvc } from './msvc.js';
9
- import { setupPwsh } from './pwsh.js';
10
- import { setupCli } from './cli.js';
11
- import { setupPyenv, setupVolta, wantsPyenv, wantsVolta } from './managers.js';
12
- import { setupNode } from './node.js';
13
- import { setupPython } from './python.js';
14
- import { setupCpp, setupDotnet, setupMesa, setupRust } from './native.js';
15
- import { setupBun, setupDeno, setupUv, uvVars } from './runtimes.js';
16
- import { setupPhp, setupRuby, setupVcRuntime } from './scripting.js';
17
- import { setupVscode } from './vscode.js';
18
- export { pickNodeVersion, pickPackageVersion } from './versions.js';
1
+ import { log, mkdirp, setSandboxStatus } from '../env.js';
2
+ import { emptyEnv, step, tool } from './common.js';
3
+ import { plan } from './plan.js';
19
4
  /** How many installers run at once: enough to overlap downloads with installs, few enough not to thrash the disk. */
20
5
  const PARALLEL = 4;
21
- /** Python command-line tools through uv (each in its own environment, commands in uv's bin folder on PATH). */
22
- const setupPythonTools = async (env, tools) => {
23
- const uv = tool('uv', 'uv.exe');
24
- if (!exists(uv))
25
- throw new Error('toolchains.python.tools needs uv (toolchains.uv.enabled)');
26
- await installOnce(env, 'uv tool', tools, (pkg) => execAsync(uv, ['tool', 'install', pkg], { env: { ...process.env, ...uvVars() } }));
27
- };
28
- /**
29
- * Where cargo builds and where sccache keeps its cache. Cargo fingerprints hold absolute paths and the host toolchain,
30
- * so a target\\ shared with builds on the PC rebuilds every time the side changes: "separate" (the default) keeps a
31
- * sandbox one next to it. sccache's cache persists unless the project sets SCCACHE_DIR itself.
32
- */
33
- const rustCaches = (rust) => {
34
- const target = rust.targetDir === 'shared' ? null
35
- : rust.targetDir === 'separate' ? path.join(workspace, 'target', 'sandbox')
36
- : rust.targetDir === 'local' ? path.join(localDir, 'cargo-target')
37
- : rust.targetDir;
38
- return { ...(target ? { CARGO_TARGET_DIR: target } : {}), ...(process.env.SCCACHE_DIR ? {} : { SCCACHE_DIR: path.join(dataDir, 'cache', 'sccache') }) };
39
- };
40
- /** "auto": the msvc host when the MSVC step runs (as on most PCs), else GNU with MinGW. */
41
- const rustHost = (config) => {
42
- if (config.rust.host !== 'auto')
43
- return config.rust.host;
44
- return (arch === 'arm64' ? 'aarch64' : 'x86_64') + (wanted(config.msvc.enabled, 'msvc') ? '-pc-windows-msvc' : '-pc-windows-gnu');
45
- };
46
- /** The toolchains and databases for this sandbox, in the order their PATH entries are listed. */
47
- const plan = (config, versions, o) => {
48
- const java = wanted(config.java.enabled, 'java');
49
- return [
50
- { name: 'Git', on: o.gitEnabled, run: (env) => setupGit(env, o.hostGit, o.refresh, o.gitAccess) },
51
- // Before Node: with package.json pins, Volta's node/npm/pnpm/yarn come first on PATH.
52
- { name: 'Volta', on: wantsVolta(config.node.volta), run: (env) => setupVolta(env) },
53
- { name: 'Node ' + versions.node.spec, on: true, run: (env) => setupNode(env, versions, config.node) },
54
- { name: 'Python', on: wanted(config.python.enabled, 'python'), run: (env) => setupPython(env, config.python, versions) },
55
- { name: 'pyenv-win', on: wanted(config.python.enabled, 'python') && wantsPyenv(config.python.pyenv), run: (env) => setupPyenv(env) },
56
- { name: 'C/C++', on: wanted(config.cpp.enabled, 'cpp') || wanted(config.rust.enabled, 'rust'), run: (env) => setupCpp(env, config.cpp) },
57
- { name: 'MSVC', on: wanted(config.msvc.enabled, 'msvc'), run: (env) => setupMsvc(env, config.msvc) },
58
- // Rust links with MSVC (msvc host) or MinGW's gcc (GNU host): whichever it uses goes first.
59
- { name: 'Rust', on: wanted(config.rust.enabled, 'rust'), after: ['C/C++', 'MSVC'], run: async (env) => { await setupRust(env, { ...config.rust, host: rustHost(config) }, o.refresh); Object.assign(env.vars, rustCaches(config.rust)); } },
60
- { name: '.NET', on: wanted(config.dotnet.enabled, 'dotnet'), run: (env) => setupDotnet(env, config.dotnet, o.refresh) },
61
- { name: 'Go', on: wanted(config.go.enabled, 'go'), run: (env) => setupGo(env, config.go) },
62
- { name: 'Java', on: java, run: (env) => setupJava(env, config.java) },
63
- { name: 'Ruby', on: wanted(config.ruby.enabled, 'ruby'), run: (env) => setupRuby(env, config.ruby) },
64
- // PHP installs the Visual C++ runtime itself: one installer at a time.
65
- { name: 'Visual C++ runtime', on: config.vcRuntime.enabled, run: async (env) => { await setupVcRuntime(env); } },
66
- { name: 'PHP', on: wanted(config.php.enabled, 'php'), after: ['Visual C++ runtime'], run: (env) => setupPhp(env, config.php) },
67
- { name: 'Deno', on: wanted(config.deno.enabled, 'deno'), run: (env) => setupDeno(env) },
68
- { name: 'Bun', on: config.bun.enabled, run: (env) => setupBun(env) },
69
- { name: 'uv', on: config.uv.enabled, run: (env) => setupUv(env) },
70
- { name: 'PowerShell 7', on: wanted(config.pwsh.enabled, 'pwsh'), run: (env) => setupPwsh(env, o.refresh) },
71
- // The openssl tool comes from Git's install: after Git.
72
- { name: 'CLI tools', on: Object.values(config.cli).some((v) => v !== false), after: ['Git'], run: (env) => setupCli(env, config.cli, o.gitAccess, o.refresh) },
73
- { name: 'Mesa 3D', on: config.mesa.enabled, run: (env) => setupMesa(env, config.mesa) },
74
- { name: 'VS Code', on: config.vscode.enabled, run: (env) => setupVscode(env, o.refresh) },
75
- { name: 'Python tools', on: wanted(config.python.enabled, 'python') && config.python.tools.length > 0, after: ['Python', 'uv'], run: (env) => setupPythonTools(env, config.python.tools) },
76
- // DynamoDB Local runs on Java: the Java step installs the JDK first instead of racing it.
77
- ...o.databases.map((db) => ({ name: DATABASE_NAMES[db.database], on: true, after: db.database === 'dynamodb' && java ? ['Java'] : [], run: (env) => setupDatabase(env, db) })),
78
- ];
6
+ /** One lock per kind of step ("Node 20" and "Node 22" share nvm's folders): held while that step runs, in any run. */
7
+ const locks = new Map();
8
+ const lockKey = (name) => name.split(' ')[0].toLowerCase();
9
+ const exclusive = async (name, work) => {
10
+ const key = lockKey(name);
11
+ const previous = locks.get(key) ?? Promise.resolve();
12
+ let release = () => undefined;
13
+ const mine = new Promise((r) => { release = r; });
14
+ const chained = previous.then(() => mine);
15
+ locks.set(key, chained);
16
+ await previous;
17
+ try {
18
+ return await work();
19
+ }
20
+ finally {
21
+ release();
22
+ if (locks.get(key) === chained)
23
+ locks.delete(key);
24
+ }
79
25
  };
80
- /** Provisions every enabled toolchain and database, and returns the combined environment for the agent. */
81
- export const setupToolchains = async (config, versions, options) => {
82
- const steps = plan(config, versions, options).filter((s) => s.on);
26
+ /** Runs the steps (each with its own environment) at most PARALLEL at a time, after the steps they depend on. */
27
+ const runSteps = async (steps, o) => {
83
28
  const envs = new Map(steps.map((s) => [s.name, emptyEnv()]));
84
29
  const finished = new Map();
85
30
  const running = new Set();
86
31
  const waiting = [];
87
32
  let done = 0;
33
+ const say = o.report ?? ((message) => setSandboxStatus('provisioning', message));
88
34
  const report = () => { if (running.size)
89
- setSandboxStatus('provisioning', 'Installing ' + [...running].join(', ') + ' (' + done + ' of ' + steps.length + ' done)'); };
35
+ say('Installing ' + [...running].join(', ') + ' (' + done + ' of ' + steps.length + ' done)'); };
90
36
  for (const s of steps) {
91
37
  // Dependencies first (they are earlier in the plan, so their promises exist), then a free slot.
92
38
  finished.set(s.name, (async () => {
93
39
  await Promise.all((s.after ?? []).map((name) => finished.get(name)));
40
+ const reused = s.shared ? o.reuse?.[s.name] : undefined;
41
+ if (reused) {
42
+ envs.set(s.name, structuredClone(reused));
43
+ done++;
44
+ return;
45
+ }
94
46
  while (running.size >= PARALLEL)
95
47
  await new Promise((r) => waiting.push(r));
96
48
  running.add(s.name);
97
49
  report();
98
50
  try {
99
- await step(s.name, envs.get(s.name), async () => s.run(envs.get(s.name)));
51
+ await exclusive(s.name, () => step(s.name, envs.get(s.name), async () => s.run(envs.get(s.name))));
100
52
  }
101
53
  finally {
102
54
  running.delete(s.name);
@@ -107,8 +59,14 @@ export const setupToolchains = async (config, versions, options) => {
107
59
  })());
108
60
  }
109
61
  await Promise.all(finished.values());
62
+ return envs;
63
+ };
64
+ /** Provisions every enabled toolchain and database, and returns the combined environment for the agent. */
65
+ export const setupToolchains = async (config, versions, options, run = {}) => {
66
+ const steps = plan(config, versions, options).filter((s) => s.on);
67
+ const envs = await runSteps(steps, run);
110
68
  // Merged in plan order, so PATH order never depends on which download finished first.
111
- const env = emptyEnv();
69
+ const env = { ...emptyEnv(), steps: Object.fromEntries(envs) };
112
70
  env.prepend.push(mkdirp(tool('bin')));
113
71
  for (const part of envs.values()) {
114
72
  Object.assign(env.vars, part.vars);
@@ -4,7 +4,7 @@ import fs from 'node:fs';
4
4
  import path from 'node:path';
5
5
  import { fetchJson, fetchText, installArchive } from '../../shared/download.js';
6
6
  import { dryRun, errMsg, exists, log, warn } from '../env.js';
7
- import { arch, resolveCached, tool, versionFromFiles } from './common.js';
7
+ import { arch, emptyEnv, resolveCached, tool, versionFromFiles } from './common.js';
8
8
  import { javaMajorFrom, pickPackageVersion } from './versions.js';
9
9
  const ADOPTIUM = 'https://api.adoptium.net/v3';
10
10
  const MAVEN = 'https://repo.maven.apache.org/maven2/org/apache/maven/apache-maven/';
@@ -29,7 +29,7 @@ const setupJdk = async (env, config) => {
29
29
  const home = tool('java', release.replace(/[^A-Za-z0-9.-]/g, '_'));
30
30
  if (!exists(path.join(home, 'bin', 'java.exe'))) {
31
31
  const asset = await temurin(major);
32
- log('Downloading ' + asset.binary.package.name + ' ...');
32
+ log('Downloading Java ' + release + ' (JDK, ~200 MB once) ...');
33
33
  await installArchive(asset.binary.package.link, home, { sha256: asset.binary.package.checksum, locate: 'release', fileName: asset.binary.package.name });
34
34
  }
35
35
  env.vars.JAVA_HOME = home;
@@ -45,7 +45,7 @@ export const ensureJdk = async () => {
45
45
  const installed = exists(root) ? fs.readdirSync(root).filter((d) => exists(path.join(root, d, 'bin', 'java.exe'))).sort((a, b) => b.localeCompare(a, undefined, { numeric: true })) : [];
46
46
  if (installed.length)
47
47
  return path.join(root, installed[0]);
48
- const scratch = { vars: {}, prepend: [], append: [], summary: [] };
48
+ const scratch = emptyEnv();
49
49
  await setupJdk(scratch, { enabled: true, version: 'lts', maven: false, gradle: false });
50
50
  return scratch.vars.JAVA_HOME;
51
51
  };
@@ -57,7 +57,7 @@ const setupMaven = async (env) => {
57
57
  const home = tool('maven', version);
58
58
  if (!exists(path.join(home, 'bin', 'mvn.cmd'))) {
59
59
  const url = MAVEN + version + '/apache-maven-' + version + '-bin.zip';
60
- log('Downloading Maven ' + version + ' ...');
60
+ log('Downloading Maven ' + version + ' (~10 MB once) ...');
61
61
  await installArchive(url, home, { sha512: (await fetchText(url + '.sha512')).trim().split(/\s+/)[0], locate: 'LICENSE' });
62
62
  }
63
63
  const repo = tool('maven-repository');
@@ -72,7 +72,7 @@ const setupGradle = async (env) => {
72
72
  if (!exists(path.join(home, 'bin', 'gradle.bat'))) {
73
73
  if (!current || current.version !== version)
74
74
  throw new Error('Gradle ' + version + ' is not installed and cannot be downloaded right now');
75
- log('Downloading Gradle ' + version + ' ...');
75
+ log('Downloading Gradle ' + version + ' (~130 MB once) ...');
76
76
  await installArchive(current.downloadUrl, home, { sha256: (await fetchText(current.checksumUrl)).trim(), locate: 'LICENSE', fileName: 'gradle-' + version + '.zip' });
77
77
  }
78
78
  const userHome = tool('gradle-home');
@@ -0,0 +1,2 @@
1
+ /** Removes helper builds older versions left in persistent storage, and interrupted installs' staging folders. */
2
+ export declare const pruneToolLeftovers: () => void;
@@ -0,0 +1,26 @@
1
+ // Clean-up of what interrupted or older installs left in the tools folder.
2
+ import fs from 'node:fs';
3
+ import path from 'node:path';
4
+ import { safeRemove } from '../../shared/safe-remove.js';
5
+ import { toolsDir } from '../env.js';
6
+ const STAGING = /\.staging-[0-9a-f]+$/i;
7
+ const entries = (dir) => { try {
8
+ return fs.readdirSync(dir, { withFileTypes: true });
9
+ }
10
+ catch {
11
+ return [];
12
+ } };
13
+ /** Removes helper builds older versions left in persistent storage, and interrupted installs' staging folders. */
14
+ export const pruneToolLeftovers = () => {
15
+ for (const legacy of ['ptyhost', 'gitshim'])
16
+ safeRemove(path.join(toolsDir, legacy), toolsDir);
17
+ // Staging folders sit next to their destination: tools\x.staging-ab12 and one level down (tools\nvm\v22.staging-ab12).
18
+ for (const top of entries(toolsDir)) {
19
+ const dir = path.join(toolsDir, top.name);
20
+ if (STAGING.test(top.name))
21
+ safeRemove(dir, toolsDir);
22
+ else if (top.isDirectory())
23
+ for (const sub of entries(dir).filter((e) => STAGING.test(e.name)))
24
+ safeRemove(path.join(dir, sub.name), dir);
25
+ }
26
+ };
@@ -2,13 +2,14 @@
2
2
  // the Python step still provide the default versions; these come along when the project or this PC uses them.
3
3
  import fs from 'node:fs';
4
4
  import path from 'node:path';
5
- import { githubAsset, installArchive, latestGithubTag } from '../../shared/download.js';
6
- import { dryRun, errMsg, execAsync, exists, log, mkdirp, session, warn, workspace } from '../env.js';
7
- import { arch, resolveCached, tool, versionOf } from './common.js';
5
+ import { installArchive, latestGithubTag } from '../../shared/download.js';
6
+ import { dryRun, errMsg, execAsync, exists, log, mkdirp, session, warn } from '../env.js';
7
+ import { projectWorkspace } from '../project-context.js';
8
+ import { arch, installFromGithub, resolveCached, tool, versionOf } from './common.js';
8
9
  /** The "volta" block of the workspace's package.json ({ node, npm, yarn, pnpm }), if any. */
9
10
  export const voltaPins = () => {
10
11
  try {
11
- const pkg = JSON.parse(fs.readFileSync(path.join(workspace, 'package.json'), 'utf8').replace(/^/, ''));
12
+ const pkg = JSON.parse(fs.readFileSync(path.join(projectWorkspace(), 'package.json'), 'utf8').replace(/^/, ''));
12
13
  return Object.fromEntries(Object.entries(pkg.volta ?? {}).filter((e) => ['node', 'npm', 'yarn', 'pnpm'].includes(e[0]) && typeof e[1] === 'string'));
13
14
  }
14
15
  catch {
@@ -28,9 +29,8 @@ export const setupVolta = async (env) => {
28
29
  }
29
30
  const dir = tool('volta');
30
31
  if (!exists(path.join(dir, 'volta.exe'))) {
31
- const asset = await githubAsset('volta-cli/volta', arch === 'arm64' ? /^volta-[\d.]+-windows-arm64\.zip$/ : /^volta-[\d.]+-windows\.zip$/);
32
- log('Downloading ' + asset.name + ' ...');
33
- await installArchive(asset.url, dir, { sha256: asset.sha256, locate: 'volta.exe', fileName: asset.name });
32
+ const pattern = arch === 'arm64' ? /^volta-[\d.]+-windows-arm64\.zip$/ : /^volta-[\d.]+-windows\.zip$/;
33
+ await installFromGithub('volta-cli/volta', pattern, { dest: dir, what: 'Volta (~15 MB once)', locate: 'volta.exe' });
34
34
  }
35
35
  // The MSI puts these shims (copies of volta-shim.exe) next to volta.exe; the portable zip leaves that to us.
36
36
  for (const shim of ['node', 'npm', 'npx', 'pnpm', 'pnpx', 'yarn', 'yarnpkg']) {
@@ -51,13 +51,15 @@ export const setupVolta = async (env) => {
51
51
  // `volta install` fetches the pinned versions now (not on the agent's first `node`) and makes them the defaults.
52
52
  for (const [toolName, version] of Object.entries(pins)) {
53
53
  try {
54
- await execAsync(volta, ['install', toolName + '@' + version], { env: { ...process.env, ...vars }, cwd: workspace, stdio: 'ignore' });
54
+ await execAsync(volta, ['install', toolName + '@' + version], { env: { ...process.env, ...vars }, cwd: projectWorkspace(), stdio: 'ignore' });
55
55
  }
56
56
  catch (e) {
57
57
  warn('Volta could not install ' + toolName + '@' + version + ': ' + errMsg(e));
58
58
  }
59
59
  }
60
- env.summary.push('volta: ' + versionOf(volta) + (Object.keys(pins).length ? ' (pins from package.json: ' + Object.entries(pins).map(([k, v]) => k + '@' + v).join(', ') + ')' : ' (no pins in this project; nvm\'s node is the default)'));
60
+ const pinned = Object.entries(pins).map(([k, v]) => k + '@' + v);
61
+ const note = pinned.length ? ' (pins from package.json: ' + pinned.join(', ') + ')' : ' (no pins in this project; nvm\'s node is the default)';
62
+ env.summary.push('volta: ' + versionOf(volta) + note);
61
63
  };
62
64
  /**
63
65
  * pyenv-win from its source archive (it ships no release assets). Its versions persist in tools\pyenv. It comes after
@@ -71,7 +73,7 @@ export const setupPyenv = async (env) => {
71
73
  const root = tool('pyenv', 'pyenv-win');
72
74
  if (!exists(path.join(root, 'bin', 'pyenv.bat'))) {
73
75
  const tag = await resolveCached('pyenv-win', () => latestGithubTag('pyenv-win/pyenv-win'));
74
- log('Downloading pyenv-win ' + tag + ' ...');
76
+ log('Downloading pyenv-win ' + tag + ' (~5 MB once) ...');
75
77
  // GitHub's tag archive: no published checksum, but HTTPS from github.com for the project's own tag.
76
78
  await installArchive('https://github.com/pyenv-win/pyenv-win/archive/refs/tags/' + encodeURIComponent(tag) + '.zip', root, { locate: 'pyenv.bat', up: 1, fileName: 'pyenv-win.zip' });
77
79
  }
@@ -2,10 +2,9 @@
2
2
  // published Windows build in seconds; only crates without one are compiled with `cargo install --locked`.
3
3
  import fs from 'node:fs';
4
4
  import path from 'node:path';
5
- import { githubAsset, installArchive } from '../../shared/download.js';
6
- import { errMsg, execAsync, exists, log, warn } from '../env.js';
7
- import { binstallAsset } from './catalogue.js';
8
- import { arch, tool } from './common.js';
5
+ import { errMsg, execAsync, exists, log, warn } from '../../env.js';
6
+ import { binstallAsset } from '../catalogue.js';
7
+ import { arch, installFromGithub, tool } from '../common.js';
9
8
  /**
10
9
  * cargo-binstall.exe into CARGO_HOME\bin (so `cargo binstall` works for the agent too). Downloaded into its own
11
10
  * tools\cargo-binstall folder first: installArchive replaces its destination, which must never be cargo's bin.
@@ -17,9 +16,7 @@ const ensureBinstall = async (cargoBin) => {
17
16
  try {
18
17
  const dir = tool('cargo-binstall');
19
18
  if (!exists(path.join(dir, 'cargo-binstall.exe'))) {
20
- const asset = await githubAsset('cargo-bins/cargo-binstall', binstallAsset(arch));
21
- log('Downloading ' + asset.name + ' (prebuilt Rust tools) ...');
22
- await installArchive(asset.url, dir, { sha256: asset.sha256, locate: 'cargo-binstall.exe', fileName: asset.name });
19
+ await installFromGithub('cargo-bins/cargo-binstall', binstallAsset(arch), { dest: dir, what: 'cargo-binstall for prebuilt Rust tools (~10 MB once)', locate: 'cargo-binstall.exe' });
23
20
  }
24
21
  fs.copyFileSync(path.join(dir, 'cargo-binstall.exe'), target);
25
22
  return true;
@@ -46,8 +43,8 @@ export const crateInstaller = (cargoHome, cargoEnv) => {
46
43
  await execAsync(cargo, ['binstall', '--no-confirm', '--locked', '--disable-strategies', 'compile', crate], binstallEnv);
47
44
  return;
48
45
  }
49
- catch (e) {
50
- log('No prebuilt ' + crate + ' for Windows (' + errMsg(e) + '); compiling it with cargo install ...');
46
+ catch {
47
+ log('No prebuilt ' + crate + ' for Windows; compiling it with cargo (takes a few minutes) ...');
51
48
  }
52
49
  }
53
50
  await execAsync(cargo, ['install', '--locked', crate], cargoEnv);
@@ -0,0 +1,4 @@
1
+ import type { Toolchains } from '../../../core/types.js';
2
+ import { type ToolEnv } from '../common.js';
3
+ /** MinGW-w64 GCC from WinLibs: gcc/g++/mingw32-make for the agent, and the linker for Rust's GNU host. */
4
+ export declare const setupCpp: (env: ToolEnv, config: Toolchains["cpp"]) => Promise<void>;
@@ -0,0 +1,38 @@
1
+ // C/C++: MinGW-w64 GCC (with CMake, Ninja, make) and the flex/bison parser generators.
2
+ import path from 'node:path';
3
+ import { dryRun, exists, log, warn } from '../../env.js';
4
+ import { arch, installFromGithub, tool, versionOf } from '../common.js';
5
+ import { CMAKE_FLOOR, cmakeTooOld } from '../versions.js';
6
+ /** MinGW-w64 GCC from WinLibs: gcc/g++/mingw32-make for the agent, and the linker for Rust's GNU host. */
7
+ export const setupCpp = async (env, config) => {
8
+ const bin = tool('mingw', 'mingw64', 'bin');
9
+ const gcc = path.join(bin, 'gcc.exe');
10
+ if (dryRun) {
11
+ log('(dry run) would provision MinGW-w64 GCC');
12
+ return;
13
+ }
14
+ if (!exists(gcc)) {
15
+ const pattern = arch === 'arm64' ? /^winlibs-aarch64-.*\.zip$/ : /^winlibs-x86_64-posix-seh-gcc-.*\.zip$/;
16
+ await installFromGithub('brechtsanders/winlibs_mingw', pattern, { dest: tool('mingw'), what: 'the C/C++ compiler, MinGW-w64 GCC with CMake and Ninja (~270 MB once)', searchOlder: true });
17
+ }
18
+ if (!exists(gcc))
19
+ throw new Error('gcc.exe missing after installing MinGW');
20
+ // Appended (not prepended) so MinGW's tools never shadow Windows or toolchain commands of the same name.
21
+ env.append.unshift(bin);
22
+ env.vars.CARGO_TARGET_X86_64_PC_WINDOWS_GNU_LINKER = gcc;
23
+ const cmake = versionOf(path.join(bin, 'cmake.exe'));
24
+ if (cmakeTooOld(cmake))
25
+ warn('The bundled CMake ' + cmake + ' is older than ' + CMAKE_FLOOR + ', which many projects need; set toolchains.cpp.enabled to refresh it, or install a newer CMake into C:\\AgentData\\tools\\bin.');
26
+ env.summary.push('gcc: ' + versionOf(gcc) + ' (MinGW-w64, with cmake ' + cmake + ', ninja, premake5)');
27
+ if (config.flexBison)
28
+ await setupFlexBison(env);
29
+ };
30
+ /** win_flex / win_bison (CMake's FindFLEX/FindBISON look for these names). Kept with the C/C++ tools on PATH. */
31
+ const setupFlexBison = async (env) => {
32
+ const dir = tool('winflexbison');
33
+ if (!exists(path.join(dir, 'win_bison.exe'))) {
34
+ await installFromGithub('lexxmark/winflexbison', /^win_flex_bison-[\d.]+\.zip$/, { dest: dir, what: 'win_flex / win_bison (~1 MB once)', locate: 'win_bison.exe' });
35
+ }
36
+ env.append.push(dir);
37
+ env.summary.push('win_flex/win_bison: ' + versionOf(path.join(dir, 'win_bison.exe')));
38
+ };
@@ -0,0 +1,4 @@
1
+ import type { Toolchains } from '../../../core/types.js';
2
+ import { type ToolEnv } from '../common.js';
3
+ /** .NET SDK channels from config plus whatever the workspace's global.json pins. Installs are remembered in a marker. */
4
+ export declare const setupDotnet: (env: ToolEnv, config: Toolchains["dotnet"], refresh: boolean) => Promise<void>;