windows-development-sandbox 1.20.0 → 1.23.1

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 (337) 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-7YPEDAJD.js} +172 -48
  43. package/dist/bin/{chunk-2IMT7ZCT.js → chunk-QFSJX2KW.js} +1 -1
  44. package/dist/bin/{chunk-Z2P5OGKL.js → chunk-USTRSHIE.js} +2436 -1693
  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/profile/claude-login.d.ts +2 -2
  221. package/dist/host/profile/claude-login.js +4 -3
  222. package/dist/host/vpn/nordvpn.d.ts +2 -4
  223. package/dist/host/vpn/nordvpn.js +2 -24
  224. package/dist/runtime/agent.js +405 -90
  225. package/dist/runtime/daemon.js +3010 -1383
  226. package/dist/runtime/db.js +333 -90
  227. package/dist/runtime/main.js +4720 -1954
  228. package/dist/runtime/netproxy.js +286 -69
  229. package/dist/runtime/tool.js +2852 -1780
  230. package/dist/runtime/view.js +286 -69
  231. package/dist/selftest/accounts.test.js +1 -1
  232. package/dist/selftest/agent-paths.test.js +39 -1
  233. package/dist/selftest/agent-settings.test.js +6 -0
  234. package/dist/selftest/agents-web.test.d.ts +2 -0
  235. package/dist/selftest/agents-web.test.js +647 -0
  236. package/dist/selftest/app.test.js +5 -2
  237. package/dist/selftest/boot-runner.test.d.ts +2 -0
  238. package/dist/selftest/boot-runner.test.js +456 -0
  239. package/dist/selftest/channel.test.js +15 -0
  240. package/dist/selftest/fivem.test.js +249 -3
  241. package/dist/selftest/github.test.js +94 -9
  242. package/dist/selftest/guest-boot.test.js +3 -0
  243. package/dist/selftest/health.test.js +1 -1
  244. package/dist/selftest/index.js +8 -0
  245. package/dist/selftest/layout.test.js +5 -0
  246. package/dist/selftest/menu-coverage.test.d.ts +2 -0
  247. package/dist/selftest/menu-coverage.test.js +46 -0
  248. package/dist/selftest/menu-screens.js +37 -1
  249. package/dist/selftest/persisted-installs.test.js +6 -3
  250. package/dist/selftest/service-accounts.test.js +3 -2
  251. package/dist/selftest/support/engine-golden.js +27 -0
  252. package/dist/selftest/support/fixtures.d.ts +6 -0
  253. package/dist/selftest/support/web-preview.d.ts +34 -1
  254. package/dist/selftest/support/web-preview.js +21 -18
  255. package/dist/selftest/toolchains.test.js +23 -1
  256. package/dist/selftest/ui-text.test.d.ts +7 -0
  257. package/dist/selftest/ui-text.test.js +300 -0
  258. package/dist/shared/accounts/anthropic.d.ts +1 -1
  259. package/dist/shared/accounts/anthropic.js +9 -8
  260. package/dist/shared/accounts/index.d.ts +2 -0
  261. package/dist/shared/accounts/index.js +2 -0
  262. package/dist/shared/accounts/kinds/comet.d.ts +12 -0
  263. package/dist/shared/accounts/kinds/comet.js +64 -0
  264. package/dist/shared/accounts/kinds/perplexity.d.ts +8 -0
  265. package/dist/shared/accounts/kinds/perplexity.js +37 -0
  266. package/dist/shared/accounts/kinds.d.ts +24 -5
  267. package/dist/shared/accounts/kinds.js +86 -4
  268. package/dist/shared/accounts/payloads.d.ts +81 -0
  269. package/dist/shared/accounts/payloads.js +39 -1
  270. package/dist/shared/apps/agent-addons.d.ts +33 -5
  271. package/dist/shared/apps/agent-addons.js +95 -3
  272. package/dist/shared/apps/catalogue-agents-web.d.ts +14 -0
  273. package/dist/shared/apps/catalogue-agents-web.js +112 -0
  274. package/dist/shared/apps/catalogue-modding.js +32 -1
  275. package/dist/shared/apps/catalogue.d.ts +27 -3
  276. package/dist/shared/apps/catalogue.js +2 -1
  277. package/dist/shared/apps/index.d.ts +3 -1
  278. package/dist/shared/apps/index.js +3 -1
  279. package/dist/shared/channel/schema.d.ts +21 -3
  280. package/dist/shared/channel/schema.js +7 -0
  281. package/dist/shared/channel/secrets.d.ts +19 -0
  282. package/dist/shared/concurrency/index.d.ts +3 -0
  283. package/dist/shared/concurrency/index.js +3 -0
  284. package/dist/shared/concurrency/installer-retry.d.ts +6 -0
  285. package/dist/shared/concurrency/installer-retry.js +10 -0
  286. package/dist/shared/concurrency/limiter.d.ts +19 -0
  287. package/dist/shared/concurrency/limiter.js +67 -0
  288. package/dist/shared/concurrency/pools.d.ts +7 -0
  289. package/dist/shared/concurrency/pools.js +9 -0
  290. package/dist/shared/download/archive.d.ts +2 -0
  291. package/dist/shared/download/archive.js +11 -1
  292. package/dist/shared/download/bounded-fetch.d.ts +13 -0
  293. package/dist/shared/download/bounded-fetch.js +40 -0
  294. package/dist/shared/download/downloader.d.ts +5 -0
  295. package/dist/shared/download/downloader.js +50 -38
  296. package/dist/shared/download/index.d.ts +2 -1
  297. package/dist/shared/download/index.js +2 -1
  298. package/dist/shared/readiness.d.ts +2 -2
  299. package/dist/shared/readiness.js +2 -2
  300. package/dist/web/index.html +1 -1
  301. package/dist/web/{main-OUUQYNK4.js → main-CCILLX2M.js} +67 -67
  302. package/native/csharp/HcsHost/AssemblyInfo.cs +3 -3
  303. package/native/csharp/HostTools/AssemblyInfo.cs +3 -3
  304. package/native/csharp/StorageHelper/AssemblyInfo.cs +3 -3
  305. package/package.json +1 -1
  306. package/sandbox/addons/browser-use-mcp/requirements.in +1 -0
  307. package/sandbox/addons/browser-use-mcp/requirements.txt +2350 -0
  308. package/sandbox/addons/chrome-devtools-mcp/package-lock.json +38 -0
  309. package/sandbox/addons/chrome-devtools-mcp/package.json +7 -0
  310. package/sandbox/addons/comet-mcp/package-lock.json +1277 -0
  311. package/sandbox/addons/comet-mcp/package.json +7 -0
  312. package/sandbox/addons/perplexity-mcp/package-lock.json +1655 -0
  313. package/sandbox/addons/perplexity-mcp/package.json +7 -0
  314. package/sandbox/scripts/add-firewall-port.ps1 +18 -4
  315. package/sandbox/scripts/set-machine-environment.ps1 +30 -15
  316. package/sandbox/templates/fivem/README.txt +6 -2
  317. package/sandbox/templates/fivem/cs/Client/Client.csproj +15 -0
  318. package/sandbox/templates/fivem/cs/Client/ClientMain.cs +20 -0
  319. package/sandbox/templates/fivem/cs/Server/Server.csproj +14 -0
  320. package/sandbox/templates/fivem/cs/Server/ServerMain.cs +20 -0
  321. package/sandbox/templates/fivem/cs/fxmanifest.lua +11 -0
  322. package/sandbox/templates/fivem/js/client.js +12 -0
  323. package/sandbox/templates/fivem/js/fxmanifest.lua +9 -0
  324. package/sandbox/templates/fivem/js/server.js +6 -0
  325. package/sandbox/templates/fivem/lua/client.lua +8 -0
  326. package/sandbox/templates/fivem/lua/fxmanifest.lua +10 -0
  327. package/sandbox/templates/fivem/lua/server.lua +6 -0
  328. package/sandbox/templates/fivem/server.cfg +37 -0
  329. package/sandbox/templates/fivem/ts/build.js +17 -0
  330. package/sandbox/templates/fivem/ts/fxmanifest.lua +9 -0
  331. package/sandbox/templates/fivem/ts/package.json +16 -0
  332. package/sandbox/templates/fivem/ts/src/client/client.ts +12 -0
  333. package/sandbox/templates/fivem/ts/src/client/tsconfig.json +13 -0
  334. package/sandbox/templates/fivem/ts/src/server/server.ts +6 -0
  335. package/sandbox/templates/fivem/ts/src/server/tsconfig.json +13 -0
  336. package/schemas/config.schema.json +56 -27
  337. package/schemas/sandbox-config.schema.json +37 -14
@@ -1,7 +1,253 @@
1
+ // FiveM / RedM: server builds from the versions API (a saved copy of its answer), the signature rule that stands in
2
+ // for checksums, the Cfx.re key's cfg on the sandbox's own disk, how sbx-fivem serve starts FXServer (RedM, txAdmin,
3
+ // oxmysql), the resource scaffolds, the toolchain hints and the modding apps.
4
+ import fs from 'node:fs';
5
+ import path from 'node:path';
6
+ import { fakeIO } from '../app/kernel/index.js';
7
+ import { importServiceFlow, loginServiceFlow } from '../app/features/accounts/index.js';
8
+ import { DEFAULTS } from '../core/config/index.js';
9
+ import { isKindReady, isToolchainReady } from '../shared/readiness.js';
10
+ import { HOST_SOURCES, ServiceAccountStore } from '../host/accounts/index.js';
11
+ import { fivemHints } from '../host/launch/hints/fivem.js';
12
+ import { SecretStore } from '../host/secrets/index.js';
13
+ import { applyServiceAccounts, readDelivered } from '../guest/accounts/apply.js';
14
+ import { accountsRoot } from '../guest/accounts/adapter.js';
15
+ import { cfxAdapter, cfxSecretsFile, cfxSecretsText } from '../guest/accounts/kinds/cfx.js';
16
+ import { ARTIFACTS_PAGE, artifactSetting, artifactTrust, chooseArtifact, parseArtifactsPage, parseVersions, pickArtifact, } from '../guest/toolchains/fivem/artifacts.js';
17
+ import { csNamespace, initServerFolder, RESOURCE_LANGS, resourceHome, scaffoldResource, templateFiles, withEnsure, } from '../guest/toolchains/fivem/scaffold.js';
18
+ import { GAME_PORT, mysqlConnectionString, servePlan, TXADMIN_PORT } from '../guest/toolchains/fivem/serve-plan.js';
19
+ import { APPS } from '../shared/apps/index.js';
20
+ import { MODDING_APPS } from '../shared/apps/catalogue-modding.js';
21
+ import { Downloader } from '../shared/download/index.js';
22
+ const KEY = 'cfxk_PlantedFivemKey_0123456789abcdef';
23
+ const STEAM = '0123456789ABCDEF0123456789ABCDEF';
24
+ const ROCKSTAR = 'CN="Rockstar Games, Inc.", O="Rockstar Games, Inc.", L=New York, S=New York, C=US';
25
+ const allFiles = (dir) => fs.existsSync(dir)
26
+ ? fs
27
+ .readdirSync(dir, { recursive: true, encoding: 'utf8' })
28
+ .map((f) => path.join(dir, f))
29
+ .filter((f) => fs.statSync(f).isFile())
30
+ : [];
1
31
  export const fivemSuite = {
2
32
  name: 'fivem',
3
- title: 'FiveM / RedM: server builds, the Cfx.re key, sbx-fivem (not available yet)',
4
- run: (t) => {
5
- t.check(true, 'fivem: not available yet');
33
+ title: 'FiveM / RedM: server builds, the Cfx.re key, sbx-fivem serve, scaffolds, hints, modding apps',
34
+ run: async (t) => {
35
+ const fixtures = path.join(t.packageRoot, 'src', 'selftest', 'fixtures', 'fivem');
36
+ const versionsJson = JSON.parse(fs.readFileSync(path.join(fixtures, 'versions.json'), 'utf8'));
37
+ const pageHtml = fs.readFileSync(path.join(fixtures, 'artifacts-page.txt'), 'utf8');
38
+ t.check(isToolchainReady('fivem') && isKindReady('cfx') && isKindReady('perplexity') && isKindReady('comet'), 'FiveM, the Cfx.re key and the Perplexity sign-ins are written: settings and accounts list them');
39
+ t.section('server builds (versions API)');
40
+ const channels = parseVersions(versionsJson);
41
+ t.check(channels.recommended.build === 35245 &&
42
+ channels.latest.build === 38182 &&
43
+ channels.optional.build === 7290 &&
44
+ channels.critical.build === 7290 &&
45
+ channels.recommended.url.endsWith('/35245-6efb47dff473c0e2a12fb50b08d74c0eb24a50d5/server.zip') &&
46
+ channels.recommended.txAdmin === '8.1.1' &&
47
+ channels.latest.txAdmin === '8.1.2', 'the versions API gives each channel its build, its server.zip and its txAdmin');
48
+ await t.throws(() => parseVersions({
49
+ ...versionsJson,
50
+ latest_download: 'https://evil.example/38182/server.zip',
51
+ }), '', 'a download anywhere but runtime.fivem.net is refused');
52
+ await t.throws(() => parseVersions({ recommended: '1' }), '', 'an answer of another shape is refused');
53
+ const page = parseArtifactsPage(pageHtml);
54
+ t.check(page.length === 4 &&
55
+ page[0].build === 38182 &&
56
+ page.every((a) => a.url.startsWith(ARTIFACTS_PAGE) && a.url.endsWith('/server.zip')) &&
57
+ page.some((a) => a.build === 38164 && a.url.includes('38164-c7a8db730dbaaf1519221ac63e179e2f3af48d55')), 'the artifacts page gives the Windows builds (newest first, as server.zip), not Linux or other servers', JSON.stringify(page));
58
+ t.check(artifactSetting('recommended') === 'recommended' &&
59
+ artifactSetting('38164') === 38164 &&
60
+ artifactSetting('newest') === null &&
61
+ artifactSetting('1e3') === null, 'a setting is a channel or a build number');
62
+ t.check(chooseArtifact('recommended', channels)?.build === 35245 &&
63
+ chooseArtifact('latest', channels)?.build === 38182 &&
64
+ chooseArtifact('optional', channels)?.build === 7290 &&
65
+ chooseArtifact('7290', channels)?.url === channels.optional.url &&
66
+ chooseArtifact('38164', channels) === null &&
67
+ chooseArtifact('38164', channels, page)?.build === 38164, 'recommended / latest / optional pick their channel; a pinned build is a channel build or one on the page');
68
+ let pageAsked = 0;
69
+ const sources = {
70
+ versions: () => Promise.resolve(versionsJson),
71
+ page: () => {
72
+ pageAsked++;
73
+ return Promise.resolve(pageHtml);
74
+ },
75
+ };
76
+ const rec = await pickArtifact('recommended', sources);
77
+ const pinned = await pickArtifact('38164', sources);
78
+ const unknown = await pickArtifact('12', sources);
79
+ t.check(rec?.build === 35245 && pinned?.build === 38164 && unknown === null && pageAsked === 2, 'pickArtifact reads the page only for a pinned build that no channel names; an unknown build is null');
80
+ t.equal(await pickArtifact('soon', sources), null, 'a setting that names nothing is null');
81
+ t.section('no checksums: the signature rule');
82
+ t.equal(artifactTrust(true, ROCKSTAR), 'signed', 'requireChecksum on: a valid Rockstar Games signature passes');
83
+ t.equal(artifactTrust(false, ROCKSTAR), 'signed', 'and without it too');
84
+ t.equal(artifactTrust(true, ''), 'refused', 'requireChecksum on: unsigned (or tampered) is refused');
85
+ t.equal(artifactTrust(false, ''), 'unsigned-allowed', 'requireChecksum off: unsigned is used with a warning');
86
+ t.equal(artifactTrust(true, 'CN="Rockstar Games, Inc. Mods", O=Somebody Else, C=US'), 'refused', 'a look-alike signer is not Rockstar Games');
87
+ t.equal(artifactTrust(true, 'CN=Valve Corp., O=Valve Corp., C=US'), 'refused', 'another vendor is not enough');
88
+ const strict = new Downloader();
89
+ strict.requireChecksum = true;
90
+ await t.throws(() => strict.file(channels.recommended.url, t.at('fivem-dl', 'server.zip'), null), 'requireChecksum', 'requireChecksum: a build without the signature check is refused before anything downloads');
91
+ t.check(!fs.existsSync(t.at('fivem-dl', 'server.zip')), 'and nothing is written');
92
+ t.section('the Cfx.re key');
93
+ const parsed = HOST_SOURCES.cfx.fromAnswers({ licenseKey: KEY, steamWebApiKey: STEAM.toLowerCase() });
94
+ t.check(parsed.licenseKey === KEY && parsed.steamWebApiKey === STEAM, 'typed: the key and the Steam Web API key (upper-cased)');
95
+ t.check(!HOST_SOURCES.cfx.fromPc && HOST_SOURCES.cfx.fromFile !== undefined && HOST_SOURCES.cfx.fromAnswers !== undefined, 'the key is typed or read from a server.cfg (nothing on the PC to take it from)');
96
+ const cfg = [
97
+ '# my server',
98
+ 'sv_licenseKey "cfxk_OldKey_000" // old',
99
+ 'endpoint_add_tcp "0.0.0.0:30120"',
100
+ '// setr steam_webApiKey "FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF"',
101
+ 'sets tags "a#b"',
102
+ 'setr steam_webApiKey "' + STEAM.toLowerCase() + '"',
103
+ 'set sv_licenseKey ' + KEY + ' # the real one',
104
+ ].join('\r\n');
105
+ const fromCfg = HOST_SOURCES.cfx.fromFile(Buffer.from(cfg, 'utf8'), {});
106
+ t.check(fromCfg.licenseKey === KEY && fromCfg.steamWebApiKey === STEAM, 'server.cfg: comments and quotes handled, setr works, the last key wins, the Steam key is upper-cased');
107
+ await t.throws(() => HOST_SOURCES.cfx.fromFile(Buffer.from('# sv_licenseKey cfxk_x\nendpoint_add_tcp "0.0.0.0:30120"', 'utf8'), {}), 'no sv_licenseKey', 'a commented-out key is not a key');
108
+ t.check(!(HOST_SOURCES.cfx.summary(fromCfg) ?? '').includes(KEY), 'the summary of an imported key never holds it');
109
+ const cfgFile = t.write(t.at('fivem-cfg', 'server.cfg'), cfg);
110
+ const cfgStore = new ServiceAccountStore(t.dir('fivem-cfg-store'), SecretStore.memory());
111
+ const cfgIO = fakeIO([true]);
112
+ const pcNone = { localAppData: '', appData: '', programData: '' };
113
+ const imported = await importServiceFlow(cfgIO.io, 'cfx', cfgFile, { store: cfgStore, pc: pcNone, share: false });
114
+ t.check(cfgStore.read('cfx')?.licenseKey === KEY &&
115
+ cfgIO.seen.questions.length === 1 &&
116
+ !JSON.stringify(cfgIO.seen).includes(KEY) &&
117
+ !imported.message.includes(KEY), 'import cfx --from server.cfg asks only to confirm (no fields), saves it, never shows it');
118
+ const big = t.write(t.at('fivem-cfg', 'big.cfg'), '#'.repeat(256 * 1024 + 1));
119
+ await t.throws(() => importServiceFlow(fakeIO([true]).io, 'cfx', big, { store: cfgStore, pc: pcNone, share: false }), 'Cannot read', 'a server.cfg over 256 KB is refused');
120
+ await t.throws(() => HOST_SOURCES.cfx.fromAnswers({ licenseKey: 'cfxk_x" +exec evil.cfg "' }), 'not a Cfx.re server key', 'a key with quotes or spaces is refused (it could not break out of its cfg line anyway)');
121
+ const summary = HOST_SOURCES.cfx.summary(parsed) ?? '';
122
+ t.check(!summary.includes(KEY) && !summary.includes(STEAM), 'the summary never holds the key');
123
+ const store = new ServiceAccountStore(t.dir('fivem-store'), SecretStore.memory());
124
+ const saved = await loginServiceFlow(fakeIO([KEY, '']).io, 'cfx', { store, share: false });
125
+ t.check(store.read('cfx')?.licenseKey === KEY &&
126
+ !saved.message.includes(KEY) &&
127
+ saved.message.includes('Share with sandboxes'), 'sandbox account login cfx saves it sealed, says how to share it, never shows it');
128
+ const oldLocal = process.env.LOCALAPPDATA;
129
+ const local = t.dir('fivem-local');
130
+ const project = t.dir('fivem-project');
131
+ const data = t.dir('fivem-data');
132
+ process.env.LOCALAPPDATA = local;
133
+ try {
134
+ const env = await applyServiceAccounts({ cfx: { licenseKey: KEY, steamWebApiKey: STEAM } }, { root: accountsRoot() });
135
+ const cfg = cfxSecretsFile();
136
+ const text = fs.existsSync(cfg) ? fs.readFileSync(cfg, 'utf8') : '';
137
+ t.check(cfg === path.join(local, 'AgentSandbox', 'accounts', 'cfx', 'server-secrets.cfg') &&
138
+ text.includes('sv_licenseKey "' + KEY + '"') &&
139
+ text.includes('set steam_webApiKey "' + STEAM + '"'), "boot writes the key to a cfg in the sandbox's own folder (%LOCALAPPDATA%\\AgentSandbox\\accounts\\cfx)", cfg);
140
+ const leaks = [...allFiles(project), ...allFiles(data), ...allFiles(path.join(local, 'AgentSandbox'))].filter((f) => f !== cfg && fs.readFileSync(f, 'utf8').includes(KEY));
141
+ t.check(!leaks.length && !Object.values(env).some((v) => v.includes(KEY)), 'the key is nowhere else: not in the project, the data folder, delivered.json or the agent environment', leaks.join(', '));
142
+ t.check(!JSON.stringify(readDelivered(accountsRoot())).includes(KEY), 'delivered.json holds only a fingerprint');
143
+ t.check((await cfxAdapter.apply({ licenseKey: KEY })).summary.includes('sbx-fivem serve') &&
144
+ !fs.readFileSync(cfg, 'utf8').includes('steam_webApiKey'), 'without a Steam Web API key its line is left out');
145
+ t.check(cfxSecretsText({ licenseKey: KEY })
146
+ .split('\r\n')
147
+ .filter((l) => l && !l.startsWith('#')).length === 1, 'the cfg is one setting per line, nothing else');
148
+ }
149
+ finally {
150
+ if (oldLocal === undefined)
151
+ delete process.env.LOCALAPPDATA;
152
+ else
153
+ process.env.LOCALAPPDATA = oldLocal;
154
+ }
155
+ t.section('sbx-fivem serve');
156
+ const secrets = 'C:\\Users\\WDAGUtilityAccount\\AppData\\Local\\AgentSandbox\\accounts\\cfx\\server-secrets.cfg';
157
+ const base = {
158
+ artifactDir: 'C:\\AgentData\\tools\\fivem\\artifacts\\35245',
159
+ serverDir: 'C:\\Workspace\\server',
160
+ game: 'gta5',
161
+ txAdmin: false,
162
+ secretsCfg: secrets,
163
+ txData: 'C:\\AgentData\\state\\fivem\\txData',
164
+ mysqlPort: null,
165
+ };
166
+ const plain = servePlan(base);
167
+ const execs = plain.args.flatMap((a, i) => (a === '+exec' ? [plain.args[i + 1]] : []));
168
+ t.check(plain.exe === 'C:\\AgentData\\tools\\fivem\\artifacts\\35245\\FXServer.exe' &&
169
+ plain.cwd === base.serverDir &&
170
+ execs.join('|') === secrets.split('\\').join('/') + '|server.cfg' &&
171
+ plain.args.includes('citizen_dir') &&
172
+ !plain.args.includes('gamename') &&
173
+ !plain.args.join(' ').includes('cfxk_'), "plain: FXServer by its full path, +exec the key's cfg first and then server.cfg, from the server folder", JSON.stringify(plain.args));
174
+ t.equal(plain.ports, [{ port: GAME_PORT, protocol: 'Both', name: 'FiveM server' }], 'plain: opens 30120 (TCP and UDP) in the sandbox firewall');
175
+ const redm = servePlan({ ...base, game: 'rdr3' });
176
+ const g = redm.args.indexOf('gamename');
177
+ t.check(g > 0 && redm.args[g - 1] === '+set' && redm.args[g + 1] === 'rdr3', 'RedM: +set gamename rdr3');
178
+ t.check(redm.args.indexOf('gamename') < redm.args.indexOf('+exec'), 'and before the cfgs (they can still change it)');
179
+ const noKey = servePlan({ ...base, secretsCfg: null });
180
+ t.check(noKey.args.filter((a) => a === '+exec').length === 1, 'no key handed over: only server.cfg');
181
+ const withDb = servePlan({ ...base, mysqlPort: 3307 });
182
+ const m = withDb.args.indexOf('mysql_connection_string');
183
+ t.check(m > 0 &&
184
+ withDb.args[m + 1] === mysqlConnectionString(3307) &&
185
+ withDb.args[m + 1] === 'mysql://root@127.0.0.1:3307/fivem?charset=utf8mb4' &&
186
+ m < withDb.args.indexOf('+exec'), "oxmysql: mysql_connection_string points at the sandbox's MariaDB, set before server.cfg (a line there wins)");
187
+ const tx = servePlan({ ...base, txAdmin: true, game: 'rdr3', mysqlPort: 3306 });
188
+ t.check(!tx.args.includes('+exec') &&
189
+ tx.env.TXHOST_DATA_PATH === base.txData &&
190
+ tx.env.TXHOST_TXA_PORT === String(TXADMIN_PORT) &&
191
+ tx.env.TXHOST_GAME_NAME === 'redm' &&
192
+ tx.env.TXHOST_DEFAULT_DBPORT === '3306' &&
193
+ !Object.keys(tx.env).includes('TXHOST_DEFAULT_CFXKEY') &&
194
+ tx.ports.some((p) => p.port === TXADMIN_PORT && p.protocol === 'TCP') &&
195
+ tx.ports.some((p) => p.port === GAME_PORT), 'txAdmin: no +exec, txData in state\\fivem through TXHOST_*, RedM recipes, the database, ports 30120 and 40120; the key is never prefilled', JSON.stringify(tx));
196
+ t.section('scaffolds');
197
+ const templates = path.join(t.packageRoot, 'sandbox', 'templates', 'fivem');
198
+ const server = t.dir('fivem-server');
199
+ t.equal(initServerFolder(templates, server, 'myserver', 'gta5'), ['server.cfg', 'resources\\[local]'], 'init writes server.cfg and resources\\[local]');
200
+ const cfgText = fs.readFileSync(path.join(server, 'server.cfg'), 'utf8');
201
+ t.check(!/^\s*(sv_licenseKey|set\s+steam_webApiKey)/m.test(cfgText) &&
202
+ cfgText.includes('endpoint_add_udp "0.0.0.0:30120"') &&
203
+ !cfgText.includes('{{'), 'the server.cfg template sets no key (only comments name it) and listens on 30120');
204
+ t.equal(initServerFolder(templates, server, 'myserver', 'gta5'), [], 'init again changes nothing');
205
+ t.equal(resourceHome(server), path.join(server, 'resources', '[local]'), 'resources go in resources\\[local]');
206
+ t.equal(resourceHome(project), project, 'outside a server folder: in the folder itself');
207
+ for (const lang of RESOURCE_LANGS) {
208
+ const made = scaffoldResource({ templates, lang, name: 'my-' + lang, into: resourceHome(server), game: 'gta5' });
209
+ const texts = made.files.map((f) => fs.readFileSync(path.join(made.dir, f), 'utf8'));
210
+ const manifest = fs.readFileSync(path.join(made.dir, 'fxmanifest.lua'), 'utf8');
211
+ t.check(made.files.length >= 3 &&
212
+ texts.every((x) => !x.includes('{{')) &&
213
+ manifest.includes("game 'gta5'") &&
214
+ manifest.includes("name 'my-" + lang + "'") &&
215
+ !manifest.includes('rdr3_warning'), lang + ': a resource with its manifest, every placeholder filled', made.files.join(', '));
216
+ }
217
+ const cs = fs.readFileSync(path.join(resourceHome(server), 'my-cs', 'Client', 'ClientMain.cs'), 'utf8');
218
+ t.check(cs.includes('namespace MyCs.Client') &&
219
+ fs.readFileSync(path.join(resourceHome(server), 'my-cs', 'fxmanifest.lua'), 'utf8').includes('.net.dll'), 'C#: a namespace from the name, and a manifest that loads the *.net.dll builds');
220
+ t.check(templateFiles(path.join(templates, 'ts')).includes(path.join('src', 'client', 'client.ts')), 'TypeScript: client and server sources with a build script');
221
+ const red = scaffoldResource({ templates, lang: 'lua', name: 'redres', into: project, game: 'rdr3' });
222
+ const redManifest = fs.readFileSync(path.join(red.dir, 'fxmanifest.lua'), 'utf8');
223
+ t.check(redManifest.includes("game 'rdr3'") && redManifest.includes('rdr3_warning'), "RedM: game 'rdr3' and its prerelease acknowledgement");
224
+ await t.throws(() => scaffoldResource({ templates, lang: 'lua', name: 'redres', into: project, game: 'rdr3' }), 'already exists', 'an existing resource is never overwritten');
225
+ await t.throws(() => scaffoldResource({ templates, lang: 'lua', name: '..\\escape', into: project, game: 'gta5' }), 'not a resource name', 'a name with a path in it is refused');
226
+ t.equal(csNamespace('9lives_mod-x'), 'R9livesModX', 'C# namespaces never start with a digit');
227
+ const ensured = withEnsure('ensure chat\n', 'my-lua');
228
+ t.check(ensured === 'ensure chat\nensure my-lua\n' &&
229
+ withEnsure(ensured, 'my-lua') === null &&
230
+ withEnsure('ensure my-luax', 'my-lua') !== null, 'new adds ensure <name> to server.cfg once');
231
+ t.section('hints');
232
+ const hinted = t.dir('fivem-hints');
233
+ t.write(path.join(hinted, 'resources', '[local]', 'shop', 'fxmanifest.lua'), "fx_version 'cerulean'\nclient_script 'client.lua'\n");
234
+ const settings = structuredClone(DEFAULTS.toolchains);
235
+ const h1 = fivemHints(hinted, settings);
236
+ t.check((h1.fivem ?? '').includes('resources/[local]/shop/fxmanifest.lua') && h1.dotnet === undefined, 'a resource 3 folders down turns fivem on (a Lua one not dotnet)', JSON.stringify(h1));
237
+ t.write(path.join(hinted, 'resources', '[local]', 'bank', 'fxmanifest.lua'), "fx_version 'cerulean'\nclient_script 'Client/bin/Release/**/publish/*.net.dll'\n");
238
+ t.check((fivemHints(hinted, settings).dotnet ?? '').includes('C#'), 'a manifest loading *.net.dll turns dotnet on');
239
+ const csproj = t.dir('fivem-hints-cs');
240
+ t.write(path.join(csproj, 'res', '__resource.lua'), "resource_manifest_version '44febabe-d386-4d18-afbe-5e627f4af937'\n");
241
+ t.write(path.join(csproj, 'res', 'Server', 'Server.csproj'), '<Project />');
242
+ t.check(Boolean(fivemHints(csproj, settings).dotnet), 'so does a .csproj in the resource (or one folder below)');
243
+ const deep = t.dir('fivem-hints-deep');
244
+ t.write(path.join(deep, 'a', 'b', 'c', 'd', 'fxmanifest.lua'), '');
245
+ t.write(path.join(deep, 'node_modules', 'x', 'fxmanifest.lua'), '');
246
+ t.equal(fivemHints(deep, settings), {}, 'deeper than 3 folders, or under node_modules: no hint');
247
+ t.equal(fivemHints(hinted, { ...settings, fivem: { ...settings.fivem, enabled: false } }), {}, 'fivem off: no hints');
248
+ t.section('modding apps');
249
+ t.check(['lua-language-server', 'stylua', 'selene'].every((id) => APPS.some((a) => a.id === id)) &&
250
+ MODDING_APPS.every((a) => 'github' in a.source && a.cli === true) &&
251
+ !APPS.some((a) => a.id === 'codewalker'), 'lua-language-server, StyLua and selene are in the catalogue (GitHub releases with their published SHA-256); CodeWalker is not (no download to check)');
6
252
  },
7
253
  };
@@ -2,20 +2,28 @@
2
2
  // one-shot secrets file), the shared-core operations, the project-file rules, and the gh commands the sandbox runs.
3
3
  import fs from 'node:fs';
4
4
  import path from 'node:path';
5
- import { deleteGithubTokenFlow, githubPage, githubTokenRowOf, saveGithubTokenFlow, } from '../app/features/github/index.js';
5
+ import { deleteGithubTokenFlow, expiryOf, githubItem, githubPage, newTokenUrl, tokenUrlFor, githubTokenRowOf, saveGithubTokenFlow, } from '../app/features/github/index.js';
6
6
  import { fakeIO } from '../app/kernel/index.js';
7
7
  import { runDefinition } from '../app/registry.js';
8
8
  import { pageLines } from '../cli/output/index.js';
9
9
  import { DEFAULTS } from '../core/config/index.js';
10
10
  import { applyOverrides, normalizeOverrides } from '../core/local-config/index.js';
11
11
  import { ConfigSchema, LocalOverridesSchema } from '../core/schema/index.js';
12
- import { checkGithubToken, GithubToken, githubTokenAllowed, prepareGithubToken } from '../host/github/index.js';
12
+ import { checkGithubToken, GithubToken, githubTokenAllowed, prepareGithubToken, verifyGithubToken, } from '../host/github/index.js';
13
+ import { UserError } from '../core/util/index.js';
14
+ import { report } from '../cli/report.js';
15
+ import { toWireError, fromWireError } from '../app/kernel/index.js';
16
+ import { accountsPage } from '../app/features/accounts/index.js';
13
17
  import { generateSandbox } from '../host/launch/index.js';
14
18
  import { insideSandbox } from '../host/sandbox/platform.js';
15
19
  import { clearSecrets, SecretStore } from '../host/secrets/index.js';
16
20
  import { RunDir } from '../shared/channel/index.js';
17
21
  import { SecretsFileSchema } from '../shared/channel/secrets.js';
18
22
  import { ghLoginCommand, ghOutputText, ghSetupGitCommand, githubTokenProblem } from '../shared/github/index.js';
23
+ const okVerdict = () => Promise.resolve({
24
+ kind: 'ok',
25
+ check: { login: 'josh', expiresAt: '2027-02-03T10:00:00.000Z', repos: { count: 2, sample: ['josh/a', 'josh/b'] } },
26
+ });
19
27
  const FINE = 'github_pat_' + 'PlantedFine0123456789'.repeat(3);
20
28
  const CLASSIC = 'ghp_' + 'PlantedClassic0123456789ab';
21
29
  const OAUTH = 'gho_' + 'PlantedOauth0123456789abcd';
@@ -86,7 +94,7 @@ export const githubSuite = {
86
94
  t.equal(prepareGithubToken(config, config.storage.root, tokens), { notes: [] }, 'off: nothing, and nothing said');
87
95
  config.github.shareToken = true;
88
96
  const none = prepareGithubToken(config, config.storage.root, tokens);
89
- t.check(none.token === undefined && none.notes.some((n) => n.includes('sandbox github login')), 'on without a saved token: not sent, and it says how to save one');
97
+ t.check(none.token === undefined && none.notes.some((n) => n.includes('Save one')), 'on without a saved token: not sent, and it says how to save one');
90
98
  tokens.save(FINE);
91
99
  for (const access of ['readonly', 'none']) {
92
100
  const strict = structuredClone(config);
@@ -173,15 +181,15 @@ export const githubSuite = {
173
181
  t.section('the shared-core operations');
174
182
  const featureStore = new GithubToken(t.dir('github-feature'), SecretStore.memory());
175
183
  const { io: loginIO, seen: loginSeen } = fakeIO([FINE]);
176
- const saved = await saveGithubTokenFlow(loginIO, featureStore);
184
+ const saved = await saveGithubTokenFlow(loginIO, featureStore, okVerdict);
177
185
  t.check(loginSeen.questions.length === 1 &&
178
186
  loginSeen.questions[0].kind === 'secret' &&
179
187
  featureStore.state() === 'saved' &&
180
- loginSeen.logs.some((l) => l.text.includes('personal-access-tokens/new')) &&
188
+ loginSeen.logs.some((l) => l.text.includes('Make a new token')) &&
181
189
  loginSeen.logs.some((l) => l.text.includes('agent in the sandbox can read')), 'login shows the steps, asks exactly one hidden question, saves, and warns the agent can read it', JSON.stringify(loginSeen.questions));
182
190
  t.check(!JSON.stringify(saved).includes(FINE) && !JSON.stringify(loginSeen).includes(FINE.slice(12)), 'nothing it returns or logs holds the token');
183
191
  const { io: retryIO, seen: retrySeen } = fakeIO([OAUTH, CLASSIC]);
184
- await saveGithubTokenFlow(retryIO, featureStore);
192
+ await saveGithubTokenFlow(retryIO, featureStore, okVerdict);
185
193
  t.check(retrySeen.questions.length === 2 &&
186
194
  featureStore.kind() === 'classic' &&
187
195
  !JSON.stringify(retrySeen).includes(OAUTH.slice(4)) &&
@@ -198,11 +206,14 @@ export const githubSuite = {
198
206
  token: 'saved',
199
207
  kind: 'classic',
200
208
  savedAt: '2026-10-18T10:00:00.000Z',
209
+ login: null,
210
+ expiresAt: null,
211
+ repoCount: null,
212
+ checkedAt: null,
213
+ tokenUrl: 'https://github.com/settings/personal-access-tokens/new',
201
214
  };
202
215
  const lines = pageLines(githubPage(sample)).join('\n');
203
- t.check(lines.includes('saved (encrypted)') &&
204
- lines.includes('only handed over with "full"') &&
205
- lines.includes('classic'), 'the status page says saved, the git access it needs, and the kind', lines);
216
+ t.check(lines.includes('saved') && lines.includes('only handed over with "full"') && lines.includes('classic'), 'the status page says saved, the git access it needs, and the kind', lines);
206
217
  t.equal(githubTokenRowOf({ ...sample, token: 'none' }).hint, 'not saved', 'the Settings row says not saved');
207
218
  const { io: logoutIO, seen: logoutSeen } = fakeIO([true]);
208
219
  const out = await deleteGithubTokenFlow(logoutIO, featureStore);
@@ -220,6 +231,80 @@ export const githubSuite = {
220
231
  await t.throws(() => runDefinition('github.login', {}, {}, io), 'inside the sandbox already', 'github login refuses inside the sandbox');
221
232
  t.check(seen.questions.length === 0, 'and asks no question first');
222
233
  }
234
+ t.section('GitHub: the pre-filled link, the check after saving, the expiry');
235
+ const url = new URL(newTokenUrl({ name: 'Windows Sandbox PC', days: 90, permissions: { contents: 'write', metadata: 'read' } }));
236
+ t.check(url.origin + url.pathname === 'https://github.com/settings/personal-access-tokens/new' &&
237
+ url.searchParams.get('name') === 'Windows Sandbox PC' &&
238
+ url.searchParams.get('expires_in') === '90' &&
239
+ url.searchParams.get('contents') === 'write' &&
240
+ url.searchParams.get('metadata') === 'read' &&
241
+ !url.search.includes('+') &&
242
+ new URL(tokenUrlFor('BOX')).searchParams.get('workflows') === 'write', 'the new-token link carries the name, days and permissions GitHub documents (and nothing else)');
243
+ const answer = (status, body, headers = {}) => (u) => Promise.resolve({
244
+ status,
245
+ header: (n) => headers[n.toLowerCase()] ?? null,
246
+ text: u.includes('/user/repos')
247
+ ? JSON.stringify([{ full_name: 'josh/a' }, { full_name: 'josh/b' }])
248
+ : JSON.stringify(body),
249
+ });
250
+ const good = await verifyGithubToken(FINE, answer(200, { login: 'josh' }, { 'github-authentication-token-expiration': '2027-02-03 10:00:00 UTC' }));
251
+ t.check(good.kind === 'ok' &&
252
+ good.check.login === 'josh' &&
253
+ good.check.expiresAt === '2027-02-03T10:00:00.000Z' &&
254
+ good.check.repos.count === 2 &&
255
+ !JSON.stringify(good).includes(FINE), 'a token GitHub accepts: who, when it ends, how many repositories (never the token)');
256
+ t.equal((await verifyGithubToken(FINE, answer(401, { message: 'Bad credentials' }))).kind, 'refused', 'a 401 is a refusal');
257
+ const big = () => Promise.reject(new Error('The answer from api.github.com is too big.'));
258
+ const tooBig = await verifyGithubToken(FINE, big);
259
+ t.check(tooBig.kind === 'unreachable' && tooBig.reason.includes('too big'), 'an oversize answer is refused, the token is not blamed');
260
+ const refusedStore = new GithubToken(t.dir('github-refused'), SecretStore.memory());
261
+ await t.throws(() => saveGithubTokenFlow(fakeIO([FINE]).io, refusedStore, () => Promise.resolve({ kind: 'refused', status: 401 })), 'GitHub refused this token', 'a refused token is not saved');
262
+ t.check(refusedStore.state() === 'none', 'nothing was written');
263
+ const checkedStore = new GithubToken(t.dir('github-checked'), SecretStore.memory());
264
+ await saveGithubTokenFlow(fakeIO([FINE]).io, checkedStore, okVerdict);
265
+ t.check(checkedStore.check()?.login === 'josh' &&
266
+ checkedStore.check()?.repoCount === 2 &&
267
+ !fs.readFileSync(path.join(t.dir('github-checked'), 'github', 'token.json'), 'utf8').includes(FINE), 'what GitHub said is kept beside the sealed token');
268
+ const now = Date.parse('2027-02-01T00:00:00.000Z');
269
+ t.equal([
270
+ expiryOf('2027-02-03T10:00:00.000Z', now)?.tone,
271
+ expiryOf('2027-04-03T10:00:00.000Z', now)?.tone,
272
+ expiryOf('2027-01-01T10:00:00.000Z', now)?.tone,
273
+ expiryOf(null, now),
274
+ ], ['warn', 'ok', 'danger', null], 'expiry tones: warn under 14 days, danger when expired');
275
+ const soon = githubItem({ ...sample, token: 'saved', expiresAt: '2027-02-03T10:00:00.000Z', login: 'josh', repoCount: 2 }, now);
276
+ t.check(soon.badges?.[0].text === 'expires soon' &&
277
+ (soon.details ?? []).some((d) => d.startsWith('Expires 3 Feb 2027 (in ')), 'the card says "expires soon" and when');
278
+ const withGithub = accountsPage({ agent: 'claude', agents: [], services: [] }, { services: [] }, { ...sample, token: 'saved' });
279
+ t.check(withGithub.sections.some((sec) => sec.id === 'github'), 'the Accounts page has a GitHub section');
280
+ const expiring = new GithubToken(t.dir('github-expiring'), SecretStore.memory());
281
+ expiring.save(FINE, {
282
+ login: 'josh',
283
+ expiresAt: '2027-02-03T10:00:00.000Z',
284
+ repoCount: 1,
285
+ checkedAt: '2027-01-01T00:00:00.000Z',
286
+ });
287
+ const shareOn = f.makeConfig();
288
+ shareOn.github.shareToken = true;
289
+ t.check(prepareGithubToken(shareOn, shareOn.storage.root, expiring, [], Date.parse('2027-01-30T00:00:00.000Z')).notes.some((n) => n.includes('expires in 5 days')) &&
290
+ !prepareGithubToken(shareOn, shareOn.storage.root, expiring, [], Date.parse('2026-12-01T00:00:00.000Z')).notes.some((n) => n.includes('expires')), 'a launch mentions a token that ends within 7 days, and only then');
291
+ t.section('errors with an action');
292
+ const fixable = new UserError('Broke.', 'Do it.', { op: 'sandbox.stop', label: 'Stop it', cli: 'sandbox stop' });
293
+ const wire = toWireError(fixable);
294
+ t.check(wire.action?.op === 'sandbox.stop' &&
295
+ wire.action.label === 'Stop it' &&
296
+ !JSON.stringify(wire).includes('sandbox stop') &&
297
+ fromWireError(wire) instanceof UserError, 'the wire error carries the action without the command line');
298
+ const printed = [];
299
+ const saveErr = console.error;
300
+ console.error = (m) => void printed.push(String(m));
301
+ try {
302
+ report(fixable);
303
+ }
304
+ finally {
305
+ console.error = saveErr;
306
+ }
307
+ t.check(printed.some((l) => l.includes('Run:') && l.includes('sandbox stop')), 'the command line prints "Run: <command>"');
223
308
  t.section('gh in the sandbox: the token on stdin only');
224
309
  const base = {
225
310
  PATH: 'C:\\x',
@@ -32,6 +32,9 @@ export const guestBootSuite = {
32
32
  'would provision uv',
33
33
  'would provision Git for Windows from this PC',
34
34
  ];
35
+ const headers = [1, 2, 3, 4, 5, 6, 7, 8, 9].map((n) => boot.out.indexOf('[' + n + '/9] '));
36
+ t.check(headers.every((at, i) => at >= 0 && (i === 0 || at > headers[i - 1])), 'the boot prints its nine stages, [1/9] to [9/9], in order', headers.join(', '));
37
+ t.check(boot.out.includes('Boot ready in ') && boot.out.includes('] Developer tools: done in '), 'the boot says how long it took and how long each task took');
35
38
  t.equal(steps.filter((s) => !boot.out.includes(s)), [], 'the guest provisions every enabled toolchain');
36
39
  t.check(boot.out.includes('AgentSandbox-Block-Private') &&
37
40
  boot.out.includes('remoteport=1-52,54-65535') &&
@@ -490,7 +490,7 @@ async function sharedCore(t) {
490
490
  const logged = fake.seen.logs.map((l) => l.text).join('\n');
491
491
  t.includes(logged, ' [ok] Windows: p1 is fine', 'ioEvents writes a line per result, like the printer');
492
492
  t.includes(logged, 'What it means: it hurts', 'ioEvents says what a problem means');
493
- t.includes(logged, 'Fix: sandbox repair will mend the cracks', 'ioEvents says what repair would do');
493
+ t.includes(logged, 'Fix: repair will mend the cracks', 'ioEvents says what repair would do');
494
494
  t.includes(logged, 'Repair will change:', 'ioEvents lists the changes before they are made');
495
495
  t.includes(logged, '[fixed] Cracks: mended the cracks', 'ioEvents reports a fix on its own line');
496
496
  t.equal(featureSummaryText({ total: 9, ok: 6, fixed: 1, warn: 1, needsYou: 1, skipped: 0, fixable: 0 }), '6 ok, 1 fixed, 1 needs you, 1 warning', 'summaryText moved to the feature and reads the same');
@@ -20,6 +20,7 @@ import { unitySuite } from './unity.test.js';
20
20
  import { fivemSuite } from './fivem.test.js';
21
21
  import { androidSuite } from './android.test.js';
22
22
  import { mobileSuite } from './mobile.test.js';
23
+ import { agentsWebSuite } from './agents-web.test.js';
23
24
  import { credentialsSuite } from './credentials.test.js';
24
25
  import { vpnSuite } from './vpn.test.js';
25
26
  import { githubSuite } from './github.test.js';
@@ -75,7 +76,10 @@ import { workspaceViewSuite } from './workspace-view.test.js';
75
76
  import { liveNetworkSuite } from './live-network.test.js';
76
77
  import { liveTerminalSuite } from './live-terminal.test.js';
77
78
  import { appSuite } from './app.test.js';
79
+ import { uiTextSuite } from './ui-text.test.js';
80
+ import { menuCoverageSuite } from './menu-coverage.test.js';
78
81
  import { auditSuite } from './audit.test.js';
82
+ import { bootRunnerSuite } from './boot-runner.test.js';
79
83
  import { layoutSuite } from './layout.test.js';
80
84
  import { menuSuite } from './menu.test.js';
81
85
  import { menuPerfSuite } from './menu-perf.test.js';
@@ -128,6 +132,7 @@ export const SUITES = [
128
132
  testGuardSuite,
129
133
  // pure helpers and the command line
130
134
  layoutSuite,
135
+ bootRunnerSuite,
131
136
  toolchainsSuite,
132
137
  persistedInstallsSuite,
133
138
  steamSuite,
@@ -136,6 +141,7 @@ export const SUITES = [
136
141
  fivemSuite,
137
142
  androidSuite,
138
143
  mobileSuite,
144
+ agentsWebSuite,
139
145
  detectSuite,
140
146
  networkSuite,
141
147
  personalizationSuite,
@@ -147,6 +153,8 @@ export const SUITES = [
147
153
  repairSuite,
148
154
  healthSuite,
149
155
  appSuite,
156
+ uiTextSuite,
157
+ menuCoverageSuite,
150
158
  cliSuite,
151
159
  menuSuite,
152
160
  menuPerfSuite,
@@ -15,7 +15,12 @@ const PORTABLE_CORE = [
15
15
  const PURE_GUEST = [
16
16
  'guest/git/decision.js',
17
17
  'guest/setup/git-rules.js',
18
+ 'guest/boot/guide-lines.js',
18
19
  'guest/boot/project-env.js',
20
+ 'guest/boot/runner.js',
21
+ 'guest/boot/graph.js',
22
+ 'guest/toolchains/split.js',
23
+ 'guest/toolchains/step-scheduler.js',
19
24
  'guest/audit/parse.js',
20
25
  'guest/sessions/cmdline.js',
21
26
  'guest/agents/agent-files.js',
@@ -0,0 +1,2 @@
1
+ import type { Suite } from './support/suite.js';
2
+ export declare const menuCoverageSuite: Suite;
@@ -0,0 +1,46 @@
1
+ // "All features and config need to be in the cli menu": every command has a place in the menu (or a reason it cannot),
2
+ // every operation and query that skips the menu says why in cli/menu/coverage-exempt.ts, and every setting of the
3
+ // schema is a row of the menu's Settings page.
4
+ import path from 'node:path';
5
+ import { DEFINITIONS } from '../app/features/defs.js';
6
+ import { ConfigFile, settingsOf } from '../app/features/settings/index.js';
7
+ import { exemption } from '../app/kernel/index.js';
8
+ import { COMMANDS } from '../cli/manifest.js';
9
+ import { COMMAND_PLACES, MENU_EXEMPT, SETTING_PLACES } from '../cli/menu/coverage-exempt.js';
10
+ import { SECTION_LABELS } from '../cli/menu/shell.js';
11
+ import { isToolchainReady } from '../shared/readiness.js';
12
+ export const menuCoverageSuite = {
13
+ name: 'menu-coverage',
14
+ title: 'menu coverage: every command, operation and setting has a place in the menu, or a written reason',
15
+ run: (t) => {
16
+ t.section('commands');
17
+ const names = COMMANDS.map((c) => c.name);
18
+ t.equal(names.filter((n) => !(n in COMMAND_PLACES)), [], 'every command has a menu place or an exemption in cli/menu/coverage-exempt.ts');
19
+ t.equal(Object.keys(COMMAND_PLACES).filter((n) => !names.includes(n)), [], 'the list names no command that is gone');
20
+ const badPlaces = Object.entries(COMMAND_PLACES).flatMap(([name, p]) => 'place' in p
21
+ ? SECTION_LABELS.some((s) => p.place.startsWith(s))
22
+ ? []
23
+ : [name + ': ' + p.place + ' is not a menu section']
24
+ : p.exempt.trim().length >= 10
25
+ ? []
26
+ : [name + ': the reason is too short']);
27
+ t.equal(badPlaces, [], 'a place starts with a menu section, an exemption gives a reason');
28
+ t.section('operations and queries');
29
+ const exempt = DEFINITIONS.filter((d) => exemption(d, 'menu')?.trim()).map((d) => d.id);
30
+ t.equal(exempt.filter((id) => !(id in MENU_EXEMPT)), [], 'every definition that skips the menu is on the allow-list with its reason');
31
+ t.equal(Object.keys(MENU_EXEMPT).filter((id) => !exempt.includes(id)), [], 'the allow-list names no definition that is in the menu or gone');
32
+ t.section('settings');
33
+ const file = ConfigFile.user(path.join(t.dir('menu-coverage'), 'config.json'));
34
+ const rows = new Set(settingsOf(file).map((s) => s.key.join('.')));
35
+ const placed = (key) => rows.has(key) ||
36
+ [...rows].some((r) => key.startsWith(r + '.')) ||
37
+ Object.keys(SETTING_PLACES).some((p) => key === p || (p.endsWith('.') && key.startsWith(p)));
38
+ const missing = file.schema
39
+ .leaves()
40
+ .map((l) => l.key.join('.'))
41
+ // A toolchain that is not finished is hidden everywhere (shared/readiness.ts), the Settings page included.
42
+ .filter((key) => !(key.startsWith('toolchains.') && !isToolchainReady(key.split('.')[1])))
43
+ .filter((key) => !placed(key));
44
+ t.equal(missing, [], 'every setting of the config schema is a row of the Settings page (or on the allow-list)');
45
+ },
46
+ };
@@ -4,6 +4,7 @@ import fs from 'node:fs';
4
4
  import path from 'node:path';
5
5
  import { Draft } from '../cli/menu/settings/draft.js';
6
6
  import { ConfigFile } from '../app/features/settings/index.js';
7
+ import { SERVICE_SPECS } from '../shared/accounts/index.js';
7
8
  import { accountsOptions } from '../cli/menu/pages/accounts-page.js';
8
9
  import { appsOptions, stageTicks } from '../cli/menu/pages/apps-page.js';
9
10
  import { maintenanceOptions } from '../cli/menu/pages/maintenance-page.js';
@@ -34,7 +35,42 @@ const DATA_FOLDER_ROW = [
34
35
  ];
35
36
  const ACCOUNTS = {
36
37
  agent: 'claude',
37
- services: [],
38
+ services: [
39
+ {
40
+ kind: 'perplexity',
41
+ label: SERVICE_SPECS.perplexity.label,
42
+ share: true,
43
+ state: 'saved',
44
+ source: 'pc',
45
+ savedAt: '2026-10-14T10:00:00Z',
46
+ summary: 'pplx-...wxyz',
47
+ heldBy: null,
48
+ usedBy: ['perplexity'],
49
+ usedFor: 'the Perplexity MCP app',
50
+ warnings: [...SERVICE_SPECS.perplexity.warnings],
51
+ canImport: true,
52
+ canLogin: true,
53
+ needsFile: false,
54
+ ready: true,
55
+ },
56
+ {
57
+ kind: 'comet',
58
+ label: SERVICE_SPECS.comet.label,
59
+ share: false,
60
+ state: 'none',
61
+ source: null,
62
+ savedAt: null,
63
+ summary: null,
64
+ heldBy: null,
65
+ usedBy: ['comet'],
66
+ usedFor: 'the Comet app',
67
+ warnings: [...SERVICE_SPECS.comet.warnings],
68
+ canImport: false,
69
+ canLogin: false,
70
+ needsFile: false,
71
+ ready: true,
72
+ },
73
+ ],
38
74
  agents: [
39
75
  {
40
76
  name: 'claude',