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,22 @@
1
+ import type { DatabasePlan, GitAccess, Toolchains, ToolVersions } from '../../core/types.js';
2
+ import { type ToolEnv } from './common.js';
3
+ export interface ToolchainOptions {
4
+ gitEnabled: boolean;
5
+ gitAccess: GitAccess;
6
+ hostGit: boolean;
7
+ refresh: boolean;
8
+ databases: DatabasePlan[];
9
+ }
10
+ /**
11
+ * shared: the step's effect is machine-wide and the same for every project (an installer, a registry entry, a
12
+ * shortcut, the git guard). A project added after boot reuses what the boot's run contributed instead of running it
13
+ * again under the sessions that already use it.
14
+ */
15
+ export interface Step {
16
+ name: string;
17
+ on: boolean;
18
+ after?: string[];
19
+ shared?: boolean;
20
+ run: (env: ToolEnv) => Promise<void> | void;
21
+ }
22
+ export declare const plan: (config: Toolchains, versions: ToolVersions, o: ToolchainOptions) => Step[];
@@ -0,0 +1,96 @@
1
+ // Which toolchain and database steps this sandbox runs, in the order their PATH entries are listed, and what each
2
+ // step depends on. The scheduler (index.ts) runs them.
3
+ import path from 'node:path';
4
+ import { dataDir, execAsync, exists, localDir } from '../env.js';
5
+ import { projectWorkspace } from '../project-context.js';
6
+ import { arch, installOnce, tool, wanted } from './common.js';
7
+ import { DATABASE_NAMES, setupDatabase } from './databases/index.js';
8
+ import { setupGit } from './git.js';
9
+ import { setupGo } from './go.js';
10
+ import { setupJava } from './java.js';
11
+ import { setupCli } from './cli.js';
12
+ import { setupPyenv, setupVolta, wantsPyenv, wantsVolta } from './managers.js';
13
+ import { setupCpp } from './native/cpp.js';
14
+ import { setupDotnet } from './native/dotnet.js';
15
+ import { setupMesa } from './native/mesa.js';
16
+ import { setupMsvc } from './native/msvc/index.js';
17
+ import { setupRust } from './native/rust.js';
18
+ import { setupNode } from './node.js';
19
+ import { setupPwsh } from './pwsh.js';
20
+ import { setupPython } from './python.js';
21
+ import { setupBun, setupDeno, setupUv, uvVars } from './runtimes.js';
22
+ import { setupPhp } from './scripting/php.js';
23
+ import { setupRuby } from './scripting/ruby.js';
24
+ import { setupVcRuntime } from './scripting/vcruntime.js';
25
+ import { setupVscode } from './vscode.js';
26
+ /** Python command-line tools through uv (each in its own environment, commands in uv's bin folder on PATH). */
27
+ const setupPythonTools = async (env, tools) => {
28
+ const uv = tool('uv', 'uv.exe');
29
+ if (!exists(uv))
30
+ throw new Error('toolchains.python.tools needs uv (toolchains.uv.enabled)');
31
+ await installOnce(env, 'uv tool', tools, (pkg) => execAsync(uv, ['tool', 'install', pkg], { env: { ...process.env, ...uvVars() } }));
32
+ };
33
+ /** Where cargo builds: shared with the PC's target\, a separate folder next to it, one on the sandbox's disk, or a path. */
34
+ const cargoTarget = (targetDir) => {
35
+ if (targetDir === 'shared')
36
+ return null;
37
+ if (targetDir === 'separate')
38
+ return path.join(projectWorkspace(), 'target', 'sandbox');
39
+ if (targetDir === 'local')
40
+ return path.join(localDir, 'cargo-target');
41
+ return targetDir;
42
+ };
43
+ /**
44
+ * Where cargo builds and where sccache keeps its cache. Cargo fingerprints hold absolute paths and the host toolchain,
45
+ * so a target\\ shared with builds on the PC rebuilds every time the side changes: "separate" (the default) keeps a
46
+ * sandbox one next to it. sccache's cache persists unless the project sets SCCACHE_DIR itself.
47
+ */
48
+ const rustCaches = (rust) => {
49
+ const target = cargoTarget(rust.targetDir);
50
+ return { ...(target ? { CARGO_TARGET_DIR: target } : {}), ...(process.env.SCCACHE_DIR ? {} : { SCCACHE_DIR: path.join(dataDir, 'cache', 'sccache') }) };
51
+ };
52
+ /** "auto": the msvc host when the MSVC step runs (as on most PCs), else GNU with MinGW. */
53
+ const rustHost = (config) => {
54
+ if (config.rust.host !== 'auto')
55
+ return config.rust.host;
56
+ const cpu = arch === 'arm64' ? 'aarch64' : 'x86_64';
57
+ return cpu + (wanted(config.msvc.enabled, 'msvc') ? '-pc-windows-msvc' : '-pc-windows-gnu');
58
+ };
59
+ export const plan = (config, versions, o) => {
60
+ const java = wanted(config.java.enabled, 'java');
61
+ const python = wanted(config.python.enabled, 'python');
62
+ return [
63
+ { name: 'Git', on: o.gitEnabled, shared: true, run: (env) => setupGit(env, o.hostGit, o.refresh, o.gitAccess) },
64
+ // Before Node: with package.json pins, Volta's node/npm/pnpm/yarn come first on PATH.
65
+ { name: 'Volta', on: wantsVolta(config.node.volta), run: (env) => setupVolta(env) },
66
+ { name: 'Node ' + versions.node.spec, on: true, run: (env) => setupNode(env, versions, config.node) },
67
+ { name: 'Python', on: python, run: (env) => setupPython(env, config.python, versions) },
68
+ { name: 'pyenv-win', on: python && wantsPyenv(config.python.pyenv), run: (env) => setupPyenv(env) },
69
+ { name: 'C/C++', on: wanted(config.cpp.enabled, 'cpp') || wanted(config.rust.enabled, 'rust'), run: (env) => setupCpp(env, config.cpp) },
70
+ { name: 'MSVC', on: wanted(config.msvc.enabled, 'msvc'), run: (env) => setupMsvc(env, config.msvc) },
71
+ // Rust links with MSVC (msvc host) or MinGW's gcc (GNU host): whichever it uses goes first.
72
+ {
73
+ name: 'Rust', on: wanted(config.rust.enabled, 'rust'), after: ['C/C++', 'MSVC'],
74
+ run: async (env) => { await setupRust(env, { ...config.rust, host: rustHost(config) }, o.refresh); Object.assign(env.vars, rustCaches(config.rust)); },
75
+ },
76
+ { name: '.NET', on: wanted(config.dotnet.enabled, 'dotnet'), run: (env) => setupDotnet(env, config.dotnet, o.refresh) },
77
+ { name: 'Go', on: wanted(config.go.enabled, 'go'), run: (env) => setupGo(env, config.go) },
78
+ { name: 'Java', on: java, run: (env) => setupJava(env, config.java) },
79
+ { name: 'Ruby', on: wanted(config.ruby.enabled, 'ruby'), run: (env) => setupRuby(env, config.ruby) },
80
+ // PHP installs the Visual C++ runtime itself: one installer at a time.
81
+ { name: 'Visual C++ runtime', on: config.vcRuntime.enabled, shared: true, run: async (env) => { await setupVcRuntime(env); } },
82
+ { name: 'PHP', on: wanted(config.php.enabled, 'php'), after: ['Visual C++ runtime'], run: (env) => setupPhp(env, config.php) },
83
+ { name: 'Deno', on: wanted(config.deno.enabled, 'deno'), run: (env) => setupDeno(env) },
84
+ { name: 'Bun', on: config.bun.enabled, run: (env) => setupBun(env) },
85
+ { name: 'uv', on: config.uv.enabled, run: (env) => setupUv(env) },
86
+ { name: 'PowerShell 7', on: wanted(config.pwsh.enabled, 'pwsh'), run: (env) => setupPwsh(env, o.refresh) },
87
+ // The openssl tool comes from Git's install: after Git.
88
+ { name: 'CLI tools', on: Object.values(config.cli).some((v) => v !== false), after: ['Git'], run: (env) => setupCli(env, config.cli, o.gitAccess, o.refresh) },
89
+ { name: 'Mesa 3D', on: config.mesa.enabled, shared: true, run: (env) => setupMesa(env, config.mesa) },
90
+ { name: 'VS Code', on: config.vscode.enabled, shared: true, run: (env) => setupVscode(env, o.refresh) },
91
+ { name: 'Python tools', on: python && config.python.tools.length > 0, after: ['Python', 'uv'], run: (env) => setupPythonTools(env, config.python.tools) },
92
+ // DynamoDB Local runs on Java: the Java step installs the JDK first instead of racing it.
93
+ // One server per database in the sandbox: a project that joins later shares the one the boot started.
94
+ ...o.databases.map((db) => ({ name: DATABASE_NAMES[db.database], on: true, shared: true, after: db.database === 'dynamodb' && java ? ['Java'] : [], run: (env) => setupDatabase(env, db) })),
95
+ ];
96
+ };
@@ -8,7 +8,7 @@ import { setupSingleExe } from './runtimes.js';
8
8
  export const pwshDir = () => tool('pwsh');
9
9
  export const setupPwsh = async (env, refresh = false) => {
10
10
  // pwsh.exe sits at the zip's root next to its runtime and modules: the whole folder is kept.
11
- await setupSingleExe(env, 'pwsh', 'PowerShell/PowerShell', pwshAsset(arch), 'pwsh.exe', pwshDir(), { refresh, versionArgs: ['-Version'], note: ' (PowerShell 7; powershell is still 5.1)' });
11
+ await setupSingleExe(env, 'pwsh', 'PowerShell/PowerShell', pwshAsset(arch), 'pwsh.exe', pwshDir(), { refresh, size: '100 MB', versionArgs: ['-Version'], note: ' (PowerShell 7; powershell is still 5.1)' });
12
12
  // No update banner or telemetry from a copy that provisioning.refresh replaces anyway.
13
13
  Object.assign(env.vars, { POWERSHELL_UPDATECHECK: 'Off', POWERSHELL_TELEMETRY_OPTOUT: '1' });
14
14
  };
@@ -5,6 +5,7 @@ import { fetchJson, installArchive } from '../../shared/download.js';
5
5
  import { dryRun, execAsync, exists, log, mkdirp } from '../env.js';
6
6
  import { resolveCached, tool } from './common.js';
7
7
  import { pickPackageVersion } from './versions.js';
8
+ import { duringSession } from '../project-context.js';
8
9
  const installPython = async (pkg, spec) => {
9
10
  const version = await resolveCached(pkg + ':' + spec, async () => {
10
11
  const { versions } = await fetchJson('https://api.nuget.org/v3-flatcontainer/' + pkg + '/index.json');
@@ -13,21 +14,26 @@ const installPython = async (pkg, spec) => {
13
14
  const dir = tool('python', pkg + '-' + version);
14
15
  const exe = path.join(dir, 'python.exe');
15
16
  if (!exists(exe)) {
16
- log('Downloading ' + pkg + ' ' + version + ' ...');
17
+ log('Downloading Python ' + version + (pkg === 'python2' ? ' (Python 2, ' : ' (') + '~30 MB once) ...');
17
18
  await installArchive('https://api.nuget.org/v3-flatcontainer/' + pkg + '/' + version + '/' + pkg + '.' + version + '.nupkg', dir, { locate: 'python.exe', fileName: pkg + '.' + version + '.zip' });
18
19
  }
19
20
  if (!exists(path.join(dir, 'Scripts', 'pip.exe')) && !exists(path.join(dir, 'Scripts', 'pip2.exe')))
20
21
  await execAsync(exe, ['-m', 'ensurepip', '--default-pip'], { stdio: 'ignore' });
21
22
  return dir;
22
23
  };
23
- /** Writes `<name>.cmd` into tools\shims, e.g. python2.cmd. */
24
- const shim = (name, command) => {
25
- fs.writeFileSync(path.join(mkdirp(tool('shims')), name + '.cmd'), '@echo off\r\n' + command + ' %*\r\n');
24
+ /**
25
+ * The shims folder: tools\shims at boot. A project added later gets one per Python version (tools\shims\<version>)
26
+ * so the boot project's python3.cmd keeps pointing at its own version.
27
+ */
28
+ const shimDir = (dir) => (duringSession() ? tool('shims', path.basename(dir)) : tool('shims'));
29
+ /** Writes `<name>.cmd` into the shims folder, e.g. python2.cmd. */
30
+ const shim = (folder, name, command) => {
31
+ fs.writeFileSync(path.join(mkdirp(folder), name + '.cmd'), '@echo off\r\n' + command + ' %*\r\n');
26
32
  };
27
- const shimPair = (version, dir) => {
33
+ const shimPair = (folder, version, dir) => {
28
34
  const python = '"' + path.join(dir, 'python.exe') + '"';
29
- shim('python' + version, python);
30
- shim('pip' + version, python + ' -m pip');
35
+ shim(folder, 'python' + version, python);
36
+ shim(folder, 'pip' + version, python + ' -m pip');
31
37
  };
32
38
  export const setupPython = async (env, config, versions) => {
33
39
  if (dryRun) {
@@ -35,13 +41,14 @@ export const setupPython = async (env, config, versions) => {
35
41
  return;
36
42
  }
37
43
  const py3 = await installPython('python', versions.python.spec);
38
- shimPair('3', py3);
39
- env.prepend.push(py3, path.join(py3, 'Scripts'), tool('shims'));
44
+ const shims = shimDir(py3);
45
+ shimPair(shims, '3', py3);
46
+ env.prepend.push(py3, path.join(py3, 'Scripts'), shims);
40
47
  env.vars.PYTHONIOENCODING = 'utf-8';
41
48
  env.summary.push('python: ' + path.basename(py3).replace('python-', '') + ' (from ' + versions.python.source + ')');
42
49
  if (!config.python2)
43
50
  return;
44
51
  const py2 = await installPython('python2', '2.7');
45
- shimPair('2', py2);
52
+ shimPair(shims, '2', py2);
46
53
  env.summary.push('python2: ' + path.basename(py2).replace('python2-', ''));
47
54
  };
@@ -7,6 +7,8 @@ export interface SingleExeOptions {
7
7
  versionArgs?: string[];
8
8
  /** Appended to the summary line, e.g. " (PowerShell 7)". */
9
9
  note?: string;
10
+ /** Rough download size for the progress line, e.g. "35 MB". */
11
+ size?: string;
10
12
  }
11
13
  /** A tool from one GitHub release archive, into `dir` (atomically), on PATH. */
12
14
  export declare const setupSingleExe: (env: ToolEnv, name: string, repo: string, pattern: RegExp, exeName: string, dir: string, options?: SingleExeOptions) => Promise<void>;
@@ -1,8 +1,7 @@
1
1
  // Bun, uv and Deno: single-binary tools from GitHub releases (claude-mem needs Bun and uv).
2
2
  import path from 'node:path';
3
- import { githubAsset, installArchive } from '../../shared/download.js';
4
3
  import { dryRun, exists, localDir, log } from '../env.js';
5
- import { arch, tool, versionOf } from './common.js';
4
+ import { arch, installFromGithub, tool, versionOf } from './common.js';
6
5
  /** A tool from one GitHub release archive, into `dir` (atomically), on PATH. */
7
6
  export const setupSingleExe = async (env, name, repo, pattern, exeName, dir, options = {}) => {
8
7
  if (dryRun) {
@@ -12,9 +11,8 @@ export const setupSingleExe = async (env, name, repo, pattern, exeName, dir, opt
12
11
  const bin = options.subdir ? path.join(dir, options.subdir) : dir;
13
12
  const exe = path.join(bin, exeName);
14
13
  if (!exists(exe) || options.refresh) {
15
- const asset = await githubAsset(repo, pattern);
16
- log('Downloading ' + asset.name + ' ...');
17
- await installArchive(asset.url, dir, { sha256: asset.sha256, fileName: asset.name, ...(options.subdir ? {} : { locate: exeName }) });
14
+ const size = options.size ? ', ~' + options.size : '';
15
+ await installFromGithub(repo, pattern, { dest: dir, what: (a) => name + ' (' + a.name + size + ' once)', ...(options.subdir ? {} : { locate: exeName }) });
18
16
  }
19
17
  if (!exists(exe))
20
18
  throw new Error(exeName + ' missing after installing ' + name);
@@ -22,19 +20,19 @@ export const setupSingleExe = async (env, name, repo, pattern, exeName, dir, opt
22
20
  env.summary.push(name + ': ' + versionOf(exe, options.versionArgs) + (options.note ?? ''));
23
21
  };
24
22
  export const setupBun = async (env) => {
25
- await setupSingleExe(env, 'bun', 'oven-sh/bun', arch === 'arm64' ? /^bun-windows-aarch64\.zip$/ : /^bun-windows-x64\.zip$/, 'bun.exe', tool('bun', 'bin'));
23
+ await setupSingleExe(env, 'bun', 'oven-sh/bun', arch === 'arm64' ? /^bun-windows-aarch64\.zip$/ : /^bun-windows-x64\.zip$/, 'bun.exe', tool('bun', 'bin'), { size: '35 MB' });
26
24
  // Bun cannot keep its cache or temp files on a mapped folder (EINVAL); they go to the sandbox's own disk.
27
25
  Object.assign(env.vars, { BUN_INSTALL: tool('bun'), BUN_INSTALL_CACHE_DIR: path.join(localDir, 'bun-cache'), BUN_TMPDIR: path.join(localDir, 'bun-tmp') });
28
26
  };
29
27
  export const setupDeno = async (env) => {
30
- await setupSingleExe(env, 'deno', 'denoland/deno', arch === 'arm64' ? /^deno-aarch64-pc-windows-msvc[.]zip$/ : /^deno-x86_64-pc-windows-msvc[.]zip$/, 'deno.exe', tool('deno', 'bin'));
28
+ await setupSingleExe(env, 'deno', 'denoland/deno', arch === 'arm64' ? /^deno-aarch64-pc-windows-msvc[.]zip$/ : /^deno-x86_64-pc-windows-msvc[.]zip$/, 'deno.exe', tool('deno', 'bin'), { size: '40 MB' });
31
29
  // `deno install` puts commands in DENO_INSTALL_ROOT/bin: the folder deno.exe is in, already on PATH.
32
30
  Object.assign(env.vars, { DENO_DIR: tool('deno-cache'), DENO_INSTALL_ROOT: tool('deno'), DENO_NO_UPDATE_CHECK: '1' });
33
31
  };
34
32
  /** Where uv keeps its cache, Pythons and tools (persistent). */
35
33
  export const uvVars = () => ({ UV_CACHE_DIR: tool('uv-cache'), UV_PYTHON_INSTALL_DIR: tool('uv-python'), UV_TOOL_DIR: tool('uv-tools'), UV_TOOL_BIN_DIR: tool('uv-bin') });
36
34
  export const setupUv = async (env) => {
37
- await setupSingleExe(env, 'uv', 'astral-sh/uv', arch === 'arm64' ? /^uv-aarch64-pc-windows-msvc\.zip$/ : /^uv-x86_64-pc-windows-msvc\.zip$/, 'uv.exe', tool('uv'));
35
+ await setupSingleExe(env, 'uv', 'astral-sh/uv', arch === 'arm64' ? /^uv-aarch64-pc-windows-msvc\.zip$/ : /^uv-x86_64-pc-windows-msvc\.zip$/, 'uv.exe', tool('uv'), { size: '20 MB' });
38
36
  Object.assign(env.vars, uvVars());
39
37
  env.prepend.push(tool('uv-bin'));
40
38
  };
@@ -0,0 +1,3 @@
1
+ import type { Toolchains } from '../../../core/types.js';
2
+ import { type ToolEnv } from '../common.js';
3
+ export declare const setupPhp: (env: ToolEnv, config: Toolchains["php"]) => Promise<void>;
@@ -0,0 +1,71 @@
1
+ // PHP from windows.php.net, with Composer. Composer's global packages live in persistent storage.
2
+ import fs from 'node:fs';
3
+ import path from 'node:path';
4
+ import { fetchJson, fetchText, installArchive } from '../../../shared/download.js';
5
+ import { dryRun, execOutput, exists, log, mkdirp } from '../../env.js';
6
+ import { downloadFile, resolveCached, tool, versionFromFiles } from '../common.js';
7
+ import { pickPackageVersion } from '../versions.js';
8
+ import { setupVcRuntime } from './vcruntime.js';
9
+ const PHP_EXTENSIONS = ['curl', 'fileinfo', 'gd', 'intl', 'mbstring', 'openssl', 'pdo_mysql', 'pdo_pgsql', 'pdo_sqlite', 'sodium', 'sqlite3', 'zip'];
10
+ const COMPOSER = 'https://getcomposer.org/download/latest-stable/composer.phar';
11
+ /** The non-thread-safe x64 zip of the newest release matching `spec` ("8" -> newest 8.x, "8.3" -> newest 8.3.x). */
12
+ const phpBuild = async (spec) => {
13
+ const releases = await fetchJson('https://windows.php.net/downloads/releases/releases.json');
14
+ const pick = pickPackageVersion(Object.values(releases).map((r) => r.version), spec);
15
+ const release = Object.values(releases).find((r) => r.version === pick);
16
+ const build = release && Object.entries(release).find(([key, value]) => /^nts-vs\d+-x64$/.test(key) && typeof value === 'object' && value.zip)?.[1];
17
+ if (!release || typeof build !== 'object' || !build.zip)
18
+ throw new Error('No PHP build for Windows matches "' + spec + '"');
19
+ return { version: release.version, ...build.zip };
20
+ };
21
+ /** php.ini from the development template with the common extensions switched on (written whole, then renamed). */
22
+ const writePhpIni = (home) => {
23
+ const ini = path.join(home, 'php.ini');
24
+ if (exists(ini) || !exists(path.join(home, 'php.ini-development')))
25
+ return;
26
+ let text = fs.readFileSync(path.join(home, 'php.ini-development'), 'utf8')
27
+ .replace(/^;\s*extension_dir\s*=\s*"ext"/m, 'extension_dir = "' + path.join(home, 'ext') + '"');
28
+ for (const ext of PHP_EXTENSIONS.filter((e) => exists(path.join(home, 'ext', 'php_' + e + '.dll'))))
29
+ text = text.replace(new RegExp('^;(extension=' + ext + ')\\s*$', 'm'), '$1');
30
+ fs.writeFileSync(ini + '.tmp', text);
31
+ fs.renameSync(ini + '.tmp', ini);
32
+ };
33
+ /** Writes `<name>.cmd` into tools\shims (on PATH). */
34
+ const shim = (env, name, command) => {
35
+ fs.writeFileSync(path.join(mkdirp(tool('shims')), name + '.cmd'), '@echo off\r\n' + command + ' %*\r\n');
36
+ env.prepend.push(tool('shims'));
37
+ };
38
+ const setupComposer = async (env, php) => {
39
+ const phar = tool('composer', 'composer.phar');
40
+ if (!exists(phar)) {
41
+ log('Downloading Composer (~3 MB once) ...');
42
+ const sha256 = (await fetchText(COMPOSER + '.sha256sum')).trim().split(/\s+/)[0];
43
+ await downloadFile(COMPOSER, phar, sha256);
44
+ }
45
+ const composerHome = tool('composer-home');
46
+ env.vars.COMPOSER_HOME = composerHome;
47
+ env.prepend.push(path.join(composerHome, 'vendor', 'bin'));
48
+ shim(env, 'composer', '"' + php + '" "' + phar + '"');
49
+ env.summary.push('composer: ' + (/\d+\.\d+\.\d+/.exec(execOutput(php, [phar, '--version', '--no-ansi']))?.[0] ?? 'installed'));
50
+ };
51
+ export const setupPhp = async (env, config) => {
52
+ const wantedSpec = versionFromFiles(['.php-version']) ?? { spec: config.version, source: 'config' };
53
+ if (dryRun) {
54
+ log('(dry run) would provision PHP ' + wantedSpec.spec + (config.composer ? ' + Composer' : ''));
55
+ return;
56
+ }
57
+ // No summary line of its own: the Visual C++ runtime step (which runs first) already reports it.
58
+ await setupVcRuntime(env, false);
59
+ const version = await resolveCached('php:' + wantedSpec.spec, async () => (await phpBuild(wantedSpec.spec)).version);
60
+ const home = tool('php', version);
61
+ if (!exists(path.join(home, 'php.exe'))) {
62
+ const build = await phpBuild(version);
63
+ log('Downloading PHP ' + version + ' (~30 MB once) ...');
64
+ await installArchive('https://windows.php.net/downloads/releases/' + build.path, home, { sha256: build.sha256, locate: 'php.exe', fileName: build.path });
65
+ }
66
+ writePhpIni(home);
67
+ env.prepend.push(home);
68
+ env.summary.push('php: ' + version + ' (from ' + wantedSpec.source + ')');
69
+ if (config.composer)
70
+ await setupComposer(env, path.join(home, 'php.exe'));
71
+ };
@@ -0,0 +1,3 @@
1
+ import type { Toolchains } from '../../../core/types.js';
2
+ import { type ToolEnv } from '../common.js';
3
+ export declare const setupRuby: (env: ToolEnv, config: Toolchains["ruby"]) => Promise<void>;
@@ -0,0 +1,47 @@
1
+ // Ruby from RubyInstaller. Gems live in persistent storage.
2
+ import path from 'node:path';
3
+ import { fetchJson, installArchive } from '../../../shared/download.js';
4
+ import { SYSTEM } from '../../../shared/system.js';
5
+ import { dryRun, execAsync, exists, log } from '../../env.js';
6
+ import { installOnce, resolveCached, tool, versionFromFiles, versionOf } from '../common.js';
7
+ import { pickPackageVersion } from '../versions.js';
8
+ const RUBY_ASSET = /^rubyinstaller-(\d+\.\d+\.\d+)-(\d+)-x64\.7z$/;
9
+ /** The build number after the version in a RubyInstaller file name (rubyinstaller-3.3.1-1-x64.7z -> 1). */
10
+ const buildNumber = (asset) => Number(RUBY_ASSET.exec(asset.name)?.[2] ?? 0);
11
+ /** RubyInstaller builds per Ruby version (newest build of each), from its GitHub releases. */
12
+ const rubyBuilds = async () => {
13
+ const releases = await fetchJson('https://api.github.com/repos/oneclick/rubyinstaller2/releases?per_page=100');
14
+ const builds = new Map();
15
+ for (const asset of releases.filter((r) => !r.draft && !r.prerelease).flatMap((r) => r.assets)) {
16
+ const version = RUBY_ASSET.exec(asset.name)?.[1];
17
+ if (!version)
18
+ continue;
19
+ const known = builds.get(version);
20
+ if (!known || buildNumber(known) < buildNumber(asset))
21
+ builds.set(version, asset);
22
+ }
23
+ return builds;
24
+ };
25
+ const sha256Of = (asset) => (asset.digest?.startsWith('sha256:') ? asset.digest.slice(7) : null);
26
+ export const setupRuby = async (env, config) => {
27
+ const wantedSpec = versionFromFiles(['.ruby-version']) ?? { spec: config.version, source: 'config' };
28
+ if (dryRun) {
29
+ log('(dry run) would provision Ruby ' + wantedSpec.spec);
30
+ return;
31
+ }
32
+ const version = await resolveCached('ruby:' + wantedSpec.spec, async () => pickPackageVersion([...(await rubyBuilds()).keys()], wantedSpec.spec) ?? Promise.reject(new Error('No RubyInstaller build matches "' + wantedSpec.spec + '"')));
33
+ const home = tool('ruby', version);
34
+ if (!exists(path.join(home, 'bin', 'ruby.exe'))) {
35
+ const asset = (await rubyBuilds()).get(version);
36
+ if (!asset)
37
+ throw new Error('RubyInstaller ' + version + ' is no longer published');
38
+ log('Downloading Ruby ' + version + ' (~100 MB once) ...');
39
+ await installArchive(asset.browser_download_url, home, { sha256: sha256Of(asset), locate: 'ruby.exe', up: 1, fileName: asset.name });
40
+ }
41
+ const gems = tool('ruby-gems', version.split('.').slice(0, 2).join('.'));
42
+ Object.assign(env.vars, { GEM_HOME: gems, BUNDLE_USER_HOME: tool('bundle-home') });
43
+ env.prepend.push(path.join(home, 'bin'), path.join(gems, 'bin'));
44
+ env.summary.push('ruby: ' + versionOf(path.join(home, 'bin', 'ruby.exe')) + ' (from ' + wantedSpec.source + '; gems with C extensions need MSYS2, not included)');
45
+ const gem = path.join(home, 'bin', 'gem.cmd');
46
+ await installOnce(env, 'gem@' + version, config.gems, (pkg) => execAsync(SYSTEM.cmd(), ['/d', '/c', gem, 'install', pkg, '--no-document'], { env: { ...process.env, GEM_HOME: gems } }));
47
+ };
@@ -0,0 +1,7 @@
1
+ import { type ToolEnv } from '../common.js';
2
+ /**
3
+ * A real installer (it writes to System32), so it runs on every boot; the installer itself is kept in persistent
4
+ * storage and only run when Microsoft's signature on it checks out. Resolves true when this call ran the installer.
5
+ * Only the "Visual C++ runtime" step reports it (`summary`); PHP calls it again just to be sure.
6
+ */
7
+ export declare const setupVcRuntime: (env: ToolEnv, summary?: boolean) => Promise<boolean>;
@@ -0,0 +1,45 @@
1
+ // The Visual C++ runtime (vcruntime140.dll & co.) that PHP and other native tools need.
2
+ import fs from 'node:fs';
3
+ import path from 'node:path';
4
+ import { spawnAsync } from '../../../shared/download.js';
5
+ import { SYSTEM } from '../../../shared/system.js';
6
+ import { dryRun, execOutput, exists, log } from '../../env.js';
7
+ import { downloadFile, tool } from '../common.js';
8
+ const VC_REDIST = 'https://aka.ms/vs/17/release/vc_redist.x64.exe';
9
+ const MICROSOFT_SIGNER = 'CN=Microsoft Corporation, O=Microsoft Corporation, L=Redmond, S=Washington, C=US';
10
+ /** 1638: a newer version is already installed; 3010: installed, a restart would finish it (not needed for the DLLs). */
11
+ const OK_EXIT_CODES = [0, 1638, 3010];
12
+ /** The subject of a valid Authenticode signature, or '' when the file is unsigned or tampered with. */
13
+ const signerOf = (file) => execOutput(SYSTEM.powershell(), ['-NoLogo', '-NoProfile', '-Command',
14
+ "$s = Get-AuthenticodeSignature -LiteralPath '" + file.replace(/'/g, "''") + "'; if ($s.Status -eq 'Valid') { $s.SignerCertificate.Subject }"]).trim();
15
+ /**
16
+ * A real installer (it writes to System32), so it runs on every boot; the installer itself is kept in persistent
17
+ * storage and only run when Microsoft's signature on it checks out. Resolves true when this call ran the installer.
18
+ * Only the "Visual C++ runtime" step reports it (`summary`); PHP calls it again just to be sure.
19
+ */
20
+ export const setupVcRuntime = async (env, summary = true) => {
21
+ if (dryRun) {
22
+ log('(dry run) would install the Visual C++ runtime');
23
+ return false;
24
+ }
25
+ const system32 = path.join(process.env.WINDIR ?? 'C:\\Windows', 'System32');
26
+ const missing = !exists(path.join(system32, 'vcruntime140.dll')) || !exists(path.join(system32, 'msvcp140.dll'));
27
+ if (missing) {
28
+ const installer = tool('vcredist', 'vc_redist.x64.exe');
29
+ if (!exists(installer)) {
30
+ log('Downloading the Visual C++ runtime (~25 MB once) ...');
31
+ await downloadFile(VC_REDIST, installer);
32
+ }
33
+ if (signerOf(installer) !== MICROSOFT_SIGNER) {
34
+ fs.rmSync(installer, { force: true });
35
+ throw new Error('vc_redist.x64.exe is not signed by Microsoft; deleted it (it is downloaded again next boot)');
36
+ }
37
+ log('Installing the Visual C++ runtime ...');
38
+ const code = await spawnAsync(installer, ['/install', '/quiet', '/norestart']);
39
+ if (!OK_EXIT_CODES.includes(code ?? -1) || !exists(path.join(system32, 'vcruntime140.dll')))
40
+ throw new Error('the Visual C++ runtime installer failed (exit ' + code + ')');
41
+ }
42
+ if (summary)
43
+ env.summary.push('vc++ runtime: installed');
44
+ return missing;
45
+ };
@@ -3,8 +3,28 @@
3
3
  import { connect, runTerminalClient } from '../shared/pty-client.js';
4
4
  import { readSession } from '../shared/rundir.js';
5
5
  import { dataDir } from './env.js';
6
+ /** A session that is still being set up (its project provisioning, its agent installing): show the progress until it runs. */
7
+ const waitUntilStarted = async (id) => {
8
+ let last = '';
9
+ for (let i = 0; i < 3600; i++) {
10
+ const s = readSession(dataDir, id);
11
+ if (s && s.phase !== 'starting')
12
+ return s;
13
+ if (s && s.message !== last) {
14
+ console.log(' ' + s.message);
15
+ last = s.message;
16
+ }
17
+ await new Promise((r) => setTimeout(r, 500));
18
+ }
19
+ return readSession(dataDir, id);
20
+ };
6
21
  export const viewSession = async (id) => {
7
- const session = readSession(dataDir, id);
22
+ const session = await waitUntilStarted(id);
23
+ if (session?.phase === 'failed') {
24
+ console.error('Session ' + id + ' could not start: ' + session.message);
25
+ await new Promise((r) => setTimeout(r, 30000));
26
+ return 1;
27
+ }
8
28
  if (!session?.attach) {
9
29
  console.error('Session ' + id + ' is not running.');
10
30
  return 1;
@@ -4,8 +4,14 @@
4
4
  // restarted in place with --continue so the conversation carries on.
5
5
  import fs from 'node:fs';
6
6
  import path from 'node:path';
7
+ import { z } from 'zod';
8
+ import { isSafeFileName } from '../shared/paths.js';
9
+ import { assertTestWritable } from './test-guard.js';
10
+ import { readValid, writeJsonAtomic } from '../shared/rundir.js';
7
11
  import { assertNoLinks, fail, fileExists, isLink, removeLink, tryReadJson, writeJson } from '../core/util.js';
8
12
  const NAME = /^[A-Za-z0-9_-]{1,40}$/;
13
+ const AccountSchema = z.object({ name: z.string().regex(NAME), savedAt: z.string().max(64), email: z.string().max(200).nullable(), credentials: z.looseObject({}), oauthAccount: z.unknown() });
14
+ const IndexSchema = z.object({ default: z.string().regex(NAME).optional(), active: z.record(z.string().max(200), z.string().regex(NAME)).optional() });
9
15
  const ACCOUNT_CACHES = ['remote-settings.json', 'policy-limits.json', 'policy-limits.json.stamp.json'];
10
16
  const MAX_LOGIN_BYTES = 1024 * 1024;
11
17
  /** .claude.json holds per-project state and can be large; it is still capped so a hostile file cannot exhaust memory. */
@@ -16,15 +22,17 @@ const storageOf = (dataPath) => path.dirname(path.dirname(dataPath));
16
22
  const dataName = (dataPath) => path.basename(dataPath);
17
23
  const accountFile = (storageRoot, name) => path.join(accountsDir(storageRoot), name + '.json');
18
24
  const indexFile = (storageRoot) => path.join(accountsDir(storageRoot), 'index.json');
19
- const readIndex = (storageRoot) => tryReadJson(indexFile(storageRoot)) ?? {};
20
- const updateIndex = (storageRoot, change) => writeJson(indexFile(storageRoot), change(readIndex(storageRoot)));
21
- const assertName = (name) => { if (!NAME.test(name))
25
+ const readIndex = (storageRoot) => readValid(IndexSchema, indexFile(storageRoot)) ?? {};
26
+ const updateIndex = (storageRoot, change) => writeJsonAtomic(indexFile(storageRoot), change(readIndex(storageRoot)));
27
+ /** A file name of its own: not "index" (the index file), not a Windows device name such as con or nul. */
28
+ const validName = (name) => NAME.test(name) && name.toLowerCase() !== 'index' && isSafeFileName(name + '.json');
29
+ const assertName = (name) => { if (!validName(name))
22
30
  fail('Invalid account name "' + name + '".', 'Use letters, numbers, - and _ only (max 40), e.g. work or personal.'); };
23
31
  const emailOf = (oauthAccount) => {
24
32
  const email = oauthAccount?.emailAddress;
25
33
  return typeof email === 'string' ? email.slice(0, 200) : null;
26
34
  };
27
- export const readAccount = (storageRoot, name) => (NAME.test(name) ? tryReadJson(accountFile(storageRoot, name)) : null);
35
+ export const readAccount = (storageRoot, name) => (validName(name) ? readValid(AccountSchema, accountFile(storageRoot, name)) : null);
28
36
  export const defaultAccount = (storageRoot) => readIndex(storageRoot).default ?? null;
29
37
  export const activeAccount = (dataPath) => readIndex(storageOf(dataPath)).active?.[dataName(dataPath)] ?? null;
30
38
  const setActive = (dataPath, name) => updateIndex(storageOf(dataPath), (i) => ({ ...i, active: { ...i.active, [dataName(dataPath)]: name } }));
@@ -51,7 +59,7 @@ const readLogin = (claudeDir, globalStateFile) => {
51
59
  };
52
60
  const store = (storageRoot, name, login) => {
53
61
  const account = { name, savedAt: new Date().toISOString(), email: emailOf(login.oauthAccount), ...login };
54
- writeJson(accountFile(storageRoot, name), account);
62
+ writeJsonAtomic(accountFile(storageRoot, name), account);
55
63
  return account;
56
64
  };
57
65
  const sandboxLogin = (dataPath) => {
@@ -71,6 +79,7 @@ export const saveAccount = (storageRoot, dataPath, name, fromHost) => {
71
79
  };
72
80
  /** Replaces a file that may be a sandbox-planted link instead of writing through it. */
73
81
  const writeSandboxJson = (file, value) => {
82
+ assertTestWritable(file);
74
83
  if (isLink(file))
75
84
  removeLink(file);
76
85
  writeJson(file, value);
@@ -0,0 +1,25 @@
1
+ import type { Config } from '../core/types.js';
2
+ export declare const commandsDir: (storageRoot: string) => string;
3
+ export interface AgentCommand {
4
+ harness: string;
5
+ command: string;
6
+ real: string;
7
+ }
8
+ /** The agent's own executable on this PC, skipping this tool's forwarding files. */
9
+ export declare const realAgent: (command: string, skipDir: string, pathValue?: string) => string | null;
10
+ /** The agents that get a forwarding command: installed on this PC, and listed (or "auto" = all of them). */
11
+ export declare const agentCommands: (config: Config, storageRoot: string, pathValue?: string) => AgentCommand[];
12
+ /** The .cmd file for one agent (cmd, PowerShell and Windows Terminal all run it). */
13
+ export declare const forwardingCmd: (a: AgentCommand, node: string, entry: string) => string;
14
+ /** The same for Git Bash, which looks for a file without an extension. */
15
+ export declare const forwardingSh: (a: AgentCommand, node: string, entry: string) => string;
16
+ export interface Reconciled {
17
+ on: boolean;
18
+ commands: AgentCommand[];
19
+ changed: boolean;
20
+ }
21
+ /**
22
+ * Makes this PC match agentCommands: on = a forwarding file per installed agent and the folder first on your PATH;
23
+ * off = no files, no PATH entry. Only touches files it wrote and its own PATH entry; does nothing when already right.
24
+ */
25
+ export declare const reconcileAgentCommands: (config: Config, storageRoot: string, node: string, entry: string) => Reconciled;