windows-development-sandbox 1.20.0 → 1.23.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 (331) hide show
  1. package/config.example.json +8 -1
  2. package/dist/app/features/access/access.def.js +5 -2
  3. package/dist/app/features/access/access.run.js +7 -3
  4. package/dist/app/features/accounts/accounts.def.d.ts +29 -1
  5. package/dist/app/features/accounts/accounts.def.js +10 -4
  6. package/dist/app/features/accounts/accounts.run.d.ts +1 -0
  7. package/dist/app/features/accounts/accounts.run.js +7 -2
  8. package/dist/app/features/accounts/services.def.d.ts +4 -1
  9. package/dist/app/features/accounts/services.def.js +15 -14
  10. package/dist/app/features/accounts/services.run.d.ts +1 -0
  11. package/dist/app/features/accounts/services.run.js +41 -15
  12. package/dist/app/features/defs.d.ts +39 -2
  13. package/dist/app/features/engine/engine.run.js +1 -1
  14. package/dist/app/features/github/github.def.d.ts +44 -3
  15. package/dist/app/features/github/github.def.js +145 -63
  16. package/dist/app/features/github/github.run.d.ts +9 -2
  17. package/dist/app/features/github/github.run.js +43 -5
  18. package/dist/app/features/health/health.def.js +8 -2
  19. package/dist/app/features/logs/logs.def.js +0 -8
  20. package/dist/app/features/projects/projects.def.js +2 -2
  21. package/dist/app/features/projects/projects.run.js +5 -5
  22. package/dist/app/features/sandbox/sandbox.def.js +7 -3
  23. package/dist/app/features/sessions/sessions.def.d.ts +4 -4
  24. package/dist/app/features/sessions/sessions.def.js +0 -5
  25. package/dist/app/features/sessions/sessions.run.d.ts +1 -1
  26. package/dist/app/features/sessions/sessions.run.js +2 -2
  27. package/dist/app/features/settings/categories.def.js +2 -0
  28. package/dist/app/features/settings/settings-rows.js +1 -1
  29. package/dist/app/features/settings/settings.def.js +1 -1
  30. package/dist/app/features/settings/settings.run.js +3 -3
  31. package/dist/app/features/storage/storage.run.js +18 -9
  32. package/dist/app/features/updates/update-lines.js +2 -2
  33. package/dist/app/features/updates/updates.def.js +1 -1
  34. package/dist/app/features/updates/updates.run.js +1 -1
  35. package/dist/app/features/web/web.def.js +2 -2
  36. package/dist/app/features/web/web.run.js +12 -4
  37. package/dist/app/kernel/errors.d.ts +7 -0
  38. package/dist/app/kernel/errors.js +18 -3
  39. package/dist/app/kernel/rpc.d.ts +5 -0
  40. package/dist/app/kernel/view.d.ts +11 -1
  41. package/dist/bin/agent-sandbox.js +2 -2
  42. package/dist/bin/{chunk-DK4NTBUF.js → chunk-JETGP2C3.js} +172 -48
  43. package/dist/bin/{chunk-2IMT7ZCT.js → chunk-XZKOPGNH.js} +1 -1
  44. package/dist/bin/{chunk-Z2P5OGKL.js → chunk-ZTTOF5V3.js} +2430 -1688
  45. package/dist/bin/claude-host.js +2 -2
  46. package/dist/bin/claude-sbx.js +2 -2
  47. package/dist/bin/codex-sbx.js +2 -2
  48. package/dist/bin/gemini-sbx.js +2 -2
  49. package/dist/bin/sandbox.js +2 -2
  50. package/dist/bin/sbx-hide.js +2 -2
  51. package/dist/bin/sbx-show.js +2 -2
  52. package/dist/cli/manifest.d.ts +2 -2
  53. package/dist/cli/manifest.js +2 -1
  54. package/dist/cli/menu/coverage-exempt.d.ts +17 -0
  55. package/dist/cli/menu/coverage-exempt.js +61 -0
  56. package/dist/cli/menu/io-menu.d.ts +2 -0
  57. package/dist/cli/menu/io-menu.js +50 -5
  58. package/dist/cli/menu/pages/accounts-page.js +1 -1
  59. package/dist/cli/menu/pages/agents-page.js +8 -2
  60. package/dist/cli/menu/pages/maintenance-page.d.ts +6 -0
  61. package/dist/cli/menu/pages/maintenance-page.js +9 -2
  62. package/dist/cli/menu/pages/sessions-page.d.ts +2 -0
  63. package/dist/cli/menu/pages/sessions-page.js +12 -4
  64. package/dist/cli/menu/pages/settings-page.js +12 -1
  65. package/dist/cli/menu/remote-menu.js +1 -1
  66. package/dist/cli/menu/shell.d.ts +5 -0
  67. package/dist/cli/menu/shell.js +13 -0
  68. package/dist/cli/report.js +2 -0
  69. package/dist/cli/tui/app.d.ts +5 -0
  70. package/dist/cli/tui/app.js +2 -0
  71. package/dist/core/config/defaults.js +3 -1
  72. package/dist/core/schema/accounts.d.ts +6 -0
  73. package/dist/core/schema/accounts.js +13 -11
  74. package/dist/core/schema/config.d.ts +20 -8
  75. package/dist/core/schema/config.js +4 -4
  76. package/dist/core/schema/github.js +1 -1
  77. package/dist/core/schema/profile.js +8 -4
  78. package/dist/core/schema/resume.js +1 -1
  79. package/dist/core/schema/sandbox.js +3 -3
  80. package/dist/core/schema/vpn.js +1 -1
  81. package/dist/core/schema/web.js +3 -3
  82. package/dist/core/types.d.ts +6 -0
  83. package/dist/core/util/errors.d.ts +11 -1
  84. package/dist/core/util/errors.js +7 -1
  85. package/dist/core/util/index.d.ts +1 -1
  86. package/dist/core/util/index.js +1 -1
  87. package/dist/guest/accounts/adapter.d.ts +2 -0
  88. package/dist/guest/accounts/adapters.js +4 -0
  89. package/dist/guest/accounts/apply.d.ts +6 -2
  90. package/dist/guest/accounts/kinds/cfx.d.ts +7 -0
  91. package/dist/guest/accounts/kinds/cfx.js +33 -3
  92. package/dist/guest/accounts/kinds/comet.d.ts +7 -0
  93. package/dist/guest/accounts/kinds/comet.js +50 -0
  94. package/dist/guest/accounts/kinds/perplexity.d.ts +2 -0
  95. package/dist/guest/accounts/kinds/perplexity.js +12 -0
  96. package/dist/guest/accounts/watch.d.ts +2 -0
  97. package/dist/guest/accounts/watch.js +12 -2
  98. package/dist/guest/agents/agent-installer.js +1 -1
  99. package/dist/guest/agents/profile-links.d.ts +2 -0
  100. package/dist/guest/agents/profile-links.js +18 -4
  101. package/dist/guest/apps/after-install.js +19 -0
  102. package/dist/guest/apps/app-installer.d.ts +11 -0
  103. package/dist/guest/apps/app-installer.js +69 -6
  104. package/dist/guest/apps/comet-unpack.d.ts +20 -0
  105. package/dist/guest/apps/comet-unpack.js +127 -0
  106. package/dist/guest/apps/index.d.ts +1 -1
  107. package/dist/guest/apps/index.js +1 -1
  108. package/dist/guest/apps/resolvers-agents-web.d.ts +2 -0
  109. package/dist/guest/apps/resolvers-agents-web.js +7 -0
  110. package/dist/guest/apps/resolvers.js +2 -0
  111. package/dist/guest/apps/shortcuts.d.ts +2 -0
  112. package/dist/guest/apps/shortcuts.js +1 -0
  113. package/dist/guest/apps/unpack.js +14 -4
  114. package/dist/guest/boot/boot.d.ts +45 -16
  115. package/dist/guest/boot/boot.js +316 -145
  116. package/dist/guest/boot/graph.d.ts +252 -0
  117. package/dist/guest/boot/graph.js +257 -0
  118. package/dist/guest/boot/guide-lines.d.ts +25 -0
  119. package/dist/guest/boot/guide-lines.js +41 -0
  120. package/dist/guest/boot/guide.d.ts +1 -4
  121. package/dist/guest/boot/guide.js +6 -8
  122. package/dist/guest/boot/runner.d.ts +77 -0
  123. package/dist/guest/boot/runner.js +215 -0
  124. package/dist/guest/browser/cdp.d.ts +71 -0
  125. package/dist/guest/browser/cdp.js +306 -0
  126. package/dist/guest/browser/comet.d.ts +28 -0
  127. package/dist/guest/browser/comet.js +155 -0
  128. package/dist/guest/daemon/project-provisioner.js +6 -1
  129. package/dist/guest/devtools/fivem.d.ts +1 -1
  130. package/dist/guest/devtools/fivem.js +243 -3
  131. package/dist/guest/entries/daemon.js +2 -0
  132. package/dist/guest/runtime/log.d.ts +12 -0
  133. package/dist/guest/runtime/log.js +29 -5
  134. package/dist/guest/runtime/pools.d.ts +24 -0
  135. package/dist/guest/runtime/pools.js +60 -0
  136. package/dist/guest/runtime/process.d.ts +4 -0
  137. package/dist/guest/runtime/process.js +38 -1
  138. package/dist/guest/runtime/status.d.ts +5 -1
  139. package/dist/guest/runtime/status.js +19 -0
  140. package/dist/guest/setup/addon-mcp.d.ts +1 -1
  141. package/dist/guest/setup/addon-mcp.js +52 -12
  142. package/dist/guest/setup/agent-addons.d.ts +3 -2
  143. package/dist/guest/setup/agent-addons.js +12 -3
  144. package/dist/guest/setup/environment.d.ts +1 -1
  145. package/dist/guest/setup/environment.js +25 -12
  146. package/dist/guest/setup/headroom.d.ts +8 -3
  147. package/dist/guest/setup/headroom.js +44 -15
  148. package/dist/guest/setup/path-plan.d.ts +28 -0
  149. package/dist/guest/setup/path-plan.js +69 -2
  150. package/dist/guest/setup/shell-path.d.ts +1 -1
  151. package/dist/guest/setup/shell-path.js +3 -3
  152. package/dist/guest/toolchains/fivem/artifacts.d.ts +48 -1
  153. package/dist/guest/toolchains/fivem/artifacts.js +97 -4
  154. package/dist/guest/toolchains/fivem/fivem.d.ts +32 -1
  155. package/dist/guest/toolchains/fivem/fivem.js +168 -3
  156. package/dist/guest/toolchains/fivem/scaffold.d.ts +35 -0
  157. package/dist/guest/toolchains/fivem/scaffold.js +105 -0
  158. package/dist/guest/toolchains/fivem/serve-plan.d.ts +34 -0
  159. package/dist/guest/toolchains/fivem/serve-plan.js +61 -0
  160. package/dist/guest/toolchains/languages/go.js +1 -1
  161. package/dist/guest/toolchains/languages/node.js +2 -1
  162. package/dist/guest/toolchains/languages/php.d.ts +2 -0
  163. package/dist/guest/toolchains/languages/php.js +13 -1
  164. package/dist/guest/toolchains/languages/python-tools.js +1 -1
  165. package/dist/guest/toolchains/languages/ruby.js +1 -1
  166. package/dist/guest/toolchains/native/binstall.js +1 -1
  167. package/dist/guest/toolchains/native/msvc/packages.js +10 -4
  168. package/dist/guest/toolchains/native/rust.d.ts +2 -0
  169. package/dist/guest/toolchains/native/rust.js +11 -4
  170. package/dist/guest/toolchains/native/unreal.d.ts +2 -0
  171. package/dist/guest/toolchains/native/unreal.js +16 -6
  172. package/dist/guest/toolchains/native/vcruntime.d.ts +2 -0
  173. package/dist/guest/toolchains/native/vcruntime.js +10 -2
  174. package/dist/guest/toolchains/native/visual-studio.d.ts +3 -0
  175. package/dist/guest/toolchains/native/visual-studio.js +44 -26
  176. package/dist/guest/toolchains/native/vs-request.d.ts +2 -0
  177. package/dist/guest/toolchains/native/vs-request.js +2 -0
  178. package/dist/guest/toolchains/plan.js +19 -8
  179. package/dist/guest/toolchains/scheduler.d.ts +2 -21
  180. package/dist/guest/toolchains/scheduler.js +7 -121
  181. package/dist/guest/toolchains/setup.d.ts +15 -2
  182. package/dist/guest/toolchains/setup.js +39 -20
  183. package/dist/guest/toolchains/split.d.ts +28 -0
  184. package/dist/guest/toolchains/split.js +60 -0
  185. package/dist/guest/toolchains/steam/client.js +2 -1
  186. package/dist/guest/toolchains/step-scheduler.d.ts +51 -0
  187. package/dist/guest/toolchains/step-scheduler.js +147 -0
  188. package/dist/guest/toolchains/step.d.ts +13 -0
  189. package/dist/guest/toolchains/tools/git.js +18 -0
  190. package/dist/guest/vpn/openvpn.js +3 -4
  191. package/dist/guest/vpn/wireguard.js +11 -4
  192. package/dist/host/accounts/deliver.js +7 -0
  193. package/dist/host/accounts/handoff.js +2 -6
  194. package/dist/host/accounts/index.d.ts +1 -1
  195. package/dist/host/accounts/index.js +1 -1
  196. package/dist/host/accounts/kinds/cfx.d.ts +1 -1
  197. package/dist/host/accounts/kinds/cfx.js +47 -4
  198. package/dist/host/accounts/kinds/comet.d.ts +2 -0
  199. package/dist/host/accounts/kinds/comet.js +6 -0
  200. package/dist/host/accounts/kinds/perplexity.d.ts +2 -0
  201. package/dist/host/accounts/kinds/perplexity.js +30 -0
  202. package/dist/host/accounts/leases.d.ts +7 -1
  203. package/dist/host/accounts/leases.js +13 -2
  204. package/dist/host/accounts/source.d.ts +7 -1
  205. package/dist/host/accounts/source.js +27 -0
  206. package/dist/host/accounts/sources.js +4 -0
  207. package/dist/host/github/deliver.d.ts +1 -1
  208. package/dist/host/github/deliver.js +20 -1
  209. package/dist/host/github/index.d.ts +2 -1
  210. package/dist/host/github/index.js +1 -0
  211. package/dist/host/github/token.d.ts +13 -1
  212. package/dist/host/github/token.js +19 -2
  213. package/dist/host/github/verify.d.ts +27 -0
  214. package/dist/host/github/verify.js +61 -0
  215. package/dist/host/launch/hints/fivem.d.ts +3 -1
  216. package/dist/host/launch/hints/fivem.js +68 -2
  217. package/dist/host/launch/session-file.js +4 -0
  218. package/dist/host/launch/toolchains.d.ts +2 -1
  219. package/dist/host/launch/toolchains.js +5 -3
  220. package/dist/host/vpn/nordvpn.d.ts +2 -4
  221. package/dist/host/vpn/nordvpn.js +2 -24
  222. package/dist/runtime/agent.js +405 -90
  223. package/dist/runtime/daemon.js +3010 -1383
  224. package/dist/runtime/db.js +333 -90
  225. package/dist/runtime/main.js +4720 -1954
  226. package/dist/runtime/netproxy.js +286 -69
  227. package/dist/runtime/tool.js +2852 -1780
  228. package/dist/runtime/view.js +286 -69
  229. package/dist/selftest/agent-paths.test.js +39 -1
  230. package/dist/selftest/agents-web.test.d.ts +2 -0
  231. package/dist/selftest/agents-web.test.js +647 -0
  232. package/dist/selftest/app.test.js +5 -2
  233. package/dist/selftest/boot-runner.test.d.ts +2 -0
  234. package/dist/selftest/boot-runner.test.js +456 -0
  235. package/dist/selftest/channel.test.js +15 -0
  236. package/dist/selftest/fivem.test.js +249 -3
  237. package/dist/selftest/github.test.js +94 -9
  238. package/dist/selftest/guest-boot.test.js +3 -0
  239. package/dist/selftest/health.test.js +1 -1
  240. package/dist/selftest/index.js +8 -0
  241. package/dist/selftest/layout.test.js +5 -0
  242. package/dist/selftest/menu-coverage.test.d.ts +2 -0
  243. package/dist/selftest/menu-coverage.test.js +46 -0
  244. package/dist/selftest/menu-screens.js +37 -1
  245. package/dist/selftest/persisted-installs.test.js +6 -3
  246. package/dist/selftest/service-accounts.test.js +3 -2
  247. package/dist/selftest/support/engine-golden.js +27 -0
  248. package/dist/selftest/support/fixtures.d.ts +6 -0
  249. package/dist/selftest/support/web-preview.d.ts +34 -1
  250. package/dist/selftest/support/web-preview.js +21 -18
  251. package/dist/selftest/toolchains.test.js +23 -1
  252. package/dist/selftest/ui-text.test.d.ts +7 -0
  253. package/dist/selftest/ui-text.test.js +300 -0
  254. package/dist/shared/accounts/index.d.ts +2 -0
  255. package/dist/shared/accounts/index.js +2 -0
  256. package/dist/shared/accounts/kinds/comet.d.ts +12 -0
  257. package/dist/shared/accounts/kinds/comet.js +64 -0
  258. package/dist/shared/accounts/kinds/perplexity.d.ts +8 -0
  259. package/dist/shared/accounts/kinds/perplexity.js +37 -0
  260. package/dist/shared/accounts/kinds.d.ts +24 -5
  261. package/dist/shared/accounts/kinds.js +86 -4
  262. package/dist/shared/accounts/payloads.d.ts +81 -0
  263. package/dist/shared/accounts/payloads.js +39 -1
  264. package/dist/shared/apps/agent-addons.d.ts +33 -5
  265. package/dist/shared/apps/agent-addons.js +95 -3
  266. package/dist/shared/apps/catalogue-agents-web.d.ts +14 -0
  267. package/dist/shared/apps/catalogue-agents-web.js +112 -0
  268. package/dist/shared/apps/catalogue-modding.js +32 -1
  269. package/dist/shared/apps/catalogue.d.ts +27 -3
  270. package/dist/shared/apps/catalogue.js +2 -1
  271. package/dist/shared/apps/index.d.ts +3 -1
  272. package/dist/shared/apps/index.js +3 -1
  273. package/dist/shared/channel/schema.d.ts +21 -3
  274. package/dist/shared/channel/schema.js +7 -0
  275. package/dist/shared/channel/secrets.d.ts +19 -0
  276. package/dist/shared/concurrency/index.d.ts +3 -0
  277. package/dist/shared/concurrency/index.js +3 -0
  278. package/dist/shared/concurrency/installer-retry.d.ts +6 -0
  279. package/dist/shared/concurrency/installer-retry.js +10 -0
  280. package/dist/shared/concurrency/limiter.d.ts +19 -0
  281. package/dist/shared/concurrency/limiter.js +67 -0
  282. package/dist/shared/concurrency/pools.d.ts +7 -0
  283. package/dist/shared/concurrency/pools.js +9 -0
  284. package/dist/shared/download/archive.d.ts +2 -0
  285. package/dist/shared/download/archive.js +11 -1
  286. package/dist/shared/download/bounded-fetch.d.ts +13 -0
  287. package/dist/shared/download/bounded-fetch.js +40 -0
  288. package/dist/shared/download/downloader.d.ts +5 -0
  289. package/dist/shared/download/downloader.js +50 -38
  290. package/dist/shared/download/index.d.ts +2 -1
  291. package/dist/shared/download/index.js +2 -1
  292. package/dist/shared/readiness.d.ts +2 -2
  293. package/dist/shared/readiness.js +2 -2
  294. package/dist/web/index.html +1 -1
  295. package/dist/web/{main-OUUQYNK4.js → main-CCILLX2M.js} +67 -67
  296. package/native/csharp/HcsHost/AssemblyInfo.cs +3 -3
  297. package/native/csharp/HostTools/AssemblyInfo.cs +3 -3
  298. package/native/csharp/StorageHelper/AssemblyInfo.cs +3 -3
  299. package/package.json +1 -1
  300. package/sandbox/addons/browser-use-mcp/requirements.in +1 -0
  301. package/sandbox/addons/browser-use-mcp/requirements.txt +2350 -0
  302. package/sandbox/addons/chrome-devtools-mcp/package-lock.json +38 -0
  303. package/sandbox/addons/chrome-devtools-mcp/package.json +7 -0
  304. package/sandbox/addons/comet-mcp/package-lock.json +1277 -0
  305. package/sandbox/addons/comet-mcp/package.json +7 -0
  306. package/sandbox/addons/perplexity-mcp/package-lock.json +1655 -0
  307. package/sandbox/addons/perplexity-mcp/package.json +7 -0
  308. package/sandbox/scripts/add-firewall-port.ps1 +18 -4
  309. package/sandbox/scripts/set-machine-environment.ps1 +30 -15
  310. package/sandbox/templates/fivem/README.txt +6 -2
  311. package/sandbox/templates/fivem/cs/Client/Client.csproj +15 -0
  312. package/sandbox/templates/fivem/cs/Client/ClientMain.cs +20 -0
  313. package/sandbox/templates/fivem/cs/Server/Server.csproj +14 -0
  314. package/sandbox/templates/fivem/cs/Server/ServerMain.cs +20 -0
  315. package/sandbox/templates/fivem/cs/fxmanifest.lua +11 -0
  316. package/sandbox/templates/fivem/js/client.js +12 -0
  317. package/sandbox/templates/fivem/js/fxmanifest.lua +9 -0
  318. package/sandbox/templates/fivem/js/server.js +6 -0
  319. package/sandbox/templates/fivem/lua/client.lua +8 -0
  320. package/sandbox/templates/fivem/lua/fxmanifest.lua +10 -0
  321. package/sandbox/templates/fivem/lua/server.lua +6 -0
  322. package/sandbox/templates/fivem/server.cfg +37 -0
  323. package/sandbox/templates/fivem/ts/build.js +17 -0
  324. package/sandbox/templates/fivem/ts/fxmanifest.lua +9 -0
  325. package/sandbox/templates/fivem/ts/package.json +16 -0
  326. package/sandbox/templates/fivem/ts/src/client/client.ts +12 -0
  327. package/sandbox/templates/fivem/ts/src/client/tsconfig.json +13 -0
  328. package/sandbox/templates/fivem/ts/src/server/server.ts +6 -0
  329. package/sandbox/templates/fivem/ts/src/server/tsconfig.json +13 -0
  330. package/schemas/config.schema.json +56 -27
  331. package/schemas/sandbox-config.schema.json +37 -14
@@ -1,25 +1,44 @@
1
- import { log } from '../runtime/log.js';
1
+ import fs from 'node:fs';
2
+ import { log, runQuiet } from '../runtime/log.js';
3
+ import { toolsDir } from '../runtime/paths.js';
4
+ import { installedToolPaths } from '../setup/path-plan.js';
2
5
  import { mkdirp } from '../runtime/fs.js';
3
- import { emptyEnv, tool } from './tool-env.js';
6
+ import { tool } from './tool-env.js';
4
7
  import { plan } from './plan.js';
5
8
  import { scheduler } from './scheduler.js';
6
- /** Provisions every enabled toolchain and database, and returns the combined environment for the agent. */
7
- export async function setupToolchains(config, versions, options, run = {}) {
9
+ import { mergeParts, runSplit } from './split.js';
10
+ /** The parts of these steps merged in the order of `names` (and the config's own variables on top). */
11
+ export function mergeToolchains(config, names, parts, planNames = names) {
12
+ const map = parts instanceof Map ? parts : new Map(Object.entries(parts));
13
+ return {
14
+ ...mergeParts(names, map, { environment: config.environment, bin: mkdirp(tool('bin')) }),
15
+ names: [...planNames],
16
+ };
17
+ }
18
+ /**
19
+ * Provisions every enabled toolchain and database, and returns the combined environment for the agent. `later` is
20
+ * set when steps were left for the background: it resolves with the result of those steps alone.
21
+ */
22
+ export async function setupToolchains(config, versions, options, run = {}, extras = {}) {
8
23
  const steps = plan(config, versions, options).filter((s) => s.enabled);
9
- const envs = await scheduler.run(steps, run);
10
- // Merged in plan order, so PATH order never depends on which download finished first.
11
- const env = { ...emptyEnv(), steps: Object.fromEntries(envs) };
12
- env.prepend.push(mkdirp(tool('bin')));
13
- for (const part of envs.values()) {
14
- Object.assign(env.vars, part.vars);
15
- env.prepend.push(...part.prepend);
16
- env.append.push(...part.append);
17
- env.summary.push(...part.summary);
18
- }
19
- Object.assign(env.vars, config.environment);
20
- // The git guard's variables (AGENT_GIT_ACCESS, GIT_DIR, GIT_CONFIG_*, BASH_ENV) always win over
21
- // toolchains.environment.
22
- Object.assign(env.vars, envs.get('Git')?.vars);
23
- log('Set up ' + steps.length + ' toolchains and databases.');
24
- return env;
24
+ const split = await runSplit(scheduler, steps, run, { ...extras, quiet: runQuiet });
25
+ const result = mergeToolchains(config, split.nowNames, split.now, steps.map((s) => s.name));
26
+ // Python and rust already installed in the persistent tools are on PATH for every project (at the end, nothing is
27
+ // installed): the project this session is for may not have asked for them, but its scripts and neighbours use them.
28
+ const have = new Set(steps.map((s) => s.name));
29
+ const installed = installedToolPaths(toolsDir, { exists: fs.existsSync, list: (dir) => (fs.existsSync(dir) ? fs.readdirSync(dir) : []) }, new Set([...(have.has('Python') ? ['python'] : []), ...(have.has('Rust') ? ['rust'] : [])]));
30
+ result.append.push(...installed.append);
31
+ result.summary.push(...installed.summary);
32
+ for (const [name, value] of Object.entries(installed.vars))
33
+ result.vars[name] ??= value;
34
+ log('Set up ' +
35
+ split.nowNames.length +
36
+ ' toolchains and databases' +
37
+ (split.laterNames.length ? ' (' + split.laterNames.length + ' more later)' : '') +
38
+ '.');
39
+ const later = split.later
40
+ ? split.later.then((parts) => mergeToolchains(config, split.laterNames, parts, steps.map((s) => s.name)))
41
+ : null;
42
+ later?.catch(() => undefined);
43
+ return Object.assign(result, { later });
25
44
  }
@@ -0,0 +1,28 @@
1
+ import { type ToolEnv } from './tool-env.js';
2
+ import type { ToolchainStep } from './step.js';
3
+ import type { RunOptions, StepScheduler } from './step-scheduler.js';
4
+ /** The steps that wait for the gate: the ones marked `background`, and every step that comes after one. */
5
+ export declare function backgroundNames(steps: readonly ToolchainStep[]): Set<string>;
6
+ export interface SplitRun {
7
+ /** What the foreground steps contributed, in plan order. */
8
+ readonly now: Map<string, ToolEnv>;
9
+ readonly nowNames: readonly string[];
10
+ readonly laterNames: readonly string[];
11
+ /** The background steps' parts, once the gate opened and they ran; null when nothing was left for later. */
12
+ readonly later: Promise<Map<string, ToolEnv>> | null;
13
+ }
14
+ /**
15
+ * Runs the foreground steps and returns; the background steps (their downloads start now, behind the foreground ones)
16
+ * run after `gate`, inside `quiet` (file-only logging).
17
+ */
18
+ export declare function runSplit(scheduler: StepScheduler, steps: readonly ToolchainStep[], run: RunOptions, o?: {
19
+ gate?: Promise<void>;
20
+ quiet?: <T>(work: () => T) => T;
21
+ }): Promise<SplitRun>;
22
+ /** The parts of these steps merged in the order of `names`, so PATH order never depends on which finished first. */
23
+ export declare function mergeParts(names: readonly string[], parts: ReadonlyMap<string, ToolEnv>, o: {
24
+ environment: Readonly<Record<string, string>>;
25
+ bin: string;
26
+ }): ToolEnv & {
27
+ steps: Record<string, ToolEnv>;
28
+ };
@@ -0,0 +1,60 @@
1
+ // Splits a plan into the steps that run now and the ones that wait for a gate (the background: Visual Studio and what
2
+ // needs it), and merges the steps' parts in plan order. Pure apart from the scheduler it is given.
3
+ import { asBackground } from '../../shared/concurrency/index.js';
4
+ import { emptyEnv } from './tool-env.js';
5
+ /** The steps that wait for the gate: the ones marked `background`, and every step that comes after one. */
6
+ export function backgroundNames(steps) {
7
+ const later = new Set(steps.filter((s) => s.background).map((s) => s.name));
8
+ for (let grew = later.size > 0; grew;) {
9
+ grew = false;
10
+ for (const s of steps)
11
+ if (!later.has(s.name) && (s.after ?? []).some((n) => later.has(n))) {
12
+ later.add(s.name);
13
+ grew = true;
14
+ }
15
+ }
16
+ return later;
17
+ }
18
+ /**
19
+ * Runs the foreground steps and returns; the background steps (their downloads start now, behind the foreground ones)
20
+ * run after `gate`, inside `quiet` (file-only logging).
21
+ */
22
+ export async function runSplit(scheduler, steps, run, o = {}) {
23
+ const waiting = backgroundNames(steps);
24
+ const now = steps.filter((s) => !waiting.has(s.name));
25
+ const rest = steps.filter((s) => waiting.has(s.name));
26
+ asBackground(() => scheduler.startFetches(rest));
27
+ const parts = await scheduler.run(now, run);
28
+ const quiet = o.quiet ?? ((work) => work());
29
+ const later = rest.length
30
+ ? (async () => {
31
+ await o.gate;
32
+ // A progress callback that does nothing: the scheduler must not rewrite the boot's status as "ready".
33
+ return quiet(() => asBackground(() => scheduler.run(rest, { progress: () => undefined })));
34
+ })()
35
+ : null;
36
+ // Nobody may see a failed background run as an unhandled rejection before the boot awaits it.
37
+ later?.catch(() => undefined);
38
+ return { now: parts, nowNames: now.map((s) => s.name), laterNames: rest.map((s) => s.name), later };
39
+ }
40
+ /** The parts of these steps merged in the order of `names`, so PATH order never depends on which finished first. */
41
+ export function mergeParts(names, parts, o) {
42
+ const present = names.filter((n) => parts.has(n));
43
+ const env = {
44
+ ...emptyEnv(),
45
+ steps: Object.fromEntries(present.map((n) => [n, parts.get(n)])),
46
+ };
47
+ env.prepend.push(o.bin);
48
+ for (const name of present) {
49
+ const p = parts.get(name);
50
+ Object.assign(env.vars, p.vars);
51
+ env.prepend.push(...p.prepend);
52
+ env.append.push(...p.append);
53
+ env.summary.push(...p.summary);
54
+ }
55
+ Object.assign(env.vars, o.environment);
56
+ // The git guard's variables (AGENT_GIT_ACCESS, GIT_DIR, GIT_CONFIG_*, BASH_ENV) always win over
57
+ // toolchains.environment.
58
+ Object.assign(env.vars, parts.get('Git')?.vars);
59
+ return env;
60
+ }
@@ -4,6 +4,7 @@
4
4
  import fs from 'node:fs';
5
5
  import path from 'node:path';
6
6
  import { errMsg } from '../../../core/util/errors.js';
7
+ import { heavy } from '../../../shared/concurrency/index.js';
7
8
  import { moveIntoPlace, spawnAsync, stagingFolder } from '../../../shared/download/index.js';
8
9
  import { safeRemove } from '../../../shared/io/safe-remove.js';
9
10
  import { exists } from '../../runtime/fs.js';
@@ -38,7 +39,7 @@ async function installSteamClient() {
38
39
  try {
39
40
  log('Unpacking the Steam client into tools\\steam ...');
40
41
  // NSIS: /S silent, /D= the folder (last, unquoted: the staging path has no spaces).
41
- const code = await spawnAsync(setup, ['/S', '/D=' + work], { timeout: SETUP_TIMEOUT_MS });
42
+ const code = await heavy.run(() => spawnAsync(setup, ['/S', '/D=' + work], { timeout: SETUP_TIMEOUT_MS }));
42
43
  const unpacked = path.join(work, 'steam.exe');
43
44
  if (!exists(unpacked))
44
45
  throw new Error('SteamSetup.exe did not unpack steam.exe (exit code ' + code + ')');
@@ -0,0 +1,51 @@
1
+ import { type ToolEnv } from './tool-env.js';
2
+ import type { ToolchainStep } from './step.js';
3
+ /** Where the scheduler's warnings and the boot status go (injected, so tests need neither the log nor session.json). */
4
+ export interface SchedulerSink {
5
+ warn(message: string): void;
6
+ status(message: string, progress: {
7
+ active: {
8
+ name: string;
9
+ percent?: number;
10
+ }[];
11
+ done: number;
12
+ total: number;
13
+ }): void;
14
+ }
15
+ export interface RunOptions {
16
+ /** Progress lines ("Installing Go, Rust (3 of 9 done)"); default: the sandbox's boot status. */
17
+ report?: (message: string) => void;
18
+ /** What machine-wide (shared) steps contributed at boot: reused instead of run again. */
19
+ reuse?: Record<string, ToolEnv>;
20
+ /** The boot: gets what runs now instead of the scheduler writing the sandbox status itself. */
21
+ progress?: (p: {
22
+ active: {
23
+ name: string;
24
+ percent?: number;
25
+ }[];
26
+ done: number;
27
+ total: number;
28
+ }) => void;
29
+ /** A step finished (installed, failed or reused). */
30
+ onDone?: (name: string) => void;
31
+ }
32
+ export declare class StepScheduler {
33
+ private readonly sink;
34
+ private readonly parallel;
35
+ /** One lock per kind of step ("Node 20" and "Node 22" share nvm's folders): held while that step runs, in any run. */
36
+ private readonly locks;
37
+ /** The step a download belongs to (several run at once), for its progress. */
38
+ private readonly currentStep;
39
+ /** Downloads started ahead of their steps (by name), until the step has used them. */
40
+ private readonly fetches;
41
+ constructor(sink: SchedulerSink, parallel?: number);
42
+ /**
43
+ * Starts the downloads of these steps now (once per step); `run` waits for a step's own download before it installs.
44
+ * Failures are not reported here: the install downloads again and reports the real reason.
45
+ */
46
+ startFetches(steps: readonly ToolchainStep[]): void;
47
+ /** Runs the steps (each with its own environment) and returns each step's environment, in plan order. */
48
+ run(steps: readonly ToolchainStep[], o: RunOptions): Promise<Map<string, ToolEnv>>;
49
+ /** Runs `work` once no other step of the same kind (first word of the name) is running. */
50
+ private exclusive;
51
+ }
@@ -0,0 +1,147 @@
1
+ // Runs the planned steps in parallel, a few at a time, after the steps they depend on. One lock per kind of step is
2
+ // held across runs: the boot and projects that join later never install two Nodes (or two Gos) at once.
3
+ import { AsyncLocalStorage } from 'node:async_hooks';
4
+ import { downloader } from '../../shared/download/index.js';
5
+ import { errMsg } from '../../core/util/errors.js';
6
+ import { emptyEnv } from './tool-env.js';
7
+ /** How many installers run at once: enough to overlap downloads with installs, few enough not to thrash the disk. */
8
+ const PARALLEL = 4;
9
+ /** Runs one installer; a failure is reported and skipped so it never blocks the other toolchains. */
10
+ async function runReported(sink, name, env, work) {
11
+ try {
12
+ await work();
13
+ }
14
+ catch (e) {
15
+ sink.warn(name +
16
+ ' could not be set up and is skipped; everything else still works, and the next start tries again. Reason: ' +
17
+ errMsg(e));
18
+ env.summary.push(name + ': FAILED');
19
+ }
20
+ }
21
+ export class StepScheduler {
22
+ sink;
23
+ parallel;
24
+ /** One lock per kind of step ("Node 20" and "Node 22" share nvm's folders): held while that step runs, in any run. */
25
+ locks = new Map();
26
+ /** The step a download belongs to (several run at once), for its progress. */
27
+ currentStep = new AsyncLocalStorage();
28
+ /** Downloads started ahead of their steps (by name), until the step has used them. */
29
+ fetches = new Map();
30
+ constructor(sink, parallel = PARALLEL) {
31
+ this.sink = sink;
32
+ this.parallel = parallel;
33
+ }
34
+ /**
35
+ * Starts the downloads of these steps now (once per step); `run` waits for a step's own download before it installs.
36
+ * Failures are not reported here: the install downloads again and reports the real reason.
37
+ */
38
+ startFetches(steps) {
39
+ for (const s of steps) {
40
+ if (!s.fetch || this.fetches.has(s.name))
41
+ continue;
42
+ this.fetches.set(s.name, this.currentStep
43
+ .run(s.name, async () => s.fetch())
44
+ .catch(() => {
45
+ /* install downloads again and says why it failed */
46
+ }));
47
+ }
48
+ }
49
+ /** Runs the steps (each with its own environment) and returns each step's environment, in plan order. */
50
+ async run(steps, o) {
51
+ const envs = new Map(steps.map((s) => [s.name, emptyEnv()]));
52
+ const finished = new Map();
53
+ const running = new Set();
54
+ const waiting = [];
55
+ let done = 0;
56
+ // The boot (no `report`) also publishes structured progress: what runs now, how far its download is, how many are
57
+ // done.
58
+ const percent = new Map();
59
+ const active = () => [...running].map((name) => ({ name, ...(percent.has(name) ? { percent: percent.get(name) } : {}) }));
60
+ const say = o.report ??
61
+ ((message) => o.progress
62
+ ? o.progress({ active: active(), done, total: steps.length })
63
+ : this.sink.status(message, { active: active(), done, total: steps.length }));
64
+ const report = () => {
65
+ if (running.size)
66
+ say('Installing ' + [...running].join(', ') + ' (' + done + ' of ' + steps.length + ' done)');
67
+ };
68
+ this.startFetches(steps.filter((s) => !(s.shared && o.reuse?.[s.name])));
69
+ if (!o.report)
70
+ downloader.watch(() => {
71
+ const name = this.currentStep.getStore();
72
+ return name
73
+ ? (fraction) => {
74
+ percent.set(name, Math.round(fraction * 100));
75
+ report();
76
+ }
77
+ : null;
78
+ });
79
+ for (const s of steps) {
80
+ // Dependencies first (they are earlier in the plan, so their promises exist), then a free slot.
81
+ finished.set(s.name, (async () => {
82
+ await Promise.all((s.after ?? []).map((name) => finished.get(name)));
83
+ const env = envs.get(s.name);
84
+ const reused = s.shared ? o.reuse?.[s.name] : undefined;
85
+ if (reused) {
86
+ envs.set(s.name, structuredClone(reused));
87
+ done++;
88
+ o.onDone?.(s.name);
89
+ return;
90
+ }
91
+ // Its download (started above, before the dependencies finished) is in by now, or still coming.
92
+ await this.fetches.get(s.name);
93
+ this.fetches.delete(s.name);
94
+ while (running.size >= this.parallel)
95
+ await new Promise((r) => waiting.push(r));
96
+ running.add(s.name);
97
+ report();
98
+ try {
99
+ await this.currentStep.run(s.name, () => this.exclusive(s.name, () => runReported(this.sink, s.name, env, async () => s.install(env))));
100
+ }
101
+ finally {
102
+ running.delete(s.name);
103
+ percent.delete(s.name);
104
+ done++;
105
+ report();
106
+ waiting.shift()?.();
107
+ o.onDone?.(s.name);
108
+ }
109
+ })());
110
+ }
111
+ try {
112
+ await Promise.all(finished.values());
113
+ }
114
+ finally {
115
+ if (!o.report)
116
+ downloader.watch(null);
117
+ }
118
+ o.progress?.({ active: [], done, total: steps.length });
119
+ if (!o.report && !o.progress)
120
+ this.sink.status('Set up ' + steps.length + ' toolchains and databases', {
121
+ active: [],
122
+ done,
123
+ total: steps.length,
124
+ });
125
+ return envs;
126
+ }
127
+ /** Runs `work` once no other step of the same kind (first word of the name) is running. */
128
+ async exclusive(name, work) {
129
+ const key = name.split(' ')[0].toLowerCase();
130
+ const previous = this.locks.get(key) ?? Promise.resolve();
131
+ let release = () => undefined;
132
+ const mine = new Promise((r) => {
133
+ release = r;
134
+ });
135
+ const chained = previous.then(() => mine);
136
+ this.locks.set(key, chained);
137
+ await previous;
138
+ try {
139
+ return await work();
140
+ }
141
+ finally {
142
+ release();
143
+ if (this.locks.get(key) === chained)
144
+ this.locks.delete(key);
145
+ }
146
+ }
147
+ }
@@ -11,6 +11,12 @@ export interface ToolchainOptions {
11
11
  apps?: Config['apps'];
12
12
  /** pcInstalls: this PC's games, engines and editors mapped read-only (session.json). */
13
13
  pcInstalls?: PcInstallManifest;
14
+ /**
15
+ * Boot only: the heavy installs (Visual Studio, and what depends on it) run in the background after the agent has
16
+ * started. Off with a VPN or network.allowOnly (their firewall would cut the downloads), in a dry run and when a
17
+ * project joins a running sandbox.
18
+ */
19
+ deferHeavy?: boolean;
14
20
  }
15
21
  /** What a toolchain module's step gets besides its settings. */
16
22
  export interface ToolCtx {
@@ -30,5 +36,12 @@ export interface ToolchainStep {
30
36
  * git guard): a project added after boot reuses what the boot's run contributed instead of running it again.
31
37
  */
32
38
  readonly shared?: boolean;
39
+ /**
40
+ * Download only what `install` will need (no-op when it is there). The scheduler starts every step's fetch at once,
41
+ * outside the step slots and `after`; only the net pool limits them. A failure is ignored: install downloads again.
42
+ */
43
+ fetch?(): Promise<void>;
44
+ /** At boot, runs after the agent has started (the steps that depend on it follow it). */
45
+ readonly background?: boolean;
33
46
  install(env: ToolEnv): Promise<void> | void;
34
47
  }
@@ -94,6 +94,22 @@ const pointAtOwnCertificates = (root) => {
94
94
  if (fixed !== text)
95
95
  fs.writeFileSync(config, fixed);
96
96
  };
97
+ /**
98
+ * Coreutils' link.exe (hard links) next to MSVC's linker: Git Bash puts its own /usr/bin first on PATH, whatever the
99
+ * Windows PATH says, so a msvc Rust build ran the wrong `link` (os error 1392). Renamed; nobody needs it here.
100
+ */
101
+ const hideCoreutilsLink = (usrBin) => {
102
+ const link = path.join(usrBin, 'link.exe');
103
+ if (!exists(link))
104
+ return;
105
+ try {
106
+ fs.rmSync(path.join(usrBin, 'link-coreutils.exe'), { force: true });
107
+ fs.renameSync(link, path.join(usrBin, 'link-coreutils.exe'));
108
+ }
109
+ catch (e) {
110
+ warn("Could not rename Git's coreutils link.exe (MSVC link may be shadowed in Git Bash): " + errMsg(e));
111
+ }
112
+ };
97
113
  export async function setupGit(env, hostCopy, refresh, access) {
98
114
  const root = tool('git');
99
115
  const bash = path.join(root, 'bin', 'bash.exe');
@@ -130,6 +146,8 @@ export async function setupGit(env, hostCopy, refresh, access) {
130
146
  throw new Error('bash.exe missing after installing Git');
131
147
  pointAtOwnCertificates(root);
132
148
  env.vars.CLAUDE_CODE_GIT_BASH_PATH = bash;
149
+ // usr\bin is on PATH for Git's tools in cmd and PowerShell, last (path-plan.ts keeps MSVC before it).
150
+ hideCoreutilsLink(path.join(root, 'usr', 'bin'));
133
151
  env.append.push(path.join(root, 'usr', 'bin'));
134
152
  const version = execOutput(gitExe, ['--version']).replace(/^git version /, '');
135
153
  if (access === 'full') {
@@ -4,7 +4,8 @@
4
4
  import crypto from 'node:crypto';
5
5
  import fs from 'node:fs';
6
6
  import path from 'node:path';
7
- import { downloader, spawnAsync } from '../../shared/download/index.js';
7
+ import { runInstaller } from '../runtime/pools.js';
8
+ import { downloader } from '../../shared/download/index.js';
8
9
  import { SYSTEM } from '../../shared/system.js';
9
10
  import { dryRun, isWin, toolsDir } from '../runtime/paths.js';
10
11
  import { exists, mkdirp } from '../runtime/fs.js';
@@ -48,9 +49,7 @@ export async function ensureOpenVpn() {
48
49
  repairServiceQueries();
49
50
  log('VPN: installing OpenVPN');
50
51
  const logFile = path.join(toolsDir, 'downloads', 'openvpn-install.log');
51
- const code = await spawnAsync(SYSTEM.msiexec(), ['/i', msi, '/qn', '/norestart', '/l*v', logFile], {
52
- stdio: 'ignore',
53
- });
52
+ const code = await runInstaller(SYSTEM.msiexec(), ['/i', msi, '/qn', '/norestart', '/l*v', logFile]);
54
53
  // 3010: installed, a restart is wanted (nothing here needs one).
55
54
  if (code !== 0 && code !== 3010)
56
55
  throw new Error('The OpenVPN installer failed (exit ' + code + '). Its log: ' + logFile);
@@ -7,7 +7,8 @@ import crypto from 'node:crypto';
7
7
  import fs from 'node:fs';
8
8
  import os from 'node:os';
9
9
  import path from 'node:path';
10
- import { downloader, spawnAsync } from '../../shared/download/index.js';
10
+ import { runInstaller } from '../runtime/pools.js';
11
+ import { downloader } from '../../shared/download/index.js';
11
12
  import { SYSTEM } from '../../shared/system.js';
12
13
  import { WIREGUARD_MSI } from '../../shared/vpn/download.js';
13
14
  import { handshakeDone, serviceRunning } from '../../shared/vpn/wireguard.js';
@@ -46,9 +47,15 @@ export async function ensureWireguard() {
46
47
  log('VPN: installing WireGuard');
47
48
  const logFile = path.join(toolsDir, 'downloads', 'wireguard-install.log');
48
49
  // DO_NOT_LAUNCH: the installer would otherwise start the manager window.
49
- const code = await spawnAsync(SYSTEM.msiexec(), ['/i', msi, '/qn', '/norestart', 'DO_NOT_LAUNCH=1', '/l*v', logFile], {
50
- stdio: 'ignore',
51
- });
50
+ const code = await runInstaller(SYSTEM.msiexec(), [
51
+ '/i',
52
+ msi,
53
+ '/qn',
54
+ '/norestart',
55
+ 'DO_NOT_LAUNCH=1',
56
+ '/l*v',
57
+ logFile,
58
+ ]);
52
59
  if (code !== 0 && code !== 3010)
53
60
  throw new Error('The WireGuard installer failed (exit ' + code + '). Its log: ' + logFile);
54
61
  if (!exists(exe))
@@ -9,6 +9,8 @@ function howToSave(spec) {
9
9
  return "sign in to the Epic Games Launcher in this sandbox once, or take this PC's: sandbox account import epic";
10
10
  if (spec.kind === 'steam')
11
11
  return 'sign in once in this sandbox with sbx-steam login';
12
+ if (spec.kind === 'comet')
13
+ return 'sign in to Comet once in this sandbox, and it is kept for the next ones';
12
14
  if (spec.returns)
13
15
  return 'sign in once in this sandbox, or: sandbox account import ' + spec.kind;
14
16
  return spec.sources.includes('typed')
@@ -56,6 +58,11 @@ export function prepareServiceAccounts(i) {
56
58
  if (spec.returns)
57
59
  leased.push(kind);
58
60
  }
61
+ else if (spec.returns) {
62
+ // Several sandboxes may use it at once: each gets its own lease, so each may hand a renewed one back.
63
+ leases.claimShared(kind, dataSet, i.launchId, payload ? fingerprint(payload) : null);
64
+ leased.push(kind);
65
+ }
59
66
  if (unreadable) {
60
67
  notes.push('Your saved ' +
61
68
  spec.label +
@@ -61,12 +61,8 @@ export function takeHandoffs(dataPath, final, store = new ServiceAccountStore(st
61
61
  const spec = SERVICE_SPECS[kind];
62
62
  const bytes = readBoundedFile(file, spec.maxBytes + ENVELOPE_BYTES);
63
63
  const parsed = bytes ? handoffSchema(kind).safeParse(JSON.parse(bytes.toString('utf8'))) : null;
64
- const lease = leases.holder(kind);
65
- if (parsed?.success &&
66
- spec.returns &&
67
- lease &&
68
- lease.dataSet === dataSet &&
69
- lease.launchId === parsed.data.launchId) {
64
+ const lease = leases.leaseOf(kind, dataSet);
65
+ if (parsed?.success && spec.returns && lease && lease.launchId === parsed.data.launchId) {
70
66
  const payload = parsed.data.payload;
71
67
  store.save(kind, payload, 'sandbox', HOST_SOURCES[kind].summary(payload));
72
68
  ok = true;
@@ -2,5 +2,5 @@ export { accountsDir, ServiceAccountStore, type AccountInfo, type AccountState }
2
2
  export { AccountLeases, type ClaimResult, type Lease } from './leases.js';
3
3
  export { prepareServiceAccounts, type AccountDelivery, type DeliveryInput } from './deliver.js';
4
4
  export { forgetEverywhere, handoffDir, takeHandoffs } from './handoff.js';
5
- export { notAvailableYet, pcEnv, type HostSource, type PcEnv } from './source.js';
5
+ export { notAvailableYet, parseRegValue, pcEnv, type HostSource, type PcEnv } from './source.js';
6
6
  export { HOST_SOURCES } from './sources.js';
@@ -4,5 +4,5 @@ export { accountsDir, ServiceAccountStore } from './store.js';
4
4
  export { AccountLeases } from './leases.js';
5
5
  export { prepareServiceAccounts } from './deliver.js';
6
6
  export { forgetEverywhere, handoffDir, takeHandoffs } from './handoff.js';
7
- export { notAvailableYet, pcEnv } from './source.js';
7
+ export { notAvailableYet, parseRegValue, pcEnv } from './source.js';
8
8
  export { HOST_SOURCES } from './sources.js';
@@ -1,2 +1,2 @@
1
- import { type HostSource } from '../source.js';
1
+ import type { HostSource } from '../source.js';
2
2
  export declare const cfxSource: HostSource<'cfx'>;
@@ -1,7 +1,50 @@
1
- import { notAvailableYet } from '../source.js';
1
+ // Cfx.re: a FiveM / RedM server licence key from the keymaster (https://portal.cfx.re), and an optional Steam Web API
2
+ // key. Typed with `sandbox account login cfx`, or read from a server.cfg with `sandbox account import cfx --from`.
3
+ import { CfxPayloadSchema } from '../../../shared/accounts/index.js';
4
+ import { fail } from '../../../core/util/index.js';
5
+ import { decodeText } from '../../../shared/io/index.js';
6
+ const MAX_LINES = 20_000;
7
+ const LICENSE_LINE = /^\s*(?:set[rs]?\s+)?sv_licenseKey\s+"?([^"\s]+)"?/i;
8
+ const STEAM_LINE = /^\s*set[rs]?\s+steam_webApiKey\s+"?([A-Fa-f0-9]{32})"?/i;
9
+ /** The line without a `#` or `//` comment (quotes are respected). */
10
+ function withoutComment(line) {
11
+ let quoted = false;
12
+ for (let i = 0; i < line.length; i++) {
13
+ const c = line[i];
14
+ if (c === '"')
15
+ quoted = !quoted;
16
+ else if (!quoted && (c === '#' || (c === '/' && line[i + 1] === '/')))
17
+ return line.slice(0, i);
18
+ }
19
+ return line;
20
+ }
2
21
  export const cfxSource = {
3
- fromAnswers(_answers) {
4
- return notAvailableYet('cfx');
22
+ fromFile(bytes) {
23
+ let licenseKey = '';
24
+ let steamWebApiKey = '';
25
+ for (const raw of decodeText(bytes).text.split(/\r?\n/, MAX_LINES)) {
26
+ const line = withoutComment(raw);
27
+ const key = LICENSE_LINE.exec(line);
28
+ if (key)
29
+ licenseKey = key[1];
30
+ const steam = STEAM_LINE.exec(line);
31
+ if (steam)
32
+ steamWebApiKey = steam[1].toUpperCase();
33
+ }
34
+ if (!licenseKey)
35
+ fail('That server.cfg has no sv_licenseKey.', 'Type the key instead: sandbox account login cfx');
36
+ const parsed = CfxPayloadSchema.safeParse({ licenseKey, ...(steamWebApiKey ? { steamWebApiKey } : {}) });
37
+ if (!parsed.success)
38
+ fail('The sv_licenseKey in that server.cfg is not a Cfx.re server key (cfxk_...).', 'Copy the key from https://portal.cfx.re (Server keys) and run: sandbox account login cfx');
39
+ return parsed.data;
5
40
  },
6
- summary: () => null,
41
+ fromAnswers(answers) {
42
+ const licenseKey = (answers.licenseKey ?? '').trim();
43
+ const steamWebApiKey = (answers.steamWebApiKey ?? '').trim().toUpperCase();
44
+ const parsed = CfxPayloadSchema.safeParse({ licenseKey, ...(steamWebApiKey ? { steamWebApiKey } : {}) });
45
+ if (!parsed.success)
46
+ fail('That is not a Cfx.re server key (cfxk_...) or a Steam Web API key (32 hex characters).', 'Copy the key from https://portal.cfx.re (Server keys) and run: sandbox account login cfx');
47
+ return parsed.data;
48
+ },
49
+ summary: (p) => 'server key' + (p.steamWebApiKey ? ' and Steam Web API key' : ''),
7
50
  };
@@ -0,0 +1,2 @@
1
+ import type { HostSource } from '../source.js';
2
+ export declare const cometSource: HostSource<'comet'>;
@@ -0,0 +1,6 @@
1
+ // Comet: nothing to take from this PC. Its sign-in there is sealed to your Windows account (and reading another
2
+ // browser's cookies is what password stealers do), so it only ever comes from a sandbox you signed in in.
3
+ import { cometSummary } from '../../../shared/accounts/index.js';
4
+ export const cometSource = {
5
+ summary: (p) => cometSummary(p),
6
+ };
@@ -0,0 +1,2 @@
1
+ import type { HostSource } from '../source.js';
2
+ export declare const perplexitySource: HostSource<'perplexity'>;
@@ -0,0 +1,30 @@
1
+ // Perplexity: an API key, taken from this PC (`sandbox account import perplexity`: PERPLEXITY_API_KEY of this
2
+ // terminal, else of your saved Windows user environment), from a file that holds it (`--from .env`: only its
3
+ // PERPLEXITY_API_KEY line is read), or typed (`sandbox account login perplexity`). The key is never printed.
4
+ import { fail } from '../../../core/util/index.js';
5
+ import { PERPLEXITY_ENV, PerplexityPayloadSchema, perplexityKeyFromText, perplexitySummary, } from '../../../shared/accounts/index.js';
6
+ import { decodeText } from '../../../shared/io/index.js';
7
+ /** A key value as it may sit in a variable (quotes and spaces around it taken off). */
8
+ const keyFrom = (value) => {
9
+ const parsed = PerplexityPayloadSchema.safeParse({ apiKey: (value ?? '').trim().replace(/^(["'])(.*)\1$/, '$2') });
10
+ return parsed.success ? parsed.data : null;
11
+ };
12
+ export const perplexitySource = {
13
+ /** PERPLEXITY_API_KEY of this PC: this terminal's environment first, then your saved user environment. */
14
+ fromPc(pc) {
15
+ return keyFrom(pc.env?.[PERPLEXITY_ENV]) ?? keyFrom(pc.userVariable?.(PERPLEXITY_ENV));
16
+ },
17
+ fromFile(bytes) {
18
+ const key = perplexityKeyFromText(decodeText(bytes).text);
19
+ if (!key)
20
+ fail('That file holds no Perplexity API key.', 'Name a .env file with a line PERPLEXITY_API_KEY=pplx-..., or type the key: sandbox account login perplexity');
21
+ return key;
22
+ },
23
+ fromAnswers(answers) {
24
+ const parsed = PerplexityPayloadSchema.safeParse({ apiKey: (answers.apiKey ?? '').trim() });
25
+ if (!parsed.success)
26
+ fail('That is not a Perplexity API key.', 'Copy it from perplexity.ai/account/api/keys: it starts with pplx-.');
27
+ return parsed.data;
28
+ },
29
+ summary: (p) => perplexitySummary(p),
30
+ };