windows-development-sandbox 1.17.3 → 1.18.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 (315) hide show
  1. package/README.md +1 -1
  2. package/config.example.json +97 -4
  3. package/dist/app/features/accounts/accounts.def.d.ts +116 -2
  4. package/dist/app/features/accounts/accounts.def.js +80 -12
  5. package/dist/app/features/accounts/accounts.run.d.ts +29 -0
  6. package/dist/app/features/accounts/accounts.run.js +36 -6
  7. package/dist/app/features/accounts/index.d.ts +2 -0
  8. package/dist/app/features/accounts/index.js +4 -1
  9. package/dist/app/features/accounts/services.def.d.ts +111 -0
  10. package/dist/app/features/accounts/services.def.js +187 -0
  11. package/dist/app/features/accounts/services.run.d.ts +65 -0
  12. package/dist/app/features/accounts/services.run.js +212 -0
  13. package/dist/app/features/defs.d.ts +112 -1
  14. package/dist/app/features/defs.js +5 -0
  15. package/dist/app/features/settings/categories.def.js +21 -4
  16. package/dist/app/registry.js +5 -1
  17. package/dist/bin/agent-sandbox.js +2 -2
  18. package/dist/bin/{chunk-QLGGAG3D.js → chunk-BKF43GJ3.js} +51 -16
  19. package/dist/bin/{chunk-REE3EMHD.js → chunk-YD3A67RS.js} +3812 -1702
  20. package/dist/bin/{chunk-XYUVIVJT.js → chunk-Z5D32FN3.js} +1 -1
  21. package/dist/bin/claude-host.js +2 -2
  22. package/dist/bin/claude-sbx.js +2 -2
  23. package/dist/bin/codex-sbx.js +2 -2
  24. package/dist/bin/gemini-sbx.js +2 -2
  25. package/dist/bin/sandbox.js +2 -2
  26. package/dist/bin/sbx-hide.js +2 -2
  27. package/dist/bin/sbx-show.js +2 -2
  28. package/dist/cli/commands/launch/launch.js +30 -1
  29. package/dist/cli/manifest.d.ts +93 -65
  30. package/dist/cli/manifest.js +23 -5
  31. package/dist/cli/menu/pages/accounts-page.js +31 -16
  32. package/dist/cli/services/accounts-service.d.ts +14 -4
  33. package/dist/cli/services/accounts-service.js +19 -6
  34. package/dist/cli/services/data-collect.js +26 -1
  35. package/dist/core/config/defaults.js +47 -4
  36. package/dist/core/detect/toolchains.d.ts +15 -1
  37. package/dist/core/detect/toolchains.js +45 -12
  38. package/dist/core/local-config/overrides.js +6 -0
  39. package/dist/core/schema/accounts.d.ts +27 -0
  40. package/dist/core/schema/accounts.js +32 -0
  41. package/dist/core/schema/config.d.ts +239 -0
  42. package/dist/core/schema/config.js +6 -0
  43. package/dist/core/schema/pc-installs.d.ts +7 -0
  44. package/dist/core/schema/pc-installs.js +17 -0
  45. package/dist/core/schema/toolchains.d.ts +61 -0
  46. package/dist/core/schema/toolchains.js +106 -0
  47. package/dist/core/types.d.ts +18 -0
  48. package/dist/guest/accounts/adapter.d.ts +25 -0
  49. package/dist/guest/accounts/adapter.js +21 -0
  50. package/dist/guest/accounts/adapters.d.ts +5 -0
  51. package/dist/guest/accounts/adapters.js +18 -0
  52. package/dist/guest/accounts/apply.d.ts +41 -0
  53. package/dist/guest/accounts/apply.js +47 -0
  54. package/dist/guest/accounts/kinds/appstore.d.ts +2 -0
  55. package/dist/guest/accounts/kinds/appstore.js +7 -0
  56. package/dist/guest/accounts/kinds/cfx.d.ts +2 -0
  57. package/dist/guest/accounts/kinds/cfx.js +7 -0
  58. package/dist/guest/accounts/kinds/epic.d.ts +2 -0
  59. package/dist/guest/accounts/kinds/epic.js +26 -0
  60. package/dist/guest/accounts/kinds/expo.d.ts +2 -0
  61. package/dist/guest/accounts/kinds/expo.js +7 -0
  62. package/dist/guest/accounts/kinds/keystore.d.ts +2 -0
  63. package/dist/guest/accounts/kinds/keystore.js +7 -0
  64. package/dist/guest/accounts/kinds/play.d.ts +2 -0
  65. package/dist/guest/accounts/kinds/play.js +7 -0
  66. package/dist/guest/accounts/kinds/steam.d.ts +4 -0
  67. package/dist/guest/accounts/kinds/steam.js +17 -0
  68. package/dist/guest/accounts/kinds/unity.d.ts +2 -0
  69. package/dist/guest/accounts/kinds/unity.js +7 -0
  70. package/dist/guest/accounts/watch.d.ts +28 -0
  71. package/dist/guest/accounts/watch.js +78 -0
  72. package/dist/guest/apps/resolvers-gamedev.d.ts +2 -0
  73. package/dist/guest/apps/resolvers-gamedev.js +1 -0
  74. package/dist/guest/apps/resolvers-mobile.d.ts +2 -0
  75. package/dist/guest/apps/resolvers-mobile.js +1 -0
  76. package/dist/guest/apps/resolvers.js +4 -0
  77. package/dist/guest/boot/boot.d.ts +5 -0
  78. package/dist/guest/boot/boot.js +35 -1
  79. package/dist/guest/daemon/accounts.d.ts +3 -0
  80. package/dist/guest/daemon/accounts.js +25 -0
  81. package/dist/guest/daemon/daemon.js +2 -0
  82. package/dist/guest/daemon/idle-shutdown.js +3 -1
  83. package/dist/guest/daemon/sessions.js +2 -0
  84. package/dist/guest/devtools/android.d.ts +2 -0
  85. package/dist/guest/devtools/android.js +7 -0
  86. package/dist/guest/devtools/devtool.d.ts +9 -0
  87. package/dist/guest/devtools/devtool.js +5 -0
  88. package/dist/guest/devtools/fivem.d.ts +2 -0
  89. package/dist/guest/devtools/fivem.js +7 -0
  90. package/dist/guest/devtools/registry.d.ts +6 -0
  91. package/dist/guest/devtools/registry.js +12 -0
  92. package/dist/guest/devtools/steam.d.ts +2 -0
  93. package/dist/guest/devtools/steam.js +228 -0
  94. package/dist/guest/devtools/unity.d.ts +2 -0
  95. package/dist/guest/devtools/unity.js +7 -0
  96. package/dist/guest/entries/daemon.js +2 -0
  97. package/dist/guest/entries/tool.d.ts +1 -0
  98. package/dist/guest/entries/tool.js +17 -0
  99. package/dist/guest/toolchains/engines/godot.d.ts +4 -0
  100. package/dist/guest/toolchains/engines/godot.js +4 -0
  101. package/dist/guest/toolchains/engines/unity-release.d.ts +15 -0
  102. package/dist/guest/toolchains/engines/unity-release.js +6 -0
  103. package/dist/guest/toolchains/engines/unity.d.ts +4 -0
  104. package/dist/guest/toolchains/engines/unity.js +4 -0
  105. package/dist/guest/toolchains/engines/unreal-engines.d.ts +4 -0
  106. package/dist/guest/toolchains/engines/unreal-engines.js +4 -0
  107. package/dist/guest/toolchains/fivem/artifacts.d.ts +6 -0
  108. package/dist/guest/toolchains/fivem/artifacts.js +7 -0
  109. package/dist/guest/toolchains/fivem/fivem.d.ts +4 -0
  110. package/dist/guest/toolchains/fivem/fivem.js +4 -0
  111. package/dist/guest/toolchains/installers.d.ts +12 -0
  112. package/dist/guest/toolchains/installers.js +39 -0
  113. package/dist/guest/toolchains/languages/java.js +1 -1
  114. package/dist/guest/toolchains/languages/kotlin.d.ts +3 -0
  115. package/dist/guest/toolchains/languages/kotlin.js +6 -0
  116. package/dist/guest/toolchains/mobile/android-repo.d.ts +11 -0
  117. package/dist/guest/toolchains/mobile/android-repo.js +10 -0
  118. package/dist/guest/toolchains/mobile/android.d.ts +17 -0
  119. package/dist/guest/toolchains/mobile/android.js +16 -0
  120. package/dist/guest/toolchains/mobile/dart.d.ts +4 -0
  121. package/dist/guest/toolchains/mobile/dart.js +4 -0
  122. package/dist/guest/toolchains/mobile/flutter.d.ts +4 -0
  123. package/dist/guest/toolchains/mobile/flutter.js +4 -0
  124. package/dist/guest/toolchains/mobile/react-native.d.ts +4 -0
  125. package/dist/guest/toolchains/mobile/react-native.js +4 -0
  126. package/dist/guest/toolchains/native/unreal.d.ts +3 -7
  127. package/dist/guest/toolchains/native/unreal.js +16 -167
  128. package/dist/guest/toolchains/native/visual-studio.d.ts +15 -0
  129. package/dist/guest/toolchains/native/visual-studio.js +159 -0
  130. package/dist/guest/toolchains/native/vs-request.d.ts +19 -0
  131. package/dist/guest/toolchains/native/vs-request.js +46 -0
  132. package/dist/guest/toolchains/not-yet.d.ts +2 -0
  133. package/dist/guest/toolchains/not-yet.js +6 -0
  134. package/dist/guest/toolchains/persisted.d.ts +48 -0
  135. package/dist/guest/toolchains/persisted.js +159 -0
  136. package/dist/guest/toolchains/plan.js +80 -4
  137. package/dist/guest/toolchains/steam/apps.d.ts +27 -0
  138. package/dist/guest/toolchains/steam/apps.js +112 -0
  139. package/dist/guest/toolchains/steam/client.d.ts +5 -0
  140. package/dist/guest/toolchains/steam/client.js +67 -0
  141. package/dist/guest/toolchains/steam/commands.d.ts +58 -0
  142. package/dist/guest/toolchains/steam/commands.js +155 -0
  143. package/dist/guest/toolchains/steam/steam.d.ts +4 -0
  144. package/dist/guest/toolchains/steam/steam.js +61 -0
  145. package/dist/guest/toolchains/steam/steamcmd-install.d.ts +29 -0
  146. package/dist/guest/toolchains/steam/steamcmd-install.js +177 -0
  147. package/dist/guest/toolchains/steam/steamcmd.d.ts +6 -0
  148. package/dist/guest/toolchains/steam/steamcmd.js +23 -0
  149. package/dist/guest/toolchains/step.d.ts +10 -0
  150. package/dist/host/accounts/deliver.d.ts +26 -0
  151. package/dist/host/accounts/deliver.js +80 -0
  152. package/dist/host/accounts/handoff.d.ts +17 -0
  153. package/dist/host/accounts/handoff.js +110 -0
  154. package/dist/host/accounts/index.d.ts +6 -0
  155. package/dist/host/accounts/index.js +8 -0
  156. package/dist/host/accounts/kinds/appstore.d.ts +2 -0
  157. package/dist/host/accounts/kinds/appstore.js +10 -0
  158. package/dist/host/accounts/kinds/cfx.d.ts +2 -0
  159. package/dist/host/accounts/kinds/cfx.js +7 -0
  160. package/dist/host/accounts/kinds/epic.d.ts +2 -0
  161. package/dist/host/accounts/kinds/epic.js +15 -0
  162. package/dist/host/accounts/kinds/expo.d.ts +2 -0
  163. package/dist/host/accounts/kinds/expo.js +7 -0
  164. package/dist/host/accounts/kinds/keystore.d.ts +2 -0
  165. package/dist/host/accounts/kinds/keystore.js +7 -0
  166. package/dist/host/accounts/kinds/play.d.ts +2 -0
  167. package/dist/host/accounts/kinds/play.js +7 -0
  168. package/dist/host/accounts/kinds/steam.d.ts +6 -0
  169. package/dist/host/accounts/kinds/steam.js +31 -0
  170. package/dist/host/accounts/kinds/unity.d.ts +2 -0
  171. package/dist/host/accounts/kinds/unity.js +13 -0
  172. package/dist/host/accounts/leases.d.ts +40 -0
  173. package/dist/host/accounts/leases.js +62 -0
  174. package/dist/host/accounts/source.d.ts +21 -0
  175. package/dist/host/accounts/source.js +15 -0
  176. package/dist/host/accounts/sources.d.ts +5 -0
  177. package/dist/host/accounts/sources.js +18 -0
  178. package/dist/host/accounts/store.d.ts +23 -0
  179. package/dist/host/accounts/store.js +74 -0
  180. package/dist/host/audit/log.d.ts +1 -1
  181. package/dist/host/launch/generate.d.ts +2 -0
  182. package/dist/host/launch/generate.js +16 -1
  183. package/dist/host/launch/hints/android.d.ts +3 -0
  184. package/dist/host/launch/hints/android.js +3 -0
  185. package/dist/host/launch/hints/engines.d.ts +3 -0
  186. package/dist/host/launch/hints/engines.js +3 -0
  187. package/dist/host/launch/hints/fivem.d.ts +3 -0
  188. package/dist/host/launch/hints/fivem.js +3 -0
  189. package/dist/host/launch/hints/hint.d.ts +2 -0
  190. package/dist/host/launch/hints/hint.js +1 -0
  191. package/dist/host/launch/hints/index.d.ts +5 -0
  192. package/dist/host/launch/hints/index.js +29 -0
  193. package/dist/host/launch/hints/mobile.d.ts +3 -0
  194. package/dist/host/launch/hints/mobile.js +3 -0
  195. package/dist/host/launch/hints/steam.d.ts +7 -0
  196. package/dist/host/launch/hints/steam.js +59 -0
  197. package/dist/host/launch/index.d.ts +2 -2
  198. package/dist/host/launch/index.js +2 -2
  199. package/dist/host/launch/mappings.js +2 -0
  200. package/dist/host/launch/session-file.js +2 -0
  201. package/dist/host/launch/stage.js +10 -1
  202. package/dist/host/launch/toolchains.d.ts +7 -0
  203. package/dist/host/launch/toolchains.js +11 -0
  204. package/dist/host/pc-installs/discover.d.ts +19 -0
  205. package/dist/host/pc-installs/discover.js +59 -0
  206. package/dist/host/pc-installs/epic.d.ts +2 -0
  207. package/dist/host/pc-installs/epic.js +7 -0
  208. package/dist/host/pc-installs/index.d.ts +3 -0
  209. package/dist/host/pc-installs/index.js +2 -0
  210. package/dist/host/pc-installs/source.d.ts +9 -0
  211. package/dist/host/pc-installs/source.js +1 -0
  212. package/dist/host/pc-installs/steam.d.ts +16 -0
  213. package/dist/host/pc-installs/steam.js +143 -0
  214. package/dist/host/pc-installs/unity.d.ts +2 -0
  215. package/dist/host/pc-installs/unity.js +7 -0
  216. package/dist/host/pc-installs/unreal.d.ts +2 -0
  217. package/dist/host/pc-installs/unreal.js +7 -0
  218. package/dist/host/profile/accounts.d.ts +21 -6
  219. package/dist/host/profile/accounts.js +108 -28
  220. package/dist/host/profile/claude-login.d.ts +32 -2
  221. package/dist/host/profile/claude-login.js +100 -8
  222. package/dist/host/profile/index.d.ts +1 -1
  223. package/dist/host/profile/index.js +1 -1
  224. package/dist/host/secrets/launch-secrets.js +1 -0
  225. package/dist/runtime/agent.js +321 -9
  226. package/dist/runtime/daemon.js +1987 -604
  227. package/dist/runtime/db.js +334 -11
  228. package/dist/runtime/main.js +2519 -723
  229. package/dist/runtime/netproxy.js +321 -9
  230. package/dist/runtime/tool.js +28549 -0
  231. package/dist/runtime/view.js +321 -9
  232. package/dist/selftest/accounts.test.js +172 -1
  233. package/dist/selftest/agent-paths.test.js +1 -0
  234. package/dist/selftest/android.test.d.ts +2 -0
  235. package/dist/selftest/android.test.js +7 -0
  236. package/dist/selftest/app.test.js +1 -1
  237. package/dist/selftest/engines.test.d.ts +2 -0
  238. package/dist/selftest/engines.test.js +7 -0
  239. package/dist/selftest/fivem.test.d.ts +2 -0
  240. package/dist/selftest/fivem.test.js +7 -0
  241. package/dist/selftest/generate.test.js +1 -1
  242. package/dist/selftest/index.js +18 -0
  243. package/dist/selftest/menu-screens.js +1 -0
  244. package/dist/selftest/menu.test.js +2 -1
  245. package/dist/selftest/mobile.test.d.ts +2 -0
  246. package/dist/selftest/mobile.test.js +7 -0
  247. package/dist/selftest/pc-installs.test.d.ts +2 -0
  248. package/dist/selftest/pc-installs.test.js +111 -0
  249. package/dist/selftest/persisted-installs.test.d.ts +2 -0
  250. package/dist/selftest/persisted-installs.test.js +107 -0
  251. package/dist/selftest/projects.test.js +6 -4
  252. package/dist/selftest/service-accounts.test.d.ts +2 -0
  253. package/dist/selftest/service-accounts.test.js +320 -0
  254. package/dist/selftest/steam.test.d.ts +2 -0
  255. package/dist/selftest/steam.test.js +316 -0
  256. package/dist/selftest/storage-helper.test.js +31 -8
  257. package/dist/selftest/support/engine-golden.js +3 -0
  258. package/dist/selftest/support/fixtures.d.ts +87 -0
  259. package/dist/selftest/support/fixtures.js +4 -3
  260. package/dist/selftest/support/pc-memory.d.ts +2 -0
  261. package/dist/selftest/support/pc-memory.js +17 -0
  262. package/dist/selftest/support/web-preview.js +20 -1
  263. package/dist/selftest/unity.test.d.ts +2 -0
  264. package/dist/selftest/unity.test.js +7 -0
  265. package/dist/shared/accounts/fingerprint.d.ts +1 -0
  266. package/dist/shared/accounts/fingerprint.js +20 -0
  267. package/dist/shared/accounts/index.d.ts +5 -0
  268. package/dist/shared/accounts/index.js +7 -0
  269. package/dist/shared/accounts/kinds/epic.d.ts +10 -0
  270. package/dist/shared/accounts/kinds/epic.js +56 -0
  271. package/dist/shared/accounts/kinds/steam.d.ts +28 -0
  272. package/dist/shared/accounts/kinds/steam.js +99 -0
  273. package/dist/shared/accounts/kinds.d.ts +41 -0
  274. package/dist/shared/accounts/kinds.js +154 -0
  275. package/dist/shared/accounts/payloads.d.ts +151 -0
  276. package/dist/shared/accounts/payloads.js +128 -0
  277. package/dist/shared/apps/catalogue-gamedev.d.ts +2 -0
  278. package/dist/shared/apps/catalogue-gamedev.js +1 -0
  279. package/dist/shared/apps/catalogue-mobile.d.ts +2 -0
  280. package/dist/shared/apps/catalogue-mobile.js +1 -0
  281. package/dist/shared/apps/catalogue-modding.d.ts +2 -0
  282. package/dist/shared/apps/catalogue-modding.js +1 -0
  283. package/dist/shared/apps/catalogue.d.ts +1 -0
  284. package/dist/shared/apps/catalogue.js +6 -1
  285. package/dist/shared/channel/schema.d.ts +183 -0
  286. package/dist/shared/channel/secrets.d.ts +42 -0
  287. package/dist/shared/channel/secrets.js +3 -0
  288. package/dist/shared/download/downloader.d.ts +7 -0
  289. package/dist/shared/download/downloader.js +10 -3
  290. package/dist/shared/installs.d.ts +19 -0
  291. package/dist/shared/installs.js +13 -0
  292. package/dist/shared/io/bounded.d.ts +6 -0
  293. package/dist/shared/io/bounded.js +38 -0
  294. package/dist/shared/io/index.d.ts +2 -0
  295. package/dist/shared/io/index.js +2 -0
  296. package/dist/shared/io/text.d.ts +9 -0
  297. package/dist/shared/io/text.js +18 -0
  298. package/dist/shared/paths/guest-paths.js +1 -1
  299. package/dist/shared/paths/index.d.ts +1 -0
  300. package/dist/shared/paths/index.js +1 -0
  301. package/dist/shared/paths/pc-installs.d.ts +20 -0
  302. package/dist/shared/paths/pc-installs.js +19 -0
  303. package/dist/shared/vdf.d.ts +25 -0
  304. package/dist/shared/vdf.js +151 -0
  305. package/dist/sync/sync-cycle.js +9 -0
  306. package/dist/web/index.html +1 -1
  307. package/dist/web/{main-JTH3FT4O.js → main-CWIF4VQI.js} +63 -61
  308. package/native/csharp/HcsHost/AssemblyInfo.cs +3 -3
  309. package/native/csharp/HostTools/AssemblyInfo.cs +3 -3
  310. package/native/csharp/StorageHelper/AssemblyInfo.cs +3 -3
  311. package/package.json +1 -1
  312. package/sandbox/scripts/add-firewall-port.ps1 +13 -0
  313. package/sandbox/templates/fivem/README.txt +2 -0
  314. package/schemas/config.schema.json +485 -0
  315. package/schemas/sandbox-config.schema.json +416 -0
@@ -0,0 +1,316 @@
1
+ // Steam: the VDF parser and its limits, the SteamCMD sign-in from a fake steamcmd folder (host) and back (sandbox),
2
+ // SteamCMD's command lines (apps, workshop, builds), the PC's libraries from a fixture libraryfolders.vdf, the SteamCMD
3
+ // install (a bad signer, the time limit, nothing private kept on the data disk), room checks with an injected statfs,
4
+ // and the build-script hint. Temp folders and fakes only: nothing is downloaded or run.
5
+ import fs from 'node:fs';
6
+ import path from 'node:path';
7
+ import { DEFAULTS } from '../core/config/index.js';
8
+ import { decideToolchains } from '../core/detect/index.js';
9
+ import { HOST_SOURCES } from '../host/accounts/index.js';
10
+ import { steamCmdFolderOf } from '../host/accounts/kinds/steam.js';
11
+ import { steamHints } from '../host/launch/hints/steam.js';
12
+ import { discoverPcInstalls } from '../host/pc-installs/index.js';
13
+ import { findSteamInstalls, steamInstalls, steamLibraries } from '../host/pc-installs/steam.js';
14
+ import { fingerprint, readSteamCmdLogin } from '../shared/accounts/index.js';
15
+ import { encodeText } from '../shared/io/index.js';
16
+ import { InstallRecordsSchema } from '../shared/installs.js';
17
+ import { PC_INSTALLS_GUEST } from '../shared/paths/index.js';
18
+ import { parseVdf, vdfGet, vdfText } from '../shared/vdf.js';
19
+ import { steamAdapterFor } from '../guest/accounts/kinds/steam.js';
20
+ import { assertAppRoom, installedApps, recordApp } from '../guest/toolchains/steam/apps.js';
21
+ import { appBuildArgs, appUpdateArgs, scriptKind, withBuildSettings, workshopGetArgs, workshopPushArgs, } from '../guest/toolchains/steam/commands.js';
22
+ import { fetchSteamCmd, prepareSteamCmd } from '../guest/toolchains/steam/steamcmd-install.js';
23
+ const GB = 1024 ** 3;
24
+ const PC = { localAppData: '', appData: '', programData: '' };
25
+ const roomy = { statfs: () => ({ bavail: 100 * GB, bsize: 1 }) };
26
+ const short = { statfs: () => ({ bavail: GB / 4, bsize: 1 }) };
27
+ /** A VDF string as Steam writes paths: backslashes doubled. */
28
+ const q = (p) => '"' + p.replace(/\\/g, '\\\\') + '"';
29
+ const CONFIG_VDF = `"InstallConfigStore"
30
+ {
31
+ "Software"
32
+ {
33
+ "Valve"
34
+ {
35
+ "Steam"
36
+ {
37
+ "Accounts"
38
+ {
39
+ "builder_bot"
40
+ {
41
+ "SteamID" "76561190000000000"
42
+ }
43
+ }
44
+ "ConnectCache"
45
+ {
46
+ "7a2c1b3d1" "0200000001000000aabbccdd"
47
+ }
48
+ }
49
+ }
50
+ }
51
+ }
52
+ `;
53
+ const APP_BUILD = `"AppBuild"
54
+ {
55
+ "AppID" "480" // Spacewar
56
+ "Desc" "nightly"
57
+ "ContentRoot" "..\\content\\"
58
+ "BuildOutput" "..\\output\\"
59
+ "Depots"
60
+ {
61
+ "481" "depot_build_481.vdf"
62
+ }
63
+ }
64
+ `;
65
+ export const steamSuite = {
66
+ name: 'steam',
67
+ title: 'Steam: the VDF parser, the SteamCMD sign-in, sbx-steam command lines, PC libraries, SteamCMD install',
68
+ run: async (t, f) => {
69
+ t.section('the VDF parser');
70
+ const parsed = parseVdf('\uFEFF// a comment\n"libraryfolders"\n{\n\t"0"\n\t{\n\t\t"path"\t\t"C:\\\\Program Files (x86)\\\\Steam"\n' +
71
+ '\t\t"Label" "say \\"hi\\""\n\t}\n\tunquoted value\n\t"cond" "1" [$WIN32]\n}\n');
72
+ t.equal(vdfText(parsed, 'libraryfolders', '0', 'path'), 'C:\\Program Files (x86)\\Steam', 'escaped paths come out');
73
+ t.equal(vdfText(parsed, 'LIBRARYFOLDERS', '0', 'label'), 'say "hi"', 'keys match in any case, quotes unescape');
74
+ t.check(vdfText(parsed, 'libraryfolders', 'unquoted') === 'value' && vdfText(parsed, 'libraryfolders', 'cond') === '1', 'comments, a byte-order mark, unquoted words and [conditions] are handled');
75
+ t.equal(vdfText(parseVdf(APP_BUILD, { escapes: false }), 'AppBuild', 'ContentRoot'), '..\\content\\', 'without escapes a hand-written "..\\content\\" stays as written');
76
+ t.equal(vdfText(parseVdf('"a" { "k" "first" "K" "second" }'), 'a', 'k'), 'first', 'a repeated key keeps the first value');
77
+ const proto = parseVdf('"__proto__" { "polluted" "yes" } "constructor" "x"');
78
+ t.check(proto !== null &&
79
+ {}.polluted === undefined &&
80
+ vdfGet(proto, '__proto__') !== undefined, 'a "__proto__" key is a plain key (nothing is polluted)');
81
+ const refused = [
82
+ ['"a" { "b" "c"', 'an unclosed block'],
83
+ ['"a" "b" }', 'a stray }'],
84
+ ['"a" { "b" "c', 'an unclosed quote'],
85
+ ['"a"', 'a key without a value'],
86
+ ['{ "a" "b" }', 'a block without a key'],
87
+ ['"x" [$WIN32', 'an unclosed condition'],
88
+ ['"a" "' + 'x'.repeat(2000) + '"', 'a text over maxBytes', { maxBytes: 1000 }],
89
+ ['"a" { "b" { "c" "d" } }', 'nesting over maxDepth', { maxDepth: 1 }],
90
+ ['"a" "1" "b" "2" "c" "3"', 'more keys than maxKeys', { maxKeys: 2 }],
91
+ ];
92
+ for (const [text, what, limits] of refused)
93
+ t.equal(parseVdf(text, limits), null, 'refused: ' + what);
94
+ const deep = '"a" {'.repeat(50_000);
95
+ const flat = '"r" {' + Array.from({ length: 50_000 }, (_, i) => '"k' + i + '" "v"').join(' ') + '}';
96
+ const started = Date.now();
97
+ const deepResult = parseVdf(deep);
98
+ const flatResult = parseVdf(flat);
99
+ t.check(deepResult === null && flatResult !== null && Date.now() - started < 5000, 'deep nesting is refused and 50000 keys parse quickly (no quadratic work)', Date.now() - started + ' ms');
100
+ t.equal(parseVdf('"a" "b"', { maxBytes: 3 }), null, 'the size limit holds before anything is parsed');
101
+ t.section('the SteamCMD sign-in: from a steamcmd folder on this PC');
102
+ const pcCmd = t.dir('pc-steamcmd');
103
+ t.write(path.join(pcCmd, 'steamcmd.exe'), 'exe');
104
+ t.write(path.join(pcCmd, 'config', 'config.vdf'), CONFIG_VDF);
105
+ for (let i = 1; i <= 5; i++) {
106
+ t.write(path.join(pcCmd, 'ssfn' + i), 'sentry' + i);
107
+ fs.utimesSync(path.join(pcCmd, 'ssfn' + i), new Date(), new Date(Date.now() - (6 - i) * 60_000));
108
+ }
109
+ t.write(path.join(pcCmd, 'ssfn99'), 'x'.repeat(60 * 1024));
110
+ t.write(path.join(pcCmd, 'ssfnabc'), 'not a sentry');
111
+ const source = HOST_SOURCES.steam;
112
+ const login = source.fromPc(PC, pcCmd);
113
+ t.check(login !== null &&
114
+ login.account === 'builder_bot' &&
115
+ login.configVdf === CONFIG_VDF &&
116
+ login.sentries.length === 4 &&
117
+ login.sentries.map((s) => s.name).join() === 'ssfn2,ssfn3,ssfn4,ssfn5' &&
118
+ Buffer.from(login.sentries[3].base64, 'base64').toString() === 'sentry5', 'config.vdf and the newest four sentry files are taken (an oversized or misnamed one is not)', JSON.stringify(login?.sentries.map((s) => s.name)));
119
+ t.equal(login ? source.summary(login) : null, 'builder_bot', 'its summary is the account name, never the token');
120
+ t.check(steamCmdFolderOf(path.join(pcCmd, 'steamcmd.exe')) === pcCmd &&
121
+ steamCmdFolderOf(path.join(pcCmd, 'config', 'config.vdf')) === pcCmd &&
122
+ fingerprint(source.fromPc(PC, path.join(pcCmd, 'config', 'config.vdf'))) === fingerprint(login), '--from may name the folder, its steamcmd.exe or its config.vdf');
123
+ const utf16 = t.dir('pc-steamcmd-utf16');
124
+ fs.mkdirSync(path.join(utf16, 'config'));
125
+ fs.writeFileSync(path.join(utf16, 'config', 'config.vdf'), encodeText(CONFIG_VDF, 'utf16le'));
126
+ t.equal(readSteamCmdLogin(utf16)?.account, 'builder_bot', 'a UTF-16 config.vdf is read too');
127
+ const signedOut = t.dir('pc-steamcmd-out');
128
+ t.write(path.join(signedOut, 'config', 'config.vdf'), CONFIG_VDF.replace(/"ConnectCache"[\s\S]*?\}/, ''));
129
+ t.check(source.fromPc(PC, signedOut) === null && source.fromPc(PC, t.at('nowhere')) === null, 'no cached login (or no folder): nothing is taken');
130
+ const big = t.dir('pc-steamcmd-big');
131
+ t.write(path.join(big, 'config', 'config.vdf'), CONFIG_VDF + ' '.repeat(300 * 1024));
132
+ t.equal(readSteamCmdLogin(big), null, 'a config.vdf over 256 KB is refused');
133
+ t.section('the SteamCMD sign-in: in the sandbox');
134
+ const guestCmd = t.dir('guest-steamcmd');
135
+ const adapter = steamAdapterFor(guestCmd);
136
+ const applied = await adapter.apply(login);
137
+ const back = adapter.current();
138
+ t.check(fs.readFileSync(path.join(guestCmd, 'config', 'config.vdf'), 'utf8') === CONFIG_VDF &&
139
+ fs.readFileSync(path.join(guestCmd, 'ssfn5'), 'utf8') === 'sentry5' &&
140
+ !applied.summary.includes('aabbccdd') &&
141
+ back !== null &&
142
+ fingerprint(back) === fingerprint(login), "applied into SteamCMD's folder and read back the same (the watch hands back nothing new)");
143
+ t.write(path.join(guestCmd, 'config', 'config.vdf'), CONFIG_VDF.replace('aabbccdd', 'eeff0011'));
144
+ t.check(fingerprint(adapter.current()) !== fingerprint(login), 'a renewed login token reads back different (it goes back to the PC)');
145
+ t.section('SteamCMD command lines');
146
+ const dir = 'C:\\AgentData\\tools\\steam-apps\\740';
147
+ t.equal(appUpdateArgs({ appId: '740', dir, login: 'anonymous', beta: 'public-beta', validate: true }), [
148
+ '+force_install_dir',
149
+ dir,
150
+ '+login',
151
+ 'anonymous',
152
+ '+app_update',
153
+ '740',
154
+ '-beta',
155
+ 'public-beta',
156
+ 'validate',
157
+ '+quit',
158
+ ], 'app: the folder comes before the login, then the app, its branch and validate');
159
+ t.equal(workshopGetArgs({
160
+ appId: '4000',
161
+ items: ['123', '456'],
162
+ dir: 'C:\\AgentData\\cache\\steam-workshop',
163
+ login: 'me',
164
+ }), [
165
+ '+force_install_dir',
166
+ 'C:\\AgentData\\cache\\steam-workshop',
167
+ '+login',
168
+ 'me',
169
+ '+workshop_download_item',
170
+ '4000',
171
+ '123',
172
+ '+workshop_download_item',
173
+ '4000',
174
+ '456',
175
+ '+quit',
176
+ ], 'workshop get: one download command per item');
177
+ t.equal(workshopPushArgs('me', 'C:\\Workspace\\game\\item.vdf'), ['+login', 'me', '+workshop_build_item', 'C:\\Workspace\\game\\item.vdf', '+quit'], 'workshop push: workshop_build_item');
178
+ t.equal(appBuildArgs('me', 'C:\\Workspace\\game\\app_build_480.vdf'), ['+login', 'me', '+run_app_build', 'C:\\Workspace\\game\\app_build_480.vdf', '+quit'], 'build: run_app_build');
179
+ const bad = [
180
+ [() => appUpdateArgs({ appId: '480 +quit', dir, login: 'me' }), 'an app ID with a command in it'],
181
+ [() => appUpdateArgs({ appId: '0', dir, login: 'me' }), 'app ID 0'],
182
+ [() => appUpdateArgs({ appId: '480', dir, login: 'me +runscript x' }), 'an account with a command in it'],
183
+ [() => appUpdateArgs({ appId: '480', dir, login: 'me', beta: 'a b' }), 'a branch with a space'],
184
+ [() => appUpdateArgs({ appId: '480', dir: 'relative\\dir', login: 'me' }), 'a relative folder'],
185
+ [() => appUpdateArgs({ appId: '480', dir: 'C:\\a "b"', login: 'me' }), 'a folder with a quote'],
186
+ [() => appUpdateArgs({ appId: '480', dir: 'C:\\x +quit', login: 'me' }), 'a folder with " +"'],
187
+ [() => workshopGetArgs({ appId: '480', items: [], dir, login: 'me' }), 'no workshop items'],
188
+ [() => workshopGetArgs({ appId: '480', items: ['12a'], dir, login: 'me' }), 'a bad item ID'],
189
+ [() => workshopPushArgs('anonymous', 'C:\\item.vdf'), 'uploading anonymously'],
190
+ [() => appBuildArgs('me', 'app_build.vdf'), 'a relative script'],
191
+ ];
192
+ for (const [fn, what] of bad)
193
+ await t.throws(fn, /not a|cannot be given|no workshop|needs your Steam/, 'refused: ' + what);
194
+ t.check(scriptKind(APP_BUILD) === 'appbuild' &&
195
+ scriptKind('"workshopitem" { "appid" "480" "publishedfileid" "0" }') === 'workshopitem' &&
196
+ scriptKind('"libraryfolders" { }') === null, 'a script is told by its first block');
197
+ const preview = withBuildSettings(APP_BUILD, { preview: true, desc: 'from the sandbox' });
198
+ const previewRoot = preview ? parseVdf(preview, { escapes: false }) : null;
199
+ t.check(vdfText(previewRoot, 'AppBuild', 'Preview') === '1' &&
200
+ vdfText(previewRoot, 'AppBuild', 'Desc') === 'from the sandbox' &&
201
+ vdfText(previewRoot, 'AppBuild', 'ContentRoot') === '..\\content\\', '--preview and --desc set Preview and Desc in a copy of the script, the rest unchanged');
202
+ t.check(vdfText(parseVdf(withBuildSettings(preview, { preview: true }), { escapes: false }), 'AppBuild', 'Preview') ===
203
+ '1' && withBuildSettings('"workshopitem" { }', { preview: true }) === null, 'an existing Preview is replaced; a workshop script is not a build');
204
+ await t.throws(() => withBuildSettings(APP_BUILD, { desc: 'a "quoted" one' }), 'one line', 'a description cannot break the script');
205
+ t.section("this PC's Steam libraries");
206
+ const steamRoot = t.dir('pc-steam');
207
+ const lib2 = t.dir('pc-steam-lib2');
208
+ t.write(path.join(steamRoot, 'steamapps', 'libraryfolders.vdf'), `"libraryfolders"\n{\n\t"0"\n\t{\n\t\t"path"\t\t${q(steamRoot)}\n\t\t"apps"\n\t\t{\n\t\t\t"228980"\t\t"1"\n\t\t}\n\t}\n` +
209
+ `\t"1"\n\t{\n\t\t"path"\t\t${q(lib2)}\n\t\t"apps"\n\t\t{\n\t\t\t"480"\t\t"1"\n\t\t}\n\t}\n}\n`);
210
+ t.write(path.join(lib2, 'steamapps', 'appmanifest_480.acf'), '"AppState"\n{\n\t"appid"\t\t"480"\n\t"name"\t\t"Spacewar"\n\t"installdir"\t\t"Spacewar"\n\t"buildid"\t\t"123"\n}\n');
211
+ t.dir('pc-steam-lib2', 'steamapps', 'common', 'Spacewar');
212
+ t.write(path.join(lib2, 'steamapps', 'appmanifest_10.acf'), '"AppState" { "appid" "10" "name" "Climber" "installdir" "..\\\\..\\\\Windows" }');
213
+ t.equal(steamLibraries(fs.readFileSync(path.join(steamRoot, 'steamapps', 'libraryfolders.vdf'), 'utf8')).map((l) => [
214
+ l.path,
215
+ l.apps,
216
+ ]), [
217
+ [steamRoot, ['228980']],
218
+ [lib2, ['480']],
219
+ ], 'libraryfolders.vdf: each library and the apps it lists');
220
+ t.equal(steamLibraries('"LibraryFolders" { "TimeNextStatsReport" "1" "ContentStatsID" "2" "1" "D:\\\\Games" }').map((l) => l.path), ['D:\\Games'], 'the old libraryfolders.vdf format too');
221
+ const libraries = findSteamInstalls(steamRoot, true);
222
+ t.check(libraries.map((l) => [l.kind, l.id, l.host].join()).join('|') ===
223
+ [
224
+ 'steam-library,library1,' + path.join(steamRoot, 'steamapps'),
225
+ 'steam-library,library2,' + path.join(lib2, 'steamapps'),
226
+ ].join('|') && libraries.every((l) => !l.name.includes(steamRoot) && !l.name.includes(lib2)), "true: every library's steamapps, named without a path of this PC");
227
+ const games = findSteamInstalls(steamRoot, [480, '10', 'abc', 99]);
228
+ t.equal(games.map((g) => [g.id, g.name, g.version, g.host]), [['480', 'Spacewar', '123', path.join(lib2, 'steamapps', 'common', 'Spacewar')]], 'a list: only those games, from their manifests (an installdir that climbs out, or an unknown ID, is left out)');
229
+ const fixture = { ...steamInstalls, find: (wanted) => findSteamInstalls(steamRoot, wanted) };
230
+ const found = discoverPcInstalls({ ...DEFAULTS.pcInstalls, steamGames: [480] }, { packageRoot: t.packageRoot, storageRoot: f.baseConfig.storage.root, sources: [fixture] }, PC);
231
+ t.check(found.mappings.length === 1 &&
232
+ found.mappings[0].guest === PC_INSTALLS_GUEST + '\\steam\\480' &&
233
+ found.mappings[0].readOnly &&
234
+ !JSON.stringify(found.manifest).includes(lib2), 'mapped read-only at C:\\PcInstalls\\steam\\480; the manifest holds no path of this PC');
235
+ t.section('installing SteamCMD');
236
+ const tools = t.dir('steam-tools');
237
+ const store = path.join(tools, 'steamcmd');
238
+ const work = t.dir('steam-work');
239
+ let downloads = 0;
240
+ const fake = (signed, extra = {}) => ({
241
+ ...roomy,
242
+ toolsDir: tools,
243
+ storeDir: store,
244
+ workDir: work,
245
+ signed,
246
+ download: async (_url, file, verify) => {
247
+ downloads++;
248
+ fs.writeFileSync(file + '.part', 'zip');
249
+ try {
250
+ await verify(file + '.part');
251
+ fs.renameSync(file + '.part', file);
252
+ }
253
+ finally {
254
+ fs.rmSync(file + '.part', { force: true });
255
+ }
256
+ },
257
+ extract: async (_archive, destination) => {
258
+ fs.mkdirSync(destination, { recursive: true });
259
+ fs.writeFileSync(path.join(destination, 'steamcmd.exe'), 'steamcmd');
260
+ },
261
+ run: async (_exe, args, cwd) => {
262
+ if (args.join() === '+quit')
263
+ fs.writeFileSync(path.join(cwd, 'steamclient64.dll'), 'updated');
264
+ return { code: 7, timedOut: false };
265
+ },
266
+ ...extra,
267
+ });
268
+ await t.throws(() => fetchSteamCmd(store, fake(() => false)), 'not signed by Valve', 'a steamcmd.exe not signed by Valve is refused');
269
+ t.check(!fs.existsSync(store) && !fs.readdirSync(tools).some((n) => n.includes('staging')), 'and nothing of it is kept');
270
+ downloads = 0;
271
+ await t.throws(() => fetchSteamCmd(store, fake(() => true, short)), 'it needs', 'too little room on the data disk: refused before downloading');
272
+ t.equal(downloads, 0, 'nothing was downloaded');
273
+ // The boot applied the sign-in into the working folder before this step (accounts come first).
274
+ t.write(path.join(work, 'config', 'config.vdf'), CONFIG_VDF);
275
+ t.write(path.join(work, 'ssfn5'), 'sentry5');
276
+ const exe = await prepareSteamCmd(fake(() => true));
277
+ t.check(exe === path.join(work, 'steamcmd.exe') &&
278
+ fs.readFileSync(path.join(work, 'config', 'config.vdf'), 'utf8') === CONFIG_VDF &&
279
+ fs.existsSync(path.join(store, 'steamclient64.dll')), 'downloaded once, copied in without touching the sign-in, updated, and the updated binaries kept');
280
+ t.check(!fs.existsSync(path.join(store, 'config')) && !fs.existsSync(path.join(store, 'ssfn5')), 'the kept copy on the data disk never holds config\\ or a sentry file');
281
+ const fresh = t.dir('steam-work-2');
282
+ t.write(path.join(store, 'config', 'config.vdf'), 'planted');
283
+ downloads = 0;
284
+ await prepareSteamCmd(fake(() => true, { workDir: fresh }));
285
+ t.check(downloads === 0 && !fs.existsSync(path.join(store, 'config')) && !fs.existsSync(path.join(fresh, 'config')), 'the next sandbox copies the kept binaries (no download); a config\\ planted there is removed, not copied');
286
+ await t.throws(() => prepareSteamCmd(fake(() => true, { workDir: t.dir('steam-work-3'), run: async () => ({ code: -1, timedOut: true }) })), 'did not finish updating itself', 'its first-run update has a time limit');
287
+ await t.throws(() => prepareSteamCmd(fake((file) => !file.startsWith(t.at('steam-work-4')), { workDir: t.dir('steam-work-4') })), 'not signed by Valve', 'a steamcmd.exe that is not Valve-signed is never run');
288
+ t.check(!fs.existsSync(store), 'and the kept copy goes (downloaded again next boot)');
289
+ t.section('apps and room');
290
+ await t.throws(() => assertAppRoom(path.join(tools, 'steam-apps', '740'), 5 * GB, 'app 740', short), 'it needs 5.0 GB', 'sbx-steam app refuses when the disk is short');
291
+ t.write(path.join(tools, 'steam-apps', '740', 'steamapps', 'appmanifest_740.acf'), '"AppState" { "appid" "740" "name" "Counter-Strike Global Offensive - Dedicated Server" "installdir" "csgo_ds" }');
292
+ recordApp('740', 123_456, { toolsDir: tools });
293
+ const apps = installedApps({ toolsDir: tools });
294
+ t.check(apps.length === 1 &&
295
+ apps[0].name.startsWith('Counter-Strike') &&
296
+ apps[0].sizeBytes === 123_456 &&
297
+ InstallRecordsSchema.safeParse(t.readJson(path.join(tools, 'installs.json'))).success, 'its size is recorded in tools\\installs.json (sandbox data shows it) and sbx-steam games lists it');
298
+ t.section('the build-script hint');
299
+ const project = t.dir('steam-project');
300
+ t.write(path.join(project, 'tools', 'ContentBuilder', 'scripts', 'app_build_480.vdf'), APP_BUILD);
301
+ t.write(path.join(project, 'node_modules', 'x', 'app_build.vdf'), APP_BUILD);
302
+ const tc = structuredClone(DEFAULTS.toolchains);
303
+ const hints = steamHints(project, tc);
304
+ t.check((hints.steam ?? '').includes('tools/ContentBuilder/scripts/app_build_480.vdf') &&
305
+ decideToolchains(tc, [], [], hints).steam.on, 'an app_build .vdf a few folders down turns Steam on');
306
+ const other = t.dir('steam-project-2');
307
+ t.write(path.join(other, 'node_modules', 'x', 'app_build.vdf'), APP_BUILD);
308
+ t.write(path.join(other, 'big.vdf'), '"workshopitem" { "title" "' + 'x'.repeat(70 * 1024) + '" }');
309
+ t.write(path.join(other, 'libraryfolders.vdf'), '"libraryfolders" { }');
310
+ t.write(path.join(other, 'item.txt'), '"workshopitem" { }');
311
+ t.equal(steamHints(other, tc), {}, 'skipped folders, oversized files and other .vdf files give no hint');
312
+ const off = structuredClone(tc);
313
+ off.steam.enabled = false;
314
+ t.equal(steamHints(project, off), {}, 'nothing is read when Steam is not "auto"');
315
+ },
316
+ };
@@ -59,6 +59,16 @@ function buildHelper(t) {
59
59
  function own(dir, sid) {
60
60
  spawnSync(SYSTEM.icacls(), [dir, '/setowner', '*' + sid, '/T', '/C', '/Q'], { windowsHide: true });
61
61
  }
62
+ /**
63
+ * A new folder whose permissions are its own (not inherited from %TEMP%, which other tools may have opened up): only
64
+ * `sid`, SYSTEM and Administrators may change it and what is made below it. Null when icacls could not set that.
65
+ */
66
+ function privateFolder(dir, sid) {
67
+ fs.mkdirSync(dir, { recursive: true });
68
+ const full = '(OI)(CI)F';
69
+ const r = spawnSync(SYSTEM.icacls(), [dir, '/inheritance:r', '/grant:r', '*' + sid + ':' + full, '*S-1-5-18:' + full, '*S-1-5-32-544:' + full, '/Q'], { windowsHide: true });
70
+ return r.status === 0 ? dir : null;
71
+ }
62
72
  function units(t) {
63
73
  t.section('the request the CLI sends, and what it makes of the answer');
64
74
  t.check(HelperRequestSchema.safeParse({ verb: 'mount', set: 'main' }).success, 'a request is a verb and a set');
@@ -164,7 +174,17 @@ function checkRun(exe, verb, root, set, sid, maxGb) {
164
174
  }
165
175
  function checks(t, exe, sid) {
166
176
  t.section('the checks (dry run): what the helper accepts and refuses');
167
- const root = t.dir('storage-helper', 'store');
177
+ // Every folder here starts private, so what is refused below is what the test opened up, never %TEMP%'s own rights.
178
+ const base = privateFolder(t.at('storage-helper', 'checks'), sid);
179
+ t.check(base !== null, 'the test folder is made private (only you, SYSTEM and Administrators may change it)');
180
+ if (!base)
181
+ return;
182
+ const dir = (name) => {
183
+ const p = path.join(base, name);
184
+ fs.mkdirSync(p, { recursive: true });
185
+ return p;
186
+ };
187
+ const root = dir('store');
168
188
  const paths = containedPaths(root, 'main');
169
189
  for (const dir of [paths.imagesDir, paths.mountsDir, paths.mount])
170
190
  fs.mkdirSync(dir, { recursive: true });
@@ -221,8 +241,8 @@ function checks(t, exe, sid) {
221
241
  }
222
242
  else
223
243
  t.skip('a folder others may change (icacls failed)');
224
- const linkTarget = t.dir('storage-helper', 'linktarget');
225
- const junctionRoot = t.dir('storage-helper', 'junction');
244
+ const linkTarget = dir('linktarget');
245
+ const junctionRoot = dir('junction');
226
246
  const junction = containedPaths(junctionRoot, 'main');
227
247
  fs.mkdirSync(junction.mount, { recursive: true });
228
248
  const mk = spawnSync(SYSTEM.cmd(), ['/c', 'mklink', '/J', junction.imagesDir, linkTarget], { windowsHide: true });
@@ -234,8 +254,8 @@ function checks(t, exe, sid) {
234
254
  }
235
255
  else
236
256
  t.skip('images folder as a junction');
237
- const above = t.dir('storage-helper', 'above');
238
- const realParent = t.dir('storage-helper', 'above-real');
257
+ const above = dir('above');
258
+ const realParent = dir('above-real');
239
259
  const inner = containedPaths(path.join(realParent, 'store'), 'main');
240
260
  for (const dir of [inner.imagesDir, inner.mount])
241
261
  fs.mkdirSync(dir, { recursive: true });
@@ -248,7 +268,7 @@ function checks(t, exe, sid) {
248
268
  }
249
269
  else
250
270
  t.skip('a junction above the storage folder');
251
- const mountRoot = t.dir('storage-helper', 'mountlink');
271
+ const mountRoot = dir('mountlink');
252
272
  const mountLinked = containedPaths(mountRoot, 'main');
253
273
  fs.mkdirSync(mountLinked.imagesDir, { recursive: true });
254
274
  fs.mkdirSync(mountLinked.mountsDir, { recursive: true });
@@ -261,7 +281,7 @@ function checks(t, exe, sid) {
261
281
  }
262
282
  else
263
283
  t.skip('mount folder as a junction');
264
- const missing = t.dir('storage-helper', 'missing');
284
+ const missing = dir('missing');
265
285
  own(missing, sid);
266
286
  refused('a storage folder without images and mounts', run({ root: missing }), 'missing');
267
287
  }
@@ -347,7 +367,10 @@ async function startConsole(exe, args) {
347
367
  }
348
368
  async function server(t, exe, sid) {
349
369
  t.section('the server in its console mode (this user, a pipe of its own, dry run)');
350
- const root = t.dir('storage-helper', 'served');
370
+ const root = privateFolder(t.at('storage-helper', 'served'), sid);
371
+ t.check(root !== null, 'the served storage folder is made private');
372
+ if (!root)
373
+ return;
351
374
  const paths = containedPaths(root, 'main');
352
375
  for (const dir of [paths.imagesDir, paths.mount])
353
376
  fs.mkdirSync(dir, { recursive: true });
@@ -29,6 +29,7 @@ export const LAUNCH_FILES_GOLDEN = [
29
29
  'runtime/gitguard.js',
30
30
  'runtime/main.js',
31
31
  'runtime/netproxy.js',
32
+ 'runtime/tool.js',
32
33
  'runtime/view.js',
33
34
  // The agent add-ons' lock files (pinned installs), copied with the rest of sandbox\.
34
35
  'sandbox/addons/ccusage/package-lock.json',
@@ -62,6 +63,7 @@ export const LAUNCH_FILES_GOLDEN = [
62
63
  'sandbox/csharp/SandboxTools/Program.cs',
63
64
  'sandbox/csharp/VsWhere.cs',
64
65
  'sandbox/csharp/VtInput.cs',
66
+ 'sandbox/scripts/add-firewall-port.ps1',
65
67
  'sandbox/scripts/enable-vt-input.ps1',
66
68
  'sandbox/scripts/expand-archive.ps1',
67
69
  'sandbox/scripts/get-gpu-names.ps1',
@@ -73,5 +75,6 @@ export const LAUNCH_FILES_GOLDEN = [
73
75
  'sandbox/scripts/set-desktop.ps1',
74
76
  'sandbox/scripts/set-dns-servers.ps1',
75
77
  'sandbox/scripts/set-machine-environment.ps1',
78
+ 'sandbox/templates/fivem/README.txt',
76
79
  'session.json',
77
80
  ];
@@ -107,6 +107,38 @@ export declare function createFixtures(t: Harness): {
107
107
  github: {
108
108
  shareToken: boolean;
109
109
  };
110
+ accounts: {
111
+ epic: {
112
+ share: boolean;
113
+ };
114
+ steam: {
115
+ share: boolean;
116
+ };
117
+ unity: {
118
+ share: boolean;
119
+ };
120
+ cfx: {
121
+ share: boolean;
122
+ };
123
+ keystore: {
124
+ share: boolean;
125
+ };
126
+ play: {
127
+ share: boolean;
128
+ };
129
+ expo: {
130
+ share: boolean;
131
+ };
132
+ appstore: {
133
+ share: boolean;
134
+ };
135
+ };
136
+ pcInstalls: {
137
+ steamGames: boolean | number[];
138
+ epicGames: boolean | string[];
139
+ unrealEngines: boolean | string[];
140
+ unityEditors: boolean | string[];
141
+ };
110
142
  provisioning: {
111
143
  copyFromHost: boolean;
112
144
  fallbackToInstall: boolean;
@@ -162,6 +194,59 @@ export declare function createFixtures(t: Harness): {
162
194
  acceptLicense: boolean;
163
195
  components: string[];
164
196
  keepInstallFiles: boolean;
197
+ persistEngines: boolean;
198
+ derivedDataCache: "persisted" | "local";
199
+ };
200
+ unity: {
201
+ enabled: boolean | "auto";
202
+ acceptLicense: boolean;
203
+ version: string;
204
+ modules: string[];
205
+ hub: boolean;
206
+ keepVersions: number;
207
+ };
208
+ godot: {
209
+ enabled: boolean | "auto";
210
+ version: string;
211
+ dotnet: boolean | "auto";
212
+ exportTemplates: boolean;
213
+ keepVersions: number;
214
+ };
215
+ steam: {
216
+ enabled: boolean | "auto";
217
+ client: boolean;
218
+ sdkFolder: string;
219
+ };
220
+ fivem: {
221
+ enabled: boolean | "auto";
222
+ acceptLicense: boolean;
223
+ game: "gta5" | "rdr3";
224
+ artifact: string;
225
+ txAdmin: boolean;
226
+ keepVersions: number;
227
+ };
228
+ android: {
229
+ enabled: boolean | "auto";
230
+ acceptLicense: boolean;
231
+ platforms: string[];
232
+ buildTools: string;
233
+ ndk: string | false;
234
+ cmake: string | false;
235
+ packages: string[];
236
+ };
237
+ flutter: {
238
+ enabled: boolean | "auto";
239
+ version: string;
240
+ windowsDesktop: boolean;
241
+ precache: string[];
242
+ };
243
+ dart: {
244
+ enabled: boolean | "auto";
245
+ version: string;
246
+ };
247
+ reactNative: {
248
+ enabled: boolean | "auto";
249
+ easCli: boolean;
165
250
  };
166
251
  cpp: {
167
252
  enabled: boolean | "auto";
@@ -174,6 +259,7 @@ export declare function createFixtures(t: Harness): {
174
259
  workloads: string[];
175
260
  frameworkTargets: string[];
176
261
  netfx35: boolean;
262
+ templates: string[];
177
263
  };
178
264
  go: {
179
265
  enabled: boolean | "auto";
@@ -185,6 +271,7 @@ export declare function createFixtures(t: Harness): {
185
271
  version: string;
186
272
  maven: boolean;
187
273
  gradle: boolean;
274
+ kotlin: boolean;
188
275
  };
189
276
  ruby: {
190
277
  enabled: boolean | "auto";
@@ -5,6 +5,7 @@ import { spawn } from 'node:child_process';
5
5
  import { DEFAULTS } from '../../core/config/index.js';
6
6
  import { generateSandbox } from '../../host/launch/index.js';
7
7
  import { resolveRun } from '../../core/run/index.js';
8
+ import { onFixedPc } from './pc-memory.js';
8
9
  export const HOST_NODE_VERSION = 'v22.3.0';
9
10
  /** Colour codes are removed from captured output, so tests can match plain text. */
10
11
  export function stripAnsi(text) {
@@ -110,14 +111,14 @@ export function createFixtures(t) {
110
111
  ...over,
111
112
  }, HOST_NODE_VERSION);
112
113
  const generate = (r, args = [], p = probe) =>
113
- // A fixed "this PC has" list, so "auto" toolchains decide the same on every machine the tests run on.
114
- generateSandbox({
114
+ // A fixed "this PC has" list and RAM, so "auto" toolchains and the memory decide the same on every machine.
115
+ onFixedPc(() => generateSandbox({
115
116
  packageRoot: t.packageRoot,
116
117
  run: r,
117
118
  harnessArgs: args,
118
119
  probe: p,
119
120
  pcLanguages: ['python', 'rust', 'cpp', 'dotnet'],
120
- });
121
+ }));
121
122
  /** Resolves a run and generates its sandbox (.wsb + launch folder + session.json) in one step. */
122
123
  const launch = async (over = {}, options = {}) => generate(await run(over, options.config ?? baseConfig), options.args, options.probe);
123
124
  /** Writes a project's `.sandbox-config.json` (objects are serialised; strings are written as they are). */
@@ -0,0 +1,2 @@
1
+ /** Runs `fn` (synchronous) as on a PC with this much RAM in total and free; the real os functions come back after. */
2
+ export declare function onFixedPc<T>(fn: () => T, totalMb?: number, freeMb?: number): T;
@@ -0,0 +1,17 @@
1
+ // A pretend PC's RAM for code that sizes the sandbox from os.totalmem() / os.freemem() (generateSandbox caps a fixed
2
+ // memoryMb at what the PC can keep resident), so launch tests decide the same on every machine they run on.
3
+ import os from 'node:os';
4
+ const MB = 1048576;
5
+ /** Runs `fn` (synchronous) as on a PC with this much RAM in total and free; the real os functions come back after. */
6
+ export function onFixedPc(fn, totalMb = 64 * 1024, freeMb = 48 * 1024) {
7
+ const real = { totalmem: os.totalmem, freemem: os.freemem };
8
+ os.totalmem = () => totalMb * MB;
9
+ os.freemem = () => freeMb * MB;
10
+ try {
11
+ return fn();
12
+ }
13
+ finally {
14
+ os.totalmem = real.totalmem;
15
+ os.freemem = real.freemem;
16
+ }
17
+ }
@@ -157,8 +157,27 @@ function sampleAccounts() {
157
157
  isDefault: true,
158
158
  isActive: true,
159
159
  savedAt: iso(3 * 24 * 60 * MINUTE),
160
+ kind: 'subscription',
161
+ keyHint: null,
162
+ },
163
+ {
164
+ name: 'personal',
165
+ email: null,
166
+ isDefault: false,
167
+ isActive: false,
168
+ savedAt: iso(9 * 24 * 60 * MINUTE),
169
+ kind: 'subscription',
170
+ keyHint: null,
171
+ },
172
+ {
173
+ name: 'free-credits',
174
+ email: null,
175
+ isDefault: false,
176
+ isActive: false,
177
+ savedAt: iso(1 * 24 * 60 * MINUTE),
178
+ kind: 'api-key',
179
+ keyHint: 'sk-ant-...x7Qa',
160
180
  },
161
- { name: 'personal', email: null, isDefault: false, isActive: false, savedAt: iso(9 * 24 * 60 * MINUTE) },
162
181
  ],
163
182
  },
164
183
  {
@@ -0,0 +1,2 @@
1
+ import type { Suite } from './support/suite.js';
2
+ export declare const unitySuite: Suite;
@@ -0,0 +1,7 @@
1
+ export const unitySuite = {
2
+ name: 'unity',
3
+ title: 'Unity: editors, modules, the release API and the licence (not available yet)',
4
+ run: (t) => {
5
+ t.check(true, 'unity: not available yet');
6
+ },
7
+ };
@@ -0,0 +1 @@
1
+ export declare function fingerprint(payload: unknown): string;