windows-development-sandbox 1.9.0 → 1.11.3

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 (398) hide show
  1. package/README.md +64 -17
  2. package/config.example.json +7 -2
  3. package/dist/app/features/accounts/accounts.def.d.ts +122 -0
  4. package/dist/app/features/accounts/accounts.def.js +243 -0
  5. package/dist/app/features/accounts/accounts.run.d.ts +51 -0
  6. package/dist/app/features/accounts/accounts.run.js +125 -0
  7. package/dist/app/features/accounts/index.d.ts +2 -0
  8. package/dist/app/features/accounts/index.js +3 -0
  9. package/dist/app/features/agents/agent-entry.d.ts +17 -0
  10. package/dist/app/features/agents/agent-entry.js +50 -0
  11. package/dist/app/features/agents/agents.def.d.ts +65 -0
  12. package/dist/app/features/agents/agents.def.js +142 -0
  13. package/dist/app/features/agents/agents.run.d.ts +31 -0
  14. package/dist/app/features/agents/agents.run.js +189 -0
  15. package/dist/app/features/agents/index.d.ts +3 -0
  16. package/dist/app/features/agents/index.js +4 -0
  17. package/dist/app/features/apps/apps-rows.d.ts +31 -0
  18. package/dist/app/features/apps/apps-rows.js +73 -0
  19. package/dist/app/features/apps/apps.def.d.ts +181 -0
  20. package/dist/app/features/apps/apps.def.js +264 -0
  21. package/dist/app/features/apps/apps.run.d.ts +58 -0
  22. package/dist/app/features/apps/apps.run.js +76 -0
  23. package/dist/app/features/apps/index.d.ts +3 -0
  24. package/dist/app/features/apps/index.js +4 -0
  25. package/dist/app/features/data/data.def.d.ts +154 -0
  26. package/dist/app/features/data/data.def.js +201 -0
  27. package/dist/app/features/data/data.run.d.ts +46 -0
  28. package/dist/app/features/data/data.run.js +141 -0
  29. package/dist/app/features/data/index.d.ts +2 -0
  30. package/dist/app/features/data/index.js +3 -0
  31. package/dist/app/features/defs.d.ts +952 -4
  32. package/dist/app/features/defs.js +147 -10
  33. package/dist/app/features/engine/engine.def.d.ts +228 -0
  34. package/dist/app/features/engine/engine.def.js +194 -0
  35. package/dist/app/features/engine/engine.run.d.ts +72 -0
  36. package/dist/app/features/engine/engine.run.js +77 -0
  37. package/dist/app/features/engine/index.d.ts +2 -0
  38. package/dist/app/features/engine/index.js +4 -0
  39. package/dist/app/features/files/files.def.d.ts +51 -0
  40. package/dist/app/features/files/files.def.js +38 -0
  41. package/dist/app/features/files/files.run.d.ts +20 -0
  42. package/dist/app/features/files/files.run.js +59 -0
  43. package/dist/app/features/files/index.d.ts +2 -0
  44. package/dist/app/features/files/index.js +3 -0
  45. package/dist/app/features/github/github.def.d.ts +80 -0
  46. package/dist/app/features/github/github.def.js +142 -0
  47. package/dist/app/features/github/github.run.d.ts +36 -0
  48. package/dist/app/features/github/github.run.js +59 -0
  49. package/dist/app/features/github/index.d.ts +2 -0
  50. package/dist/app/features/github/index.js +3 -0
  51. package/dist/app/features/health/health.def.d.ts +275 -0
  52. package/dist/app/features/health/health.def.js +281 -0
  53. package/dist/app/features/health/health.run.d.ts +125 -0
  54. package/dist/app/features/health/health.run.js +204 -0
  55. package/dist/app/features/health/index.d.ts +2 -0
  56. package/dist/app/features/health/index.js +3 -0
  57. package/dist/app/features/logs/index.d.ts +2 -0
  58. package/dist/app/features/logs/index.js +3 -0
  59. package/dist/app/features/logs/logs.def.d.ts +78 -0
  60. package/dist/app/features/logs/logs.def.js +115 -0
  61. package/dist/app/features/logs/logs.run.d.ts +20 -0
  62. package/dist/app/features/logs/logs.run.js +42 -0
  63. package/dist/app/features/projects/index.d.ts +2 -0
  64. package/dist/app/features/projects/index.js +3 -0
  65. package/dist/app/features/projects/projects.def.d.ts +101 -0
  66. package/dist/app/features/projects/projects.def.js +208 -0
  67. package/dist/app/features/projects/projects.run.d.ts +56 -0
  68. package/dist/app/features/projects/projects.run.js +155 -0
  69. package/dist/app/features/sandbox/sandbox.def.js +11 -5
  70. package/dist/app/features/sessions/sessions.def.js +3 -0
  71. package/dist/app/features/settings/categories.def.js +3 -2
  72. package/dist/{cli/menu/settings/share.d.ts → app/features/settings/settings-share.d.ts} +1 -1
  73. package/dist/{cli/menu/settings/share.js → app/features/settings/settings-share.js} +3 -2
  74. package/dist/app/features/settings/settings.def.d.ts +22 -5
  75. package/dist/app/features/settings/settings.def.js +81 -6
  76. package/dist/app/features/settings/settings.run.d.ts +17 -0
  77. package/dist/app/features/settings/settings.run.js +89 -4
  78. package/dist/app/features/storage/index.d.ts +2 -0
  79. package/dist/app/features/storage/index.js +3 -0
  80. package/dist/app/features/storage/storage.def.d.ts +133 -0
  81. package/dist/app/features/storage/storage.def.js +225 -0
  82. package/dist/app/features/storage/storage.run.d.ts +62 -0
  83. package/dist/app/features/storage/storage.run.js +370 -0
  84. package/dist/app/features/updates/index.d.ts +3 -0
  85. package/dist/app/features/updates/index.js +4 -0
  86. package/dist/app/features/updates/update-lines.d.ts +13 -0
  87. package/dist/app/features/updates/update-lines.js +61 -0
  88. package/dist/app/features/updates/updates.def.d.ts +89 -0
  89. package/dist/app/features/updates/updates.def.js +225 -0
  90. package/dist/app/features/updates/updates.run.d.ts +48 -0
  91. package/dist/app/features/updates/updates.run.js +154 -0
  92. package/dist/app/features/vpn/index.d.ts +2 -0
  93. package/dist/app/features/vpn/index.js +3 -0
  94. package/dist/app/features/vpn/vpn.def.d.ts +112 -0
  95. package/dist/app/features/vpn/vpn.def.js +174 -0
  96. package/dist/app/features/vpn/vpn.run.d.ts +37 -0
  97. package/dist/app/features/vpn/vpn.run.js +54 -0
  98. package/dist/app/features/web/index.d.ts +2 -0
  99. package/dist/app/features/web/index.js +3 -0
  100. package/dist/app/features/web/web.def.d.ts +33 -0
  101. package/dist/app/features/web/web.def.js +90 -0
  102. package/dist/app/features/web/web.run.d.ts +24 -0
  103. package/dist/app/features/web/web.run.js +93 -0
  104. package/dist/app/kernel/define.d.ts +10 -1
  105. package/dist/app/kernel/io.d.ts +34 -0
  106. package/dist/app/kernel/io.js +9 -0
  107. package/dist/app/kernel/rpc.d.ts +17 -0
  108. package/dist/app/kernel/text.d.ts +2 -0
  109. package/dist/app/kernel/text.js +7 -0
  110. package/dist/app/kernel/view.d.ts +24 -1
  111. package/dist/app/kernel/view.js +1 -1
  112. package/dist/app/registry.js +74 -1
  113. package/dist/cli/args.js +1 -1
  114. package/dist/cli/commands/context.d.ts +7 -0
  115. package/dist/cli/commands/context.js +29 -2
  116. package/dist/cli/commands/engine.js +5 -58
  117. package/dist/cli/commands/github.d.ts +2 -0
  118. package/dist/cli/commands/github.js +11 -0
  119. package/dist/cli/commands/index.d.ts +3 -5
  120. package/dist/cli/commands/index.js +3 -5
  121. package/dist/cli/commands/launch/launch.js +23 -4
  122. package/dist/cli/commands/launch/running.js +4 -1
  123. package/dist/cli/commands/maintenance/config.js +1 -6
  124. package/dist/cli/commands/maintenance/index.d.ts +0 -1
  125. package/dist/cli/commands/maintenance/index.js +1 -2
  126. package/dist/cli/commands/maintenance/logs.js +27 -32
  127. package/dist/cli/commands/maintenance/migrate.d.ts +0 -8
  128. package/dist/cli/commands/maintenance/migrate.js +2 -44
  129. package/dist/cli/commands/maintenance/update.d.ts +0 -19
  130. package/dist/cli/commands/maintenance/update.js +1 -99
  131. package/dist/cli/commands/projects/index.d.ts +0 -1
  132. package/dist/cli/commands/projects/index.js +0 -1
  133. package/dist/cli/commands/registry.d.ts +1 -1
  134. package/dist/cli/commands/registry.js +29 -11
  135. package/dist/cli/commands/repair/checks/data.js +1 -1
  136. package/dist/cli/commands/repair/checks/hcs.js +28 -1
  137. package/dist/cli/commands/repair/checks/sandbox.js +1 -1
  138. package/dist/cli/commands/repair/health-run.d.ts +30 -0
  139. package/dist/cli/commands/repair/health-run.js +32 -0
  140. package/dist/cli/commands/repair/printer.d.ts +2 -2
  141. package/dist/cli/commands/repair/printer.js +2 -11
  142. package/dist/cli/commands/repair/run.d.ts +1 -21
  143. package/dist/cli/commands/repair/run.js +13 -70
  144. package/dist/cli/commands/storage/index.d.ts +0 -1
  145. package/dist/cli/commands/storage/index.js +0 -1
  146. package/dist/cli/commands/storage/offer.js +6 -2
  147. package/dist/cli/commands/vpn.d.ts +1 -3
  148. package/dist/cli/commands/vpn.js +8 -88
  149. package/dist/cli/commands/web.d.ts +7 -1
  150. package/dist/cli/commands/web.js +45 -71
  151. package/dist/cli/index.js +3 -2
  152. package/dist/cli/manifest.d.ts +16 -153
  153. package/dist/cli/manifest.js +20 -50
  154. package/dist/cli/menu/command-form.js +1 -2
  155. package/dist/cli/menu/io-menu.d.ts +2 -2
  156. package/dist/cli/menu/io-menu.js +6 -4
  157. package/dist/cli/menu/menu.js +1 -1
  158. package/dist/cli/menu/pages/accounts-page.d.ts +5 -17
  159. package/dist/cli/menu/pages/accounts-page.js +28 -170
  160. package/dist/cli/menu/pages/agents-page.d.ts +17 -18
  161. package/dist/cli/menu/pages/agents-page.js +67 -124
  162. package/dist/cli/menu/pages/apps-page.d.ts +15 -8
  163. package/dist/cli/menu/pages/apps-page.js +121 -29
  164. package/dist/cli/menu/pages/list-page.js +10 -1
  165. package/dist/cli/menu/pages/maintenance-page.d.ts +51 -52
  166. package/dist/cli/menu/pages/maintenance-page.js +246 -427
  167. package/dist/cli/menu/pages/sandbox-page.d.ts +3 -5
  168. package/dist/cli/menu/pages/sandbox-page.js +34 -112
  169. package/dist/cli/menu/pages/settings-page.d.ts +21 -3
  170. package/dist/cli/menu/pages/settings-page.js +89 -17
  171. package/dist/cli/menu/render-view.js +2 -1
  172. package/dist/cli/menu/settings/index.d.ts +0 -1
  173. package/dist/cli/menu/settings/index.js +3 -3
  174. package/dist/cli/menu/views.js +28 -0
  175. package/dist/cli/operations/handler.js +7 -3
  176. package/dist/cli/operations/input.d.ts +1 -1
  177. package/dist/cli/operations/input.js +29 -5
  178. package/dist/cli/operations/io-cli.js +9 -1
  179. package/dist/cli/operations/spec.js +8 -2
  180. package/dist/cli/output/render-view.js +2 -0
  181. package/dist/cli/{commands/data/collect.js → services/data-collect.js} +2 -2
  182. package/dist/cli/{commands/data/remove.js → services/data-remove.js} +2 -2
  183. package/dist/cli/services/data-service.d.ts +1 -1
  184. package/dist/cli/services/data-service.js +2 -2
  185. package/dist/cli/services/databases-service.d.ts +5 -0
  186. package/dist/cli/services/databases-service.js +15 -0
  187. package/dist/cli/services/health-service.d.ts +8 -4
  188. package/dist/cli/services/health-service.js +10 -8
  189. package/dist/cli/services/index.d.ts +4 -1
  190. package/dist/cli/services/index.js +4 -1
  191. package/dist/cli/services/logs-service.d.ts +7 -0
  192. package/dist/cli/services/logs-service.js +13 -0
  193. package/dist/cli/services/storage-service.d.ts +19 -0
  194. package/dist/cli/services/storage-service.js +71 -0
  195. package/dist/cli/services/vpn-service.d.ts +6 -4
  196. package/dist/cli/services/vpn-service.js +5 -11
  197. package/dist/cli/services/web-service.d.ts +4 -3
  198. package/dist/cli/services/web-service.js +12 -32
  199. package/dist/core/config/defaults.js +3 -2
  200. package/dist/core/schema/config.d.ts +6 -0
  201. package/dist/core/schema/config.js +2 -0
  202. package/dist/core/schema/github.d.ts +4 -0
  203. package/dist/core/schema/github.js +7 -0
  204. package/dist/core/schema/index.d.ts +1 -0
  205. package/dist/core/schema/index.js +1 -0
  206. package/dist/core/schema/profile.d.ts +1 -0
  207. package/dist/core/schema/profile.js +3 -0
  208. package/dist/core/schema/sandbox.js +3 -3
  209. package/dist/core/schema/toolchains.js +2 -2
  210. package/dist/core/schema/vpn.js +1 -1
  211. package/dist/core/schema/web.d.ts +1 -0
  212. package/dist/core/schema/web.js +4 -1
  213. package/dist/core/types.d.ts +6 -3
  214. package/dist/guest/boot/boot.js +11 -1
  215. package/dist/guest/daemon/daemon.js +1 -1
  216. package/dist/guest/daemon/sessions.d.ts +3 -2
  217. package/dist/guest/daemon/sessions.js +38 -7
  218. package/dist/guest/governor/core.d.ts +44 -0
  219. package/dist/guest/governor/core.js +124 -0
  220. package/dist/guest/governor/decide.d.ts +53 -15
  221. package/dist/guest/governor/decide.js +155 -72
  222. package/dist/guest/governor/governor.d.ts +2 -35
  223. package/dist/guest/governor/governor.js +35 -99
  224. package/dist/guest/governor/index.d.ts +1 -0
  225. package/dist/guest/governor/index.js +1 -0
  226. package/dist/guest/network/firewall.d.ts +3 -0
  227. package/dist/guest/network/firewall.js +65 -23
  228. package/dist/guest/runtime/process.js +53 -5
  229. package/dist/guest/sessions/viewer.js +5 -1
  230. package/dist/guest/setup/claude-mem.js +12 -4
  231. package/dist/guest/setup/github-token.d.ts +7 -0
  232. package/dist/guest/setup/github-token.js +65 -0
  233. package/dist/guest/toolchains/languages/node.js +40 -4
  234. package/dist/guest/toolchains/native/rust-extras.js +2 -2
  235. package/dist/guest/toolchains/tools/cli.d.ts +8 -0
  236. package/dist/guest/toolchains/tools/cli.js +16 -2
  237. package/dist/guest/vpn/openvpn.d.ts +5 -0
  238. package/dist/guest/vpn/openvpn.js +14 -1
  239. package/dist/guest/vpn/vpn.js +55 -16
  240. package/dist/guest/vpn/wireguard.js +2 -0
  241. package/dist/host/channel/attach.d.ts +1 -0
  242. package/dist/host/channel/attach.js +1 -0
  243. package/dist/host/channel/channel.d.ts +1 -1
  244. package/dist/host/channel/channel.js +2 -2
  245. package/dist/host/engines/hcs/check.d.ts +1 -0
  246. package/dist/host/engines/hcs/check.js +1 -1
  247. package/dist/host/engines/hcs/engine.d.ts +1 -1
  248. package/dist/host/engines/hcs/engine.js +6 -4
  249. package/dist/host/engines/hcs/helper.d.ts +11 -0
  250. package/dist/host/engines/hcs/helper.js +31 -2
  251. package/dist/host/engines/hcs/leftovers.d.ts +42 -0
  252. package/dist/host/engines/hcs/leftovers.js +101 -0
  253. package/dist/host/engines/index.d.ts +1 -0
  254. package/dist/host/engines/index.js +1 -0
  255. package/dist/host/github/deliver.d.ts +13 -0
  256. package/dist/host/github/deliver.js +51 -0
  257. package/dist/host/github/index.d.ts +2 -0
  258. package/dist/host/github/index.js +3 -0
  259. package/dist/host/github/token.d.ts +21 -0
  260. package/dist/host/github/token.js +74 -0
  261. package/dist/host/launch/generate.js +11 -5
  262. package/dist/host/launch/index.d.ts +1 -1
  263. package/dist/host/launch/index.js +1 -1
  264. package/dist/host/launch/session-file.js +1 -2
  265. package/dist/host/launch/wsb-file.d.ts +11 -7
  266. package/dist/host/launch/wsb-file.js +30 -12
  267. package/dist/host/life/host-memory.d.ts +1 -0
  268. package/dist/host/life/host-memory.js +4 -1
  269. package/dist/host/life/watchdog.d.ts +11 -0
  270. package/dist/host/life/watchdog.js +36 -4
  271. package/dist/host/sandbox/tasklist.d.ts +6 -0
  272. package/dist/host/sandbox/tasklist.js +37 -0
  273. package/dist/host/secrets/launch-secrets.d.ts +2 -1
  274. package/dist/host/secrets/launch-secrets.js +4 -2
  275. package/dist/host/vpn/status.js +13 -3
  276. package/dist/host/web/backend.d.ts +1 -3
  277. package/dist/host/web/index.d.ts +2 -1
  278. package/dist/host/web/index.js +2 -1
  279. package/dist/host/web/live.d.ts +23 -0
  280. package/dist/host/web/live.js +107 -0
  281. package/dist/host/web/rpc.d.ts +6 -1
  282. package/dist/host/web/rpc.js +23 -31
  283. package/dist/host/web/runner.d.ts +4 -0
  284. package/dist/host/web/runner.js +12 -1
  285. package/dist/host/web/server.d.ts +30 -2
  286. package/dist/host/web/server.js +87 -29
  287. package/dist/host/web/state.d.ts +1 -0
  288. package/dist/host/web/state.js +2 -0
  289. package/dist/runtime/agent.js +44 -16
  290. package/dist/runtime/daemon.js +1154 -857
  291. package/dist/runtime/db.js +44 -16
  292. package/dist/runtime/main.js +532 -115
  293. package/dist/runtime/netproxy.js +34 -9
  294. package/dist/runtime/view.js +499 -149
  295. package/dist/selftest/app.test.js +620 -12
  296. package/dist/selftest/channel.test.js +18 -1
  297. package/dist/selftest/cli.test.js +1 -1
  298. package/dist/selftest/config.test.js +1 -1
  299. package/dist/selftest/contained.test.js +1 -1
  300. package/dist/selftest/data.test.js +1 -2
  301. package/dist/selftest/engine.test.js +3 -1
  302. package/dist/selftest/github.test.d.ts +2 -0
  303. package/dist/selftest/github.test.js +243 -0
  304. package/dist/selftest/guest-boot.test.js +47 -1
  305. package/dist/selftest/hcs.test.js +105 -8
  306. package/dist/selftest/health.test.js +71 -0
  307. package/dist/selftest/heavy-load.test.js +152 -66
  308. package/dist/selftest/index.js +4 -0
  309. package/dist/selftest/layout.test.js +8 -0
  310. package/dist/selftest/live-terminal.test.js +74 -0
  311. package/dist/selftest/menu-screens.js +171 -56
  312. package/dist/selftest/menu.test.js +54 -37
  313. package/dist/selftest/migrate.test.js +14 -12
  314. package/dist/selftest/network.test.js +20 -1
  315. package/dist/selftest/powershell-variables.test.d.ts +2 -0
  316. package/dist/selftest/powershell-variables.test.js +61 -0
  317. package/dist/selftest/support/engine-golden.js +1 -0
  318. package/dist/selftest/support/fixtures.d.ts +5 -0
  319. package/dist/selftest/support/web-preview.d.ts +1 -0
  320. package/dist/selftest/support/web-preview.js +856 -0
  321. package/dist/selftest/update-service.test.js +15 -8
  322. package/dist/selftest/vpn.test.js +86 -2
  323. package/dist/selftest/web.test.js +58 -11
  324. package/dist/shared/channel/run-dir.d.ts +2 -0
  325. package/dist/shared/channel/run-dir.js +3 -0
  326. package/dist/shared/channel/schema.d.ts +7 -0
  327. package/dist/shared/channel/schema.js +9 -1
  328. package/dist/shared/channel/secrets.d.ts +1 -0
  329. package/dist/shared/channel/secrets.js +3 -0
  330. package/dist/shared/github/index.d.ts +1 -0
  331. package/dist/shared/github/index.js +2 -0
  332. package/dist/shared/github/token.d.ts +29 -0
  333. package/dist/shared/github/token.js +89 -0
  334. package/dist/shared/io/copy-tree.d.ts +7 -0
  335. package/dist/shared/io/copy-tree.js +45 -0
  336. package/dist/shared/io/index.d.ts +2 -0
  337. package/dist/shared/io/index.js +2 -0
  338. package/dist/shared/io/robocopy-result.d.ts +6 -0
  339. package/dist/shared/io/robocopy-result.js +23 -0
  340. package/dist/shared/memory/policy.d.ts +38 -12
  341. package/dist/shared/memory/policy.js +58 -19
  342. package/dist/shared/network/firewall-rules.d.ts +8 -0
  343. package/dist/shared/network/firewall-rules.js +28 -0
  344. package/dist/shared/pty/ctrl-c.d.ts +18 -0
  345. package/dist/shared/pty/ctrl-c.js +36 -0
  346. package/dist/shared/pty/index.d.ts +1 -0
  347. package/dist/shared/pty/index.js +1 -0
  348. package/dist/shared/pty/terminal.d.ts +2 -0
  349. package/dist/shared/pty/terminal.js +21 -2
  350. package/dist/shared/vpn/kill-switch.d.ts +13 -2
  351. package/dist/shared/vpn/kill-switch.js +88 -8
  352. package/dist/shared/web/protocol.d.ts +0 -4
  353. package/dist/sync/watcher.js +1 -1
  354. package/dist/web/index.html +2 -2
  355. package/dist/web/{main-T5DMCHYV.css → main-6JF3ASB5.css} +1 -1
  356. package/dist/web/main-Z645W45E.js +219 -0
  357. package/native/csharp/HcsHost/AssemblyInfo.cs +3 -3
  358. package/native/csharp/HostTools/AssemblyInfo.cs +3 -3
  359. package/native/csharp/HostTools/MemoryStatus.cs +128 -107
  360. package/native/scripts/memory-status.ps1 +10 -2
  361. package/package.json +10 -3
  362. package/sandbox/csharp/MemGov.cs +668 -398
  363. package/sandbox/csharp/PtyHost.cs +104 -15
  364. package/sandbox/csharp/SandboxTools/CpuCap.cs +50 -1
  365. package/sandbox/csharp/SandboxTools/Program.cs +1 -1
  366. package/sandbox/scripts/new-shortcuts.ps1 +3 -1
  367. package/sandbox/scripts/repair-wmi-services.ps1 +26 -0
  368. package/schemas/config.schema.json +25 -7
  369. package/schemas/sandbox-config.schema.json +7 -3
  370. package/dist/cli/commands/accounts.d.ts +0 -2
  371. package/dist/cli/commands/accounts.js +0 -96
  372. package/dist/cli/commands/data/clean.d.ts +0 -3
  373. package/dist/cli/commands/data/clean.js +0 -60
  374. package/dist/cli/commands/data/index.d.ts +0 -4
  375. package/dist/cli/commands/data/index.js +0 -62
  376. package/dist/cli/commands/maintenance/clean.d.ts +0 -2
  377. package/dist/cli/commands/maintenance/clean.js +0 -24
  378. package/dist/cli/commands/maintenance/config-share.d.ts +0 -12
  379. package/dist/cli/commands/maintenance/config-share.js +0 -80
  380. package/dist/cli/commands/projects/projects.d.ts +0 -2
  381. package/dist/cli/commands/projects/projects.js +0 -162
  382. package/dist/cli/commands/storage/contained.d.ts +0 -3
  383. package/dist/cli/commands/storage/contained.js +0 -179
  384. package/dist/cli/commands/storage/storage.d.ts +0 -8
  385. package/dist/cli/commands/storage/storage.js +0 -179
  386. package/dist/cli/menu/add-agent.d.ts +0 -47
  387. package/dist/cli/menu/add-agent.js +0 -168
  388. package/dist/cli/menu/apps.d.ts +0 -69
  389. package/dist/cli/menu/apps.js +0 -214
  390. package/dist/cli/menu/data-folder.d.ts +0 -18
  391. package/dist/cli/menu/data-folder.js +0 -99
  392. package/dist/cli/menu/share-settings.d.ts +0 -21
  393. package/dist/cli/menu/share-settings.js +0 -112
  394. package/dist/cli/menu/vpn-login.d.ts +0 -16
  395. package/dist/cli/menu/vpn-login.js +0 -74
  396. package/dist/web/main-NRRS5O6Z.js +0 -157
  397. /package/dist/cli/{commands/data/collect.d.ts → services/data-collect.d.ts} +0 -0
  398. /package/dist/cli/{commands/data/remove.d.ts → services/data-remove.d.ts} +0 -0
@@ -3,22 +3,38 @@
3
3
  // exempt with a reason), the reference views' pages, their CLI and menu renderings, and the settings feature run
4
4
  // against a config file of its own.
5
5
  import fs from 'node:fs';
6
+ import path from 'node:path';
6
7
  import { z } from 'zod';
7
- import { DEFINITIONS, NAV, QUERIES, VIEWS, viewById } from '../app/features/defs.js';
8
+ import { agentsPage, agentsView } from '../app/features/agents/index.js';
9
+ import { DEFINITIONS, NAV, navSections, navViews, QUERIES, VIEWS, viewById } from '../app/features/defs.js';
10
+ import { accountsPage, agentPage, listAccounts } from '../app/features/accounts/index.js';
11
+ import { addProject, projectRoots, projectsPage } from '../app/features/projects/index.js';
12
+ import { cleanData, dataPage, showData } from '../app/features/data/index.js';
13
+ import { showStorage, storagePage, storageView, } from '../app/features/storage/index.js';
14
+ import { insideSandbox, isSandboxRunning } from '../host/sandbox/index.js';
15
+ import { CRASH, listLogs, logsPage, tailLog } from '../app/features/logs/index.js';
16
+ import { DEFAULTS } from '../core/config/index.js';
8
17
  import { sandboxPage } from '../app/features/sandbox/index.js';
9
18
  import { sessionsPage } from '../app/features/sessions/index.js';
19
+ import { APP_GROUPS, appsPage, listApps } from '../app/features/apps/index.js';
10
20
  import { settingsPage } from '../app/features/settings/index.js';
11
- import { ago, Cancelled, ClientMessageSchema, defineOperation, exemption, fakeIO, fieldOf, fieldProblem, FieldSchema, fieldsOf, fromWireError, humanize, implementer, invoke, pageActions, ServerMessageSchema, SURFACE_NAMES, toWireError, valueText, } from '../app/kernel/index.js';
12
- import { IMPLEMENTATIONS, readQuery, runDefinition } from '../app/registry.js';
21
+ import { checkEngine, checkText, enginePage } from '../app/features/engine/index.js';
22
+ import { runUpdate, updatesPage } from '../app/features/updates/index.js';
23
+ import { MenuOperations } from '../cli/menu/io-menu.js';
24
+ import { ago, bytesText, Cancelled, ClientMessageSchema, defineOperation, exemption, fakeIO, fieldOf, fieldProblem, FieldSchema, fieldsOf, fromWireError, humanize, implementer, invoke, pageActions, pageHasContent, ServerMessageSchema, SURFACE_NAMES, toWireError, valueText, } from '../app/kernel/index.js';
25
+ import { createRpcHost, IMPLEMENTATIONS, readQuery, runDefinition } from '../app/registry.js';
26
+ import { webPage } from '../app/features/web/index.js';
13
27
  import { parseCli } from '../cli/args.js';
28
+ import { contextOrDefaults } from '../cli/commands/index.js';
14
29
  import { COMMANDS, commandSpec } from '../cli/manifest.js';
15
30
  import { menuView } from '../cli/menu/render-view.js';
16
31
  import { MENU_VIEWS } from '../cli/menu/views.js';
17
- import { cliInput, commandSpecOf, definitionFor } from '../cli/operations/index.js';
32
+ import { cliIO, cliInput, commandSpecOf, definitionFor } from '../cli/operations/index.js';
18
33
  import { pageLines } from '../cli/output/index.js';
19
34
  import { secret } from '../core/schema/index.js';
20
35
  import { UserError } from '../core/util/index.js';
21
36
  const NOW = Date.now();
37
+ const NPM_COMMAND = 'npm install -g windows-development-sandbox';
22
38
  const running = (id, here) => ({
23
39
  id,
24
40
  launchId: 'l1',
@@ -66,7 +82,11 @@ const STATUS = {
66
82
  };
67
83
  /** The operations and queries a list of views can reach. */
68
84
  function reached(views) {
69
- return new Set(views.flatMap((v) => [...v.operations.map((o) => o.id), ...Object.values(v.queries).map((q) => q.id)]));
85
+ return new Set(views.flatMap((v) => [
86
+ ...v.operations.map((o) => o.id),
87
+ ...Object.values(v.queries).map((q) => q.id),
88
+ ...(v.pickers ?? []).map((q) => q.id),
89
+ ]));
70
90
  }
71
91
  /** Why a definition is not reachable on a surface ('' when it is). */
72
92
  function gap(def, surface, reach) {
@@ -189,7 +209,7 @@ export const appSuite = {
189
209
  ids.every((id) => implemented.includes(id)) &&
190
210
  implemented.every((id) => ids.includes(id)) &&
191
211
  IMPLEMENTATIONS.every((i) => DEFINITIONS.includes(i.def)), 'every definition has exactly one implementation and every implementation is registered', ids.filter((id) => !implemented.includes(id)).join(', '));
192
- const webViews = NAV.flatMap((n) => (n.view ? [viewById(n.view)] : []));
212
+ const webViews = navViews(NAV).map((id) => viewById(id));
193
213
  const reach = {
194
214
  cli: new Set(),
195
215
  menu: reached(MENU_VIEWS.map((m) => m.view)),
@@ -198,7 +218,7 @@ export const appSuite = {
198
218
  const gaps = DEFINITIONS.flatMap((d) => SURFACE_NAMES.map((s) => [d.id, s, gap(d, s, reach)]).filter(([, , why]) => why)).map(([id, s, why]) => id + ' on ' + s + ': ' + why);
199
219
  t.equal(gaps, [], 'the parity list shows no gaps');
200
220
  t.check(VIEWS.every((v) => webViews.includes(v) || MENU_VIEWS.some((m) => m.view === v)) &&
201
- NAV.every((n) => n.view !== undefined || Boolean(n.elsewhere)), 'every view is shown somewhere, and a web section without a view says where the area is');
221
+ navSections(NAV).every((n) => n.view !== undefined || (n.children?.length ?? 0) > 0 || Boolean(n.elsewhere)), 'every view is shown somewhere, and a web section without a view says where the area is');
202
222
  const hinted = DEFINITIONS.filter((d) => d.cli);
203
223
  t.check(hinted.every((d) => {
204
224
  const names = [...(d.cli?.positional ?? []), ...Object.keys(d.cli?.flags ?? {})];
@@ -233,11 +253,14 @@ export const appSuite = {
233
253
  shared: { line: 'Running, started by bob (not shared)', limited: true, startedBy: 'bob' },
234
254
  });
235
255
  const ops = (page) => pageActions(page).flatMap((a) => (a.kind === 'operation' ? [a.op] : []));
236
- t.check(ops(statusPage).join() === 'sandbox.showWindow,sandbox.hideWindow,sandbox.stop' &&
256
+ const hiddenPage = sandboxPage({ ...STATUS, sandbox: { ...STATUS.sandbox, headless: true } });
257
+ const shownPage = sandboxPage({ ...STATUS, sandbox: { ...STATUS.sandbox, headless: false } });
258
+ t.check(ops(hiddenPage).join() === 'sandbox.showWindow,sandbox.stop' &&
259
+ ops(shownPage).join() === 'sandbox.hideWindow,sandbox.stop' &&
237
260
  pageActions(statusPage).some((a) => a.kind === 'operation' && a.op === 'sandbox.stop' && a.danger) &&
238
261
  ops(stoppedPage).length === 0 &&
239
262
  limited.sections.length === 0 &&
240
- limited.notice?.text === 'Running, started by bob (not shared)', "status: the window and stop (marked dangerous) only while it runs; another user's unshared sandbox shows only whose it is");
263
+ limited.notice?.text === 'Running, started by bob (not shared)', "status: show or hide (whichever changes the window) and stop (marked dangerous) only while it runs; another user's unshared sandbox shows only whose it is");
241
264
  const list = {
242
265
  sandboxRunning: true,
243
266
  defaultAgent: 'codex',
@@ -258,6 +281,36 @@ export const appSuite = {
258
281
  if (sample)
259
282
  t.check(ops(sample).every((op) => known.has(op)), view.id + ': the page offers only operations its view declares');
260
283
  }
284
+ t.section('agents');
285
+ const agentsSample = agentsPage({
286
+ folder: 'C:\\code\\api',
287
+ defaultAgent: 'claude',
288
+ agents: [
289
+ { name: 'codex', ready: false, isDefault: false, removable: true },
290
+ { name: 'claude', ready: true, isDefault: true, removable: false },
291
+ ],
292
+ });
293
+ const agentItems = agentsSample.sections[0].items ?? [];
294
+ const agentKeys = (id) => (agentItems.find((i) => i.id === id)?.actions ?? [])
295
+ .flatMap((a) => (a.kind === 'operation' && a.key ? [a.key] : []))
296
+ .join();
297
+ t.check(agentItems.map((i) => i.id).join() === 'agent:claude,agent:codex,add' &&
298
+ agentKeys('agent:claude') === '' &&
299
+ agentKeys('agent:codex') === 'd,x' &&
300
+ agentKeys('add') === '' &&
301
+ agentItems[0].primary?.kind === 'link' &&
302
+ ops(agentsSample).every((op) => agentsView.operations.some((o) => o.id === op)), "agents: the default first, Add last, d/x only where they apply; the page offers only its view's operations");
303
+ const agentIO = fakeIO().io;
304
+ const noSandboxCtx = {};
305
+ await t.throws(() => runDefinition('agents.start', noSandboxCtx, { agent: '../x' }, agentIO), 'That does not work for start an agent', 'agents.start refuses odd names');
306
+ await t.throws(() => runDefinition('agents.start', noSandboxCtx, { agent: 'claude' }, agentIO), 'No Windows Sandbox is running.', 'agents.start without a running sandbox says how to start one');
307
+ const agentsConfig = t.at('app-agents', 'config.json');
308
+ t.write(agentsConfig, '{ "defaultHarness": "claude" }\n');
309
+ const agentsCtx = { loaded: { configPath: agentsConfig } };
310
+ await runDefinition('agents.add', agentsCtx, {}, fakeIO(['npm', 'mine', '@me/mine@latest', 'mine']).io);
311
+ const before = fs.readFileSync(agentsConfig, 'utf8');
312
+ await t.throws(() => runDefinition('agents.remove', agentsCtx, { name: 'mine' }, fakeIO([false]).io), 'Nothing changed.', 'agents.remove asks, and a no changes nothing');
313
+ t.check(fs.readFileSync(agentsConfig, 'utf8') === before, 'the config file is unchanged after a no');
261
314
  t.section('the CLI and the menu draw the same page');
262
315
  const sessionLines = pageLines(sessionsPage({ ...list, wide: true }));
263
316
  t.check(/^\s+ID\s+AGENT\s+PROJECT\s+STATE\s+ATTACHED\s+FOLDER\s+STARTED$/.test(sessionLines[0]) &&
@@ -265,7 +318,7 @@ export const appSuite = {
265
318
  sessionLines.includes('* = the project of this folder') &&
266
319
  !sessionLines.some((l) => l.includes('--wide')), 'sessions on the command line: the table with the folder column (--wide), the marker and its note', sessionLines.join('\n'));
267
320
  const statusLines = pageLines(statusPage);
268
- t.check(statusLines.some((l) => /^Running\s+yes$/.test(l)) &&
321
+ t.check(statusLines.some((l) => /^State\s+\S/.test(l)) &&
269
322
  statusLines.some((l) => /^Helper\s+responding, 2 sessions running$/.test(l)) &&
270
323
  statusLines.includes('* api C:\\Projects\\api -> C:\\Projects\\api (started from here)') &&
271
324
  statusLines.some((l) => l.startsWith('Repo guard 1 folder(s) guarded')), 'status on the command line: facts as rows, projects with their details, the repo guard', statusLines.join('\n'));
@@ -273,11 +326,11 @@ export const appSuite = {
273
326
  const menu = menuView(sessions);
274
327
  const chosen = menu.resolve({ kind: 'pick', value: 'new' });
275
328
  t.check(menu.items.length === 2 &&
276
- menu.actions.map((a) => a.key).join() === 'r,o,l' &&
329
+ menu.actions.map((a) => a.key).join() === 'n,r,o,l' &&
277
330
  menu.enterLabel(undefined, 'new') === 'attach' &&
278
331
  chosen?.kind === 'action' &&
279
332
  chosen.action.kind === 'link' &&
280
- menu.resolve({ kind: 'action', key: 'l', value: undefined })?.kind === 'refresh', 'sessions in the menu: rows, the r/o/l keys, Enter attaches');
333
+ menu.resolve({ kind: 'action', key: 'l', value: undefined })?.kind === 'refresh', 'sessions in the menu: rows, the n/r/o/l keys, Enter attaches');
281
334
  const menuStatus = menuView(statusPage, { category: 'status', prefix: 'status:' });
282
335
  t.check(menuStatus.items.every((i) => i.category === 'status' && i.value.startsWith('status:')) &&
283
336
  menuStatus.actions.some((a) => a.key === 'x' && a.label === 'stop'), 'a view inside one category of a page: rows in it, prefixed, with the keys of its actions');
@@ -304,6 +357,18 @@ export const appSuite = {
304
357
  const reset = (await runDefinition('settings.reset', ctx, { key: 'sandbox.memoryMb' }, quiet));
305
358
  t.check(reset.message.startsWith('sandbox.memoryMb is back to its default') &&
306
359
  !fs.readFileSync(configFile, 'utf8').includes('8192'), 'reset removes the line');
360
+ t.section('the folder picker (files.browse)');
361
+ const browseRoot = t.dir('app-browse');
362
+ fs.mkdirSync(path.join(browseRoot, 'zeta'));
363
+ fs.mkdirSync(path.join(browseRoot, 'Alpha'));
364
+ fs.mkdirSync(path.join(browseRoot, '$Recycle.Bin'));
365
+ fs.writeFileSync(path.join(browseRoot, 'notes.txt'), 'x');
366
+ const folders = (await runDefinition('files.browse', ctx, { path: browseRoot }, quiet));
367
+ t.equal(folders.entries.map((e) => e.name).join(), 'Alpha,zeta', 'a folder lists its folders by name, without files or $ system folders');
368
+ const withFiles = (await runDefinition('files.browse', ctx, { path: browseRoot, files: true }, quiet));
369
+ t.equal(withFiles.entries.map((e) => e.kind + ':' + e.name).join(), 'dir:Alpha,dir:zeta,file:notes.txt', 'with files: folders first, then files');
370
+ t.check(folders.parent === path.dirname(browseRoot), 'it names the folder above');
371
+ await t.throws(() => runDefinition('files.browse', ctx, { path: 'relative\\folder' }, quiet), 'is not a full path', 'a path that is not a full path is refused');
307
372
  const listed = await readQuery(QUERIES.find((q) => q.id === 'settings.list'), ctx, { scope: 'user' });
308
373
  const rows = listed.rows;
309
374
  const text = JSON.stringify(listed);
@@ -319,6 +384,549 @@ export const appSuite = {
319
384
  edit.op === 'settings.set' &&
320
385
  edit.fields?.[0]?.name === 'value' &&
321
386
  page.sections.some((s) => s.items?.some((i) => i.id === '@data-folder')), 'the settings page: a section per category, each setting edited through its Field');
387
+ t.section('settings export and import, run against a config file');
388
+ const exportFile = t.at('app-settings', 'out.json');
389
+ const exported = (await runDefinition('settings.export', ctx, { settingsFile: exportFile }, fakeIO().io));
390
+ const written = fs.readFileSync(exportFile, 'utf8');
391
+ t.check(!('storage' in JSON.parse(written)) &&
392
+ !written.includes('sk-planted-secret') &&
393
+ exported.leftOut.some((l) => l.startsWith('harnesses.planted') && l.includes('a secret, never shared')), 'settings.export writes only what differs and says what it left out', exported.leftOut.join('; '));
394
+ await t.throws(() => runDefinition('settings.export', ctx, { settingsFile: exportFile }, fakeIO([false]).io), 'Nothing changed.', 'settings.export asks before replacing a file that is there');
395
+ await runDefinition('settings.export', ctx, { settingsFile: exportFile }, fakeIO([true]).io);
396
+ t.check(fs.existsSync(exportFile), 'settings.export replaces the file when asked to');
397
+ const shared = t.writeJson(t.at('app-settings', 'shared.json'), { sandbox: { memoryMb: 2048 } });
398
+ const imported = fakeIO([true]);
399
+ const done = (await runDefinition('settings.import', ctx, { settingsFile: shared }, imported.io));
400
+ const got2048 = (await runDefinition('settings.get', ctx, { key: 'sandbox.memoryMb' }, quiet));
401
+ t.check(done.message.startsWith('Imported 1 setting') &&
402
+ imported.seen.logs.some((l) => l.text.startsWith(' sandbox.memoryMb: ') && l.text.endsWith('-> 2048')) &&
403
+ imported.seen.questions[0]?.title === 'Change your settings like this?' &&
404
+ got2048.value === 2048, 'settings.import logs the diff, asks once, and applies', done.message);
405
+ const shared1024 = t.writeJson(t.at('app-settings', 'shared1024.json'), { sandbox: { memoryMb: 1024 } });
406
+ const before1024 = fs.readFileSync(configFile, 'utf8');
407
+ await t.throws(() => runDefinition('settings.import', ctx, { settingsFile: shared1024 }, fakeIO([false]).io), 'Nothing was changed.', 'settings.import with a no changes nothing');
408
+ t.check(fs.readFileSync(configFile, 'utf8') === before1024, 'a declined import leaves the file as it was');
409
+ const own = settingsPage(listed);
410
+ const theirs = settingsPage({ ...listed, scope: 'project' });
411
+ const pageOps = (p) => (p.actions ?? []).flatMap((a) => (a.kind === 'operation' ? [a.op] : []));
412
+ t.check(pageOps(own).join() === 'settings.export,settings.import' &&
413
+ (own.actions ?? []).every((a) => a.kind === 'operation' && a.fields?.length === 1 && a.fields[0]?.kind === 'path') &&
414
+ pageOps(theirs).length === 0, 'the settings page offers export and import only for your own settings, each asking one file path');
415
+ t.section('apps, run against a config file');
416
+ const appsConfig = t.writeJson(t.at('app-apps', 'config.json'), {
417
+ storage: { root: t.dir('app-apps', 'storage') },
418
+ });
419
+ const appsCtx = {
420
+ loaded: { configPath: appsConfig },
421
+ options: { workspace: t.dir('app-apps', 'project') },
422
+ storageRoot: t.at('app-apps', 'storage'),
423
+ dataPath: t.dir('app-apps', 'data'),
424
+ };
425
+ await runDefinition('apps.toggle', appsCtx, { id: 'lazygit', on: true }, quiet);
426
+ const onText = fs.readFileSync(appsConfig, 'utf8');
427
+ await runDefinition('apps.toggle', appsCtx, { id: 'lazygit', on: false }, quiet);
428
+ const offText = fs.readFileSync(appsConfig, 'utf8');
429
+ t.check(onText.includes('"lazygit"') && !offText.includes('"lazygit"'), 'apps: turning an app on writes apps.install, off removes it');
430
+ await t.throws(() => runDefinition('apps.toggle', appsCtx, { scope: 'project', id: 'vscode', on: false }, quiet), 'is not an app this scope can change', 'apps: a project cannot change VS Code');
431
+ const dataDir = t.dir('app-apps', 'data');
432
+ fs.mkdirSync(path.join(dataDir, 'tools', 'apps', 'lazygit'), { recursive: true });
433
+ fs.writeFileSync(path.join(dataDir, 'tools', 'apps', 'lazygit', '.agent-sandbox-app'), 'lazygit');
434
+ const appsList = (await readQuery(listApps, appsCtx, { scope: 'user' }));
435
+ const lazygit = appsList.apps.find((a) => a.id === 'lazygit');
436
+ t.check(lazygit?.installed === true && appsList.apps.every((a) => APP_GROUPS.some((g) => g.id === a.group)), 'apps: the list marks downloaded apps installed, and every row sits in a group');
437
+ const hand = {
438
+ scope: 'user',
439
+ file: 'config.json',
440
+ exists: true,
441
+ apps: [
442
+ {
443
+ id: 'lazygit',
444
+ name: 'lazygit',
445
+ about: 'Git in a terminal',
446
+ group: 'tools',
447
+ size: '5 MB',
448
+ cli: true,
449
+ mesa: false,
450
+ fullGitOnly: false,
451
+ installed: true,
452
+ on: true,
453
+ },
454
+ ],
455
+ custom: [],
456
+ customValue: {},
457
+ customField: { name: 'custom', key: ['apps', 'custom'], kind: 'map', label: 'Your own programs', optional: true },
458
+ };
459
+ const userPage = appsPage(hand);
460
+ const projectPage = appsPage({ ...hand, scope: 'project' });
461
+ const actionsOf = (page) => pageActions(page).flatMap((a) => (a.kind === 'operation' ? [a.op] : []));
462
+ t.check(userPage.sections.some((s) => s.id === 'tools') &&
463
+ actionsOf(userPage).includes('apps.setCustom') &&
464
+ actionsOf(userPage).includes('apps.request') &&
465
+ !actionsOf(projectPage).includes('apps.setCustom') &&
466
+ !actionsOf(projectPage).includes('apps.request'), 'apps: the page has a category per group; your own programs and the request only in your own scope');
322
467
  t.equal(ago(NOW - 90 * 60000, NOW), '2 h ago', 'times read the same on every surface');
468
+ t.section('accounts, run against a temp folder');
469
+ const accCtx = {
470
+ loaded: { config: structuredClone(DEFAULTS), configPath: null, base: t.temp },
471
+ options: { workspace: t.dir('app-acc', 'project') },
472
+ storageRoot: t.dir('app-acc', 'storage'),
473
+ dataPath: t.dir('app-acc', 'data'),
474
+ };
475
+ t.write(t.at('app-acc', 'data', 'state', 'codex', 'auth.json'), '{"token":"A"}');
476
+ const saving = fakeIO(['bad name', 'main']);
477
+ const saved = (await runDefinition('accounts.save', accCtx, { agent: 'codex' }, saving.io));
478
+ t.check(saving.seen.questions[0]?.kind === 'text' &&
479
+ saving.seen.questions.length === 2 &&
480
+ saved.message === 'Saved account "main".', 'accounts: save asks a name when none is given, and refuses a bad one', saved.message);
481
+ const listed2 = (await readQuery(listAccounts, accCtx, { agent: 'codex' }));
482
+ t.check(listed2.agents[0].name === 'claude' &&
483
+ listed2.agents.find((a) => a.name === 'codex')?.accounts[0]?.name === 'main' &&
484
+ JSON.stringify(listAccounts.cli?.shapeJson?.(listed2)) ===
485
+ JSON.stringify(listed2.agents.find((a) => a.name === 'codex')?.accounts), 'accounts: list shows Claude first and the saved codex login; --json keeps the rows of the one agent');
486
+ await t.throws(() => runDefinition('accounts.remove', accCtx, { agent: 'codex', name: 'main' }, fakeIO([false]).io), 'Nothing removed.', 'accounts: remove asks first; no is Nothing removed.');
487
+ const removed = (await runDefinition('accounts.remove', accCtx, { agent: 'codex', name: 'main' }, fakeIO([true]).io));
488
+ t.equal(removed.message, 'Removed account main.', 'accounts: yes removes the login');
489
+ const codex = {
490
+ name: 'codex',
491
+ kept: true,
492
+ canSavePc: true,
493
+ accounts: [{ name: 'main', email: null, isDefault: false, isActive: false, savedAt: '2026-10-12T08:00:00Z' }],
494
+ };
495
+ const codexMenu = menuView(agentPage(codex), { category: 'codex', refresh: false });
496
+ t.check(codexMenu.enterLabel('codex', 'codex:main') === 'use' &&
497
+ ['s', 'p', 'd', 'r', 'x'].every((k) => codexMenu.actions.some((a) => a.key === k)) &&
498
+ accountsPage({ agent: 'codex', agents: [codex] }).sections[0].cards === true, 'accounts: the page offers use on Enter and d/r/x on a login; the codex agent is a card');
499
+ const useDef = definitionFor('account', ['use']);
500
+ t.check(JSON.stringify(cliInput(useDef, parseCli(['account', 'use', 'work', '--no-restart']))) ===
501
+ JSON.stringify({ name: 'work', noRestart: true }) &&
502
+ !('noRestart' in cliInput(useDef, parseCli(['account', 'use', 'work']))), 'a --no- flag reaches the input (and is absent without it)');
503
+ t.section('projects, run against a temp folder');
504
+ const projCfg = t.at('app-projects', 'config.json');
505
+ t.write(projCfg, '{}\n');
506
+ const projCtx = {
507
+ loaded: { config: structuredClone(DEFAULTS), configPath: projCfg, base: t.temp },
508
+ options: { workspace: t.dir('app-projects', 'here') },
509
+ storageRoot: t.dir('app-projects', 'storage'),
510
+ packageRoot: t.dir('app-projects', 'package'),
511
+ dataPath: t.dir('app-projects', 'data'),
512
+ };
513
+ const projQuiet = fakeIO().io;
514
+ const rootDir = t.dir('app-projects', 'roots', 'code');
515
+ const addedRoot = (await runDefinition('projects.roots', projCtx, { change: 'add', rootFolder: rootDir }, projQuiet));
516
+ t.check(addedRoot.message.startsWith('Added the project root ' + rootDir + ':'), 'projects roots add: the folder becomes a root and the message says so', addedRoot.message);
517
+ const rootAgain = (await runDefinition('projects.roots', projCtx, { change: 'add', rootFolder: rootDir }, projQuiet));
518
+ t.equal(rootAgain.message, rootDir + ' is a root already.', 'projects roots add: a root twice is said, not added');
519
+ await t.throws(() => runDefinition('projects.roots', projCtx, { change: 'remove', rootFolder: rootDir }, fakeIO([false]).io), 'Nothing changed.', 'projects roots remove asks first; a no changes nothing');
520
+ t.check(fs.readFileSync(projCfg, 'utf8').includes('roots'), 'the root is still in the settings file after a no');
521
+ const removedRoot = (await runDefinition('projects.roots', projCtx, { change: 'remove', rootFolder: rootDir }, fakeIO([true]).io));
522
+ t.check(removedRoot.message.startsWith('Removed the project root ' + rootDir), 'projects roots remove: yes removes it', removedRoot.message);
523
+ await t.throws(() => runDefinition('projects.roots', projCtx, { change: 'add', rootFolder: 'C:\\' }, projQuiet), 'A whole drive cannot be a project root', 'a whole drive cannot be a project root');
524
+ const projectDir = t.dir('app-projects', 'api');
525
+ const addedProject = (await runDefinition('projects.add', projCtx, { projectFolder: projectDir }, projQuiet));
526
+ t.check(addedProject.message === 'Added ' + projectDir + ' to your projects.' &&
527
+ addedProject.next === 'Next: sandbox projects all on (map every project at every start)' &&
528
+ (addProject.text?.(addedProject) ?? '').endsWith(addedProject.next ?? '~'), 'projects add: says it was added, and what to do next while all projects is off (on the command line too)', addedProject.message);
529
+ await t.throws(() => runDefinition('projects.remove', projCtx, { projectFolder: projectDir }, fakeIO([false]).io), 'Nothing changed.', 'projects remove: a no is Nothing changed and keeps the project');
530
+ const allOn = (await runDefinition('projects.all', projCtx, { state: 'on' }, projQuiet));
531
+ t.check(allOn.message.startsWith('Mapping all projects at every start is on. This changes what is mapped'), 'projects all on: the setting is written and the next start is said', allOn.message);
532
+ await t.throws(() => runDefinition('projects.choose', projCtx, {}, fakeIO([undefined]).io), 'Nothing changed.', 'projects choose: backing out changes nothing');
533
+ const projectsSample = projectsPage({
534
+ all: false,
535
+ roots: ['C:\\Projects'],
536
+ exclude: [],
537
+ projects: [
538
+ {
539
+ folder: 'C:\\Projects\\api',
540
+ here: true,
541
+ known: true,
542
+ root: 'C:\\Projects',
543
+ excluded: false,
544
+ mappedAtStart: true,
545
+ running: 'api (root)',
546
+ },
547
+ {
548
+ folder: 'F:\\Code\\scratch',
549
+ here: false,
550
+ known: true,
551
+ root: null,
552
+ excluded: true,
553
+ mappedAtStart: false,
554
+ running: null,
555
+ },
556
+ ],
557
+ });
558
+ const projMenu = menuView(projectsSample, { category: 'projects', refresh: false });
559
+ t.check(['@add-root', 'root:C:\\Projects', 'project:C:\\Projects\\api', 'project:F:\\Code\\scratch'].every((v) => projMenu.items.some((i) => i.value === v)) &&
560
+ projMenu.actions.map((a) => a.key).join() === 'a,f,f' &&
561
+ projMenu.actions
562
+ .filter((a) => a.key === 'f')
563
+ .map((a) => a.label)
564
+ .join() === 'remove root,forget project' &&
565
+ projMenu.enterLabel(undefined, '@add-root') === 'choose folder' &&
566
+ projMenu.resolve({ kind: 'action', key: 'a', value: undefined })?.kind === 'action', 'projects in the menu: the row values (@add-root, root:, project:) and the keys a, f (root), f (project)', projMenu.actions.map((a) => a.key + ':' + a.label).join(' '));
567
+ const projLines = pageLines(projectsSample);
568
+ t.check(projLines.some((l) => /^All projects\s+off$/.test(l)) &&
569
+ projLines.some((l) => /FOLDER\s+AT START\s+RUNNING SANDBOX/.test(l)) &&
570
+ projLines.includes('* = this folder. Choose with: sandbox projects choose'), 'projects list on the command line: the facts, the table and its note', projLines.join('\n'));
571
+ const rootFieldDef = fieldsOf(projectRoots.input).find((f) => f.name === 'rootFolder');
572
+ const folderFieldDef = fieldsOf(addProject.input).find((f) => f.name === 'projectFolder');
573
+ t.check(rootFieldDef?.kind === 'path' &&
574
+ rootFieldDef.pathKind === 'dir' &&
575
+ folderFieldDef?.kind === 'path' &&
576
+ folderFieldDef.pathKind === 'dir', 'the folder fields of projects are folder pickers');
577
+ const commandChoices = commandSpec('projects')?.args[0]?.choices ?? [];
578
+ t.check(['list', 'choose', 'add', 'remove', 'include', 'exclude', 'all', 'roots'].every((a) => commandChoices.includes(a)) &&
579
+ definitionFor('projects', ['roots'])?.id === 'projects.roots' &&
580
+ definitionFor('projects', []) === undefined, 'the commands keep their names: projects takes the eight actions; roots is its own definition');
581
+ t.section('data, run against temp folders');
582
+ const stored = showData.output.parse({
583
+ dataPath: 'C:\\AgentData',
584
+ storageRoot: 'C:\\ProgramData\\AgentSandbox',
585
+ totalBytes: 6.5e9,
586
+ total: '6.5 GB',
587
+ storageShare: '<- sandbox storage (6.5 GB of it)',
588
+ areaTotals: { tools: '4.6 GB', state: '1.8 GB', host: '4.0 KB' },
589
+ entries: [
590
+ {
591
+ area: 'tools',
592
+ name: 'rust',
593
+ path: 'C:\\AgentData\\tools\\rust',
594
+ bytes: 4.6e9,
595
+ size: '4.6 GB',
596
+ target: 'tools/rust',
597
+ },
598
+ {
599
+ area: 'state',
600
+ name: 'state/claude',
601
+ path: 'C:\\AgentData\\state\\claude',
602
+ bytes: 1.8e9,
603
+ size: '1.8 GB',
604
+ target: 'state/claude',
605
+ },
606
+ {
607
+ area: 'host',
608
+ name: 'accounts',
609
+ path: 'C:\\Users\\me\\AppData\\Local\\AgentSandbox\\accounts',
610
+ bytes: 4096,
611
+ size: '4.0 KB',
612
+ target: null,
613
+ },
614
+ ],
615
+ disks: [
616
+ {
617
+ drive: 'C:\\',
618
+ totalBytes: 40e9,
619
+ freeBytes: 22e9,
620
+ usedBytes: 18e9,
621
+ holdsStorage: true,
622
+ used: '18 GB',
623
+ usedPercent: 45,
624
+ free: '22 GB',
625
+ total: '40 GB',
626
+ },
627
+ ],
628
+ });
629
+ const dataSample = dataPage(stored);
630
+ const dataSectionIds = dataSample.sections.map((s) => s.id);
631
+ const dataOps = pageActions(dataSample);
632
+ const accountsItem = dataSample.sections.find((s) => s.id === 'area:host')?.items?.[0];
633
+ t.check(dataSectionIds.includes('area:tools') &&
634
+ dataSectionIds.includes('area:state') &&
635
+ dataOps.some((a) => a.kind === 'operation' && a.op === 'data.clean' && JSON.stringify(a.input) === '{"targets":["tools/rust"]}') &&
636
+ (accountsItem?.actions ?? []).length === 0 &&
637
+ dataOps.some((a) => a.kind === 'operation' && a.op === 'data.prune'), 'data: disks, one section per area, x deletes only what may be deleted', dataSectionIds.join());
638
+ const shaped = showData.cli.shapeJson(stored);
639
+ const shapedEntries = shaped.entries;
640
+ const shapedDisks = shaped.disks;
641
+ t.check(Object.keys(shaped).join() === 'dataPath,storageRoot,totalBytes,entries,disks' &&
642
+ Object.keys(shapedEntries[0]).join() === 'area,name,path,bytes' &&
643
+ Object.keys(shapedDisks[0]).join() === 'drive,totalBytes,freeBytes,usedBytes,holdsStorage', 'data show --json keeps its old shape');
644
+ const dataLines = pageLines(dataSample);
645
+ t.check(dataLines.some((l) => l.startsWith('Disks')) &&
646
+ dataLines.some((l) => /^DRIVE\s+USED\s+FREE\s+SIZE/.test(l)) &&
647
+ dataLines.some((l) => l.startsWith('Persistent data for the sandbox: ')) &&
648
+ dataLines.some((l) => l.startsWith('Free space with:')), 'data on the command line: the disks table, the data folder line and the free space line', dataLines.join('\n'));
649
+ t.check(definitionFor('data', ['clean'])?.id === 'data.clean' &&
650
+ definitionFor('data', ['show'])?.id === 'data.show' &&
651
+ JSON.stringify(cliInput(definitionFor('data', ['clean']), parseCli(['data', 'clean', 'tools/go', 'logs']))) ===
652
+ '{"targets":["tools/go","logs"]}' &&
653
+ JSON.stringify(cliInput(cleanData, parseCli(['data', 'remove', 'logs']))) === '{"targets":["logs"]}' &&
654
+ JSON.stringify(commandSpec('data')?.args[0].choices) === '["show","clean"]', 'data clean and remove are actions; the targets are the rest of the line');
655
+ t.equal(cleanData.next?.({ message: 'x', freedBytes: 5 }), 'sandbox data (see what is left)', 'data clean suggests what is left once something was freed');
656
+ const cleanOptions = commandSpec('clean')?.options ?? [];
657
+ t.check(cleanOptions.map((o) => [o.flag, o.short ?? '', o.type, o.description]).join('|') ===
658
+ '--days,,number,launch folders older than this many days|--yes,-y,boolean,do not ask before deleting' &&
659
+ cleanOptions.find((o) => o.flag === '--days')?.default === 7, 'clean keeps --days <n> (7) and --yes');
660
+ const dataRoot = t.dir('app-data', 'data');
661
+ const storageRoot = t.dir('app-data', 'storage');
662
+ const logsFolder = t.dir('app-data', 'data', 'logs');
663
+ t.write(path.join(logsFolder, 'old.log'), 'x'.repeat(100));
664
+ const dataCtx = { dataPath: dataRoot, storageRoot };
665
+ if (isSandboxRunning())
666
+ await t.throws(() => runDefinition('data.clean', dataCtx, { targets: ['logs'] }, fakeIO([]).io), 'A Windows Sandbox is running', 'data clean refuses while a sandbox runs');
667
+ else {
668
+ await t.throws(() => runDefinition('data.clean', dataCtx, { targets: ['logs'] }, fakeIO([false]).io), 'Nothing removed.', 'data clean: a no changes nothing');
669
+ t.check(fs.existsSync(path.join(logsFolder, 'old.log')), 'the logs are still there after a no');
670
+ const gone = (await runDefinition('data.clean', dataCtx, { targets: ['logs'] }, fakeIO([true]).io));
671
+ t.check(gone.message.startsWith('Removed logs and freed') && !fs.existsSync(logsFolder), 'data clean: yes removes the logs', gone.message);
672
+ }
673
+ const emptyCtx = {
674
+ storageRoot: t.dir('app-data', 'empty-storage'),
675
+ dataPath: t.dir('app-data', 'empty-data'),
676
+ };
677
+ t.equal(await runDefinition('data.prune', emptyCtx, {}, fakeIO([]).io), { message: 'Nothing to clean.', removed: false }, 'clean on an empty storage folder says so');
678
+ if (process.stdin.isTTY)
679
+ t.skip('a question without a terminal (this run has a terminal)');
680
+ else
681
+ await t.throws(() => cliIO(parseCli(['data'])).io.checklist({
682
+ title: 'x',
683
+ choices: [],
684
+ noTerminal: { message: 'Which data should be cleaned?', fix: 'f' },
685
+ }), 'Which data should be cleaned?', 'a question without a terminal fails with its own words');
686
+ t.section("the kernel's text blocks, view parameters and sizes");
687
+ const textPage = { title: 'Text', sections: [{ id: 'log', text: ['a', 'b'] }] };
688
+ t.equal(pageLines(textPage), ['a', 'b'], 'a text block is shown as its lines, in order, on the command line');
689
+ t.check(pageHasContent(textPage), 'a page whose only section is a text block has content');
690
+ t.check(bytesText(1536) === '1.5 KB' && bytesText(0) === '0 B' && bytesText(5 * 1024 ** 3) === '5.0 GB', 'bytesText says sizes the way humanBytes does', [bytesText(1536), bytesText(0), bytesText(5 * 1024 ** 3)].join(' | '));
691
+ t.section('updates: flags, input, the page');
692
+ const flagsOf = (name) => JSON.stringify(commandSpec(name)?.options.map((o) => [o.flag, o.short ?? '', o.description]));
693
+ t.check(flagsOf('update') ===
694
+ JSON.stringify([
695
+ ['--check', '', 'only look whether a newer version exists'],
696
+ ['--rollback', '', 'go back to the version before the last update'],
697
+ ['--auto', '', 'install new versions by themselves (for everyone on this PC: needs an admin terminal)'],
698
+ ['--unattended', '', 'used by automatic updates: no questions, short output'],
699
+ ['--feed', '', 'take the release from this folder (a feed.json and the package) instead of GitHub'],
700
+ ['--yes', '-y', '(--rollback) do not ask first'],
701
+ ]) &&
702
+ flagsOf('migrate') ===
703
+ JSON.stringify([
704
+ ['--dry-run', '', 'only list what would change'],
705
+ ['--restore', '', 'put back the files saved before an update (a folder under <storage>\\backups)'],
706
+ ['--yes', '-y', '(--restore) do not ask first'],
707
+ ]), 'update and migrate keep their flags and help (--yes moved last)');
708
+ t.equal(cliInput(definitionFor('update', []), parseCli(['update', '--auto', 'on'])), { auto: 'on' }, 'update --auto on reaches the input');
709
+ t.check(parseCli(['resume', '--auto']).auto === true, 'resume --auto is still a switch');
710
+ const brokenCfg = t.write(t.at('app-updates', 'config.json'), 'broken');
711
+ const updOptions = { ...parseCli(['update']), config: brokenCfg };
712
+ const updCtx = contextOrDefaults(t.dir('app-updates', 'package'), 'sandbox', updOptions);
713
+ t.check(JSON.stringify(updCtx.loaded.config) === JSON.stringify(DEFAULTS), 'update runs with a broken settings file (the built-in defaults)');
714
+ await t.throws(() => runDefinition('updates.update', { ...updCtx, options: { ...updCtx.options, feed: undefined } }, { feed: 'C:\\x' }, fakeIO().io), 'works only on the command line.', '--feed from the web is refused before anything is installed');
715
+ const updPage = (status, latest) => updatesPage(status, latest);
716
+ const managed = {
717
+ managed: true,
718
+ version: '1.4.3',
719
+ scope: 'user',
720
+ autoUpdate: true,
721
+ autoOffText: null,
722
+ previous: '1.4.2',
723
+ failed: null,
724
+ npmCommand: NPM_COMMAND,
725
+ error: null,
726
+ settings: { pending: ['2026-10-x: change'], error: null },
727
+ };
728
+ const onPage = updPage(managed, '1.5.0');
729
+ const updIds = onPage.sections.flatMap((s) => (s.items ?? []).map((i) => i.id));
730
+ const keys = menuView(onPage, { category: 'updates', refresh: false }).actions.map((a) => a.key);
731
+ t.check(['updates:version', 'updates:latest', 'updates:auto', 'updates:previous'].every((id) => updIds.includes(id)) &&
732
+ keys.join() === 'c,u,a,r,s', 'the updates page has its rows, and the keys c u a r s (with a pending migration)', keys.join());
733
+ const npmPage = updPage({ ...managed, managed: false, previous: null, settings: { pending: [], error: null } }, null);
734
+ t.check(menuView(npmPage, { category: 'updates', refresh: false }).actions.length === 0 &&
735
+ npmPage.sections[0].items?.some((i) => i.id === 'updates:npm') === true, 'an npm install shows how to update and offers no keys');
736
+ // The menu hands what an operation returned to its page once (the page keeps what a check found). A folder with no
737
+ // install in it is not managed, so this checks nothing on this PC.
738
+ const npmCtx = {
739
+ packageRoot: t.dir('app-updates', 'npm-package'),
740
+ options: {},
741
+ loaded: { config: structuredClone(DEFAULTS), configPath: null, base: t.temp },
742
+ storageRoot: t.dir('app-updates', 'storage'),
743
+ dataPath: t.dir('app-updates', 'data'),
744
+ };
745
+ const handed = [];
746
+ const menuSaid = await new MenuOperations({}, npmCtx).run(runUpdate, { check: false, rollback: false, unattended: false }, [], undefined, (o) => handed.push(o));
747
+ t.check(handed.length === 1 &&
748
+ handed[0].latest === null &&
749
+ (menuSaid ?? '').includes('npm install -g windows-development-sandbox'), 'the menu hands an operation its output once, and shows its line');
750
+ t.section('logs, run against temp folders');
751
+ const logsCfg = t.write(t.at('app-logs', 'config.json'), '{}\n');
752
+ const logsCtx = {
753
+ loaded: { config: structuredClone(DEFAULTS), configPath: logsCfg, base: t.temp },
754
+ options: { workspace: t.dir('app-logs', 'here') },
755
+ storageRoot: t.dir('app-logs', 'storage'),
756
+ packageRoot: t.dir('app-logs', 'package'),
757
+ dataPath: t.dir('app-logs', 'data'),
758
+ };
759
+ const logsDir = t.dir('app-logs', 'data', 'logs');
760
+ const guestLog = t.write(path.join(logsDir, 'guest-a.log'), Array.from({ length: 300 }, (_, i) => 'line ' + (i + 1)).join('\n') + '\n');
761
+ const daemonLog = t.write(path.join(logsDir, 'daemon-b.log'), 'd\n');
762
+ t.write(path.join(logsDir, 'notes.txt'), 'not a log\n');
763
+ t.touch(guestLog, -60);
764
+ t.touch(daemonLog);
765
+ const logList = await readQuery(listLogs, logsCtx);
766
+ t.check(logList.files.map((f) => f.name).join() === 'daemon-b.log,guest-a.log' &&
767
+ logList.folder === logsDir &&
768
+ !logList.crash, 'logs.list lists only the log files, newest first, and says there is no crash report yet', logList.files.map((f) => f.name).join());
769
+ const endOfLog = await readQuery(tailLog, logsCtx, { name: 'guest-a.log' });
770
+ t.check(endOfLog.name === 'guest-a.log' &&
771
+ endOfLog.file === guestLog &&
772
+ endOfLog.text.startsWith('line 182\n') &&
773
+ endOfLog.text.endsWith('line 300\n') &&
774
+ endOfLog.text.split('\n').length === 120, 'logs.tail shows the end of the log as the command always printed it (the last 120 pieces: 119 lines and the newline)', endOfLog.text.slice(0, 40));
775
+ fs.appendFileSync(guestLog, 'appended\n');
776
+ const more = await readQuery(tailLog, logsCtx, { name: 'guest-a.log', from: endOfLog.next });
777
+ t.equal(more.text, 'appended\n', 'logs.tail read on from next gives only the new text');
778
+ await t.throws(() => readQuery(tailLog, logsCtx, { name: '..\\config.json' }), 'There is no log called', 'a log name from outside cannot become a path of its own (..\\config.json)');
779
+ await t.throws(() => readQuery(tailLog, logsCtx, { name: 'C:\\Windows\\win.ini' }), 'There is no log called', 'a log name from outside cannot become a path of its own (a drive path)');
780
+ await t.throws(() => readQuery(tailLog, logsCtx, { name: CRASH }), 'No crash report yet', 'the reserved crash name with no report fails with the words the command line always printed');
781
+ const noLogsCtx = { ...logsCtx, dataPath: t.dir('app-logs', 'empty') };
782
+ const nothing = await readQuery(tailLog, noLogsCtx, {});
783
+ t.check(nothing.file === '' && nothing.text === '' && (await readQuery(listLogs, noLogsCtx)).files.length === 0, 'with no logs yet the tail is empty (the page says so; the command line fails with its words)');
784
+ const crashFile = t.write(path.join(logsCtx.storageRoot, 'logs', 'crashes', 'crash-20261014-101500.txt'), 'Crash.\n');
785
+ const withCrash = await readQuery(listLogs, logsCtx);
786
+ const crashTail = await readQuery(tailLog, logsCtx, { name: CRASH });
787
+ t.check(withCrash.crash && crashTail.file === crashFile && crashTail.text === 'Crash.\n' && crashTail.next === 0, 'logs.tail with the reserved name reads the last crash report, whole', crashTail.file);
788
+ const logsPageNow = logsPage(withCrash, endOfLog);
789
+ const itemIds = logsPageNow.sections[0].items?.map((i) => i.id) ?? [];
790
+ t.check(itemIds.join() === 'log:daemon-b.log,log:guest-a.log,log:' + CRASH &&
791
+ logsPageNow.sections[0].items?.find((i) => i.id === 'log:guest-a.log')?.badges?.[0]?.text === 'showing' &&
792
+ logsPageNow.sections[0].items?.find((i) => i.id === 'log:daemon-b.log')?.badges === undefined &&
793
+ logsPageNow.sections[1]?.text?.length === 119, 'the logs page: one row per log, the shown one badged, the crash report last, the end of the log as text', itemIds.join());
794
+ const logsMenuNow = menuView(logsPage(withCrash), { category: 'logs', refresh: false });
795
+ t.check(logsMenuNow.actions.map((a) => a.key).join() === 'f' &&
796
+ logsMenuNow.enterLabel('logs', 'log:guest-a.log') === 'show' &&
797
+ logsMenuNow.resolve({ kind: 'action', key: 'f', value: undefined })?.kind === 'action', 'the logs menu: the key f (follow in the terminal) and Enter shows', logsMenuNow.actions.map((a) => a.key).join());
798
+ t.section('storage');
799
+ t.check(definitionFor('storage', ['contained', 'on'])?.id === 'storage.containedOn' &&
800
+ definitionFor('storage', ['contained', 'status'])?.id === 'storage.containedStatus' &&
801
+ definitionFor('storage', ['move'])?.id === 'storage.move', 'storage contained on and off are two-word actions, storage move is one word');
802
+ const movedInput = cliInput(definitionFor('storage', ['move']), parseCli(['storage', 'move', 'D:\\x', '--keep-old']));
803
+ t.check(JSON.stringify(movedInput) === JSON.stringify({ folder: 'D:\\x', keepOld: true }), 'storage move reads its folder and --keep-old as its input', JSON.stringify(movedInput));
804
+ const storageActions = commandSpec('storage')?.args[0]?.choices;
805
+ t.check(Array.isArray(storageActions) && storageActions.join() === 'show,move,use,contained', 'storage keeps its actions as the first argument choices', String(storageActions));
806
+ const shownStorage = {
807
+ root: 'D:\\AgentSandbox',
808
+ exists: true,
809
+ holdsData: true,
810
+ measured: { bytes: 2048, files: 3, partial: false },
811
+ size: '2 KB',
812
+ defaultRoot: 'C:\\ProgramData\\AgentSandbox',
813
+ defaultInUse: false,
814
+ otherFoldersWithData: ['C:\\ProgramData\\AgentSandbox'],
815
+ };
816
+ const shapedStorage = showStorage.cli?.shapeJson?.(shownStorage);
817
+ t.check(Object.keys(shapedStorage).sort().join() === 'bytes,defaultRoot,exists,files,otherFoldersWithData,partial,root', 'storage show --json keeps its old shape', Object.keys(shapedStorage).join());
818
+ t.check(showStorage.text?.(shownStorage).startsWith('Your saved data is in: D:\\AgentSandbox\n 2 KB\n') === true, 'storage show prints its lines as before');
819
+ if (insideSandbox())
820
+ await t.throws(() => runDefinition('storage.move', { storageRoot: 'D:\\x' }, { folder: 'D:\\x' }, fakeIO([]).io), 'has nothing to control', 'storage refuses inside the sandbox');
821
+ else
822
+ t.skip('storage refuses inside the sandbox (this PC is not a sandbox)');
823
+ const yesIO = cliIO(parseCli(['storage', 'contained', 'off', '--yes'])).io;
824
+ t.check((await yesIO.confirm({ title: 'Delete the image file now?', skipWithYes: true })) === false, 'with --yes the image file is kept (the question is skipped)');
825
+ const shownContained = {
826
+ setting: 'off',
827
+ maxGb: 60,
828
+ available: true,
829
+ reasons: [],
830
+ fixes: [],
831
+ image: 'D:\\AgentSandbox\\contained\\agent.vhdx',
832
+ imageExists: false,
833
+ imageBytes: null,
834
+ mountFolder: 'D:\\AgentSandbox\\mount',
835
+ recordedMounted: false,
836
+ holders: 0,
837
+ mountedNow: null,
838
+ };
839
+ const storagePageNow = storagePage(shownStorage, shownContained);
840
+ const folderPrimary = storagePageNow.sections[0]?.items?.[0]?.primary;
841
+ const containedActions = storagePageNow.sections[1]?.actions ?? [];
842
+ t.check(folderPrimary?.kind === 'operation' &&
843
+ folderPrimary.op === 'storage.change' &&
844
+ containedActions.length === 1 &&
845
+ containedActions[0].kind === 'operation' &&
846
+ containedActions[0].op === 'storage.containedOn', 'the storage page: the data folder row changes it, and contained storage offers to turn it on while it is off');
847
+ t.check(pageActions(storagePageNow).every((a) => a.kind !== 'operation' || storageView.operations.some((o) => o.id === a.op)), 'the storage page offers only operations its view declares');
848
+ t.section('engine: the page, the check text, the exit code');
849
+ // Hand-made reports: this suite never runs the real checks (sc, whoami) that a Windows sandbox could not pass.
850
+ const caps = {
851
+ desktop: true,
852
+ gpu: true,
853
+ clipboard: true,
854
+ audio: true,
855
+ video: true,
856
+ printers: true,
857
+ liveFolderAdd: true,
858
+ attachedDisks: false,
859
+ multipleInstances: true,
860
+ hvsocket: false,
861
+ };
862
+ const missingHcs = {
863
+ id: 'hcs',
864
+ inUse: false,
865
+ capabilities: caps,
866
+ check: {
867
+ available: false,
868
+ reasons: ['computecore.dll is missing'],
869
+ fixes: ['Enable-WindowsOptionalFeature -Online -FeatureName Containers -All'],
870
+ },
871
+ };
872
+ const readyHcs = { ...missingHcs, check: { available: true, reasons: [], fixes: [] } };
873
+ const enginesNow = {
874
+ engine: 'windows-sandbox',
875
+ engines: [{ ...missingHcs, id: 'windows-sandbox', inUse: true, check: readyHcs.check }, missingHcs],
876
+ };
877
+ const enginePageNow = enginePage(enginesNow);
878
+ const engineLines = pageLines(enginePageNow);
879
+ t.check(engineLines[0] === 'Engine in use: windows-sandbox (setting: sandbox.engine)' &&
880
+ engineLines.includes(' Fix: Enable-WindowsOptionalFeature -Online -FeatureName Containers -All') &&
881
+ pageActions(enginePageNow).length === 0, 'engine page: the engine in use first, a card per engine, reasons and fixes, nothing to run', engineLines.slice(0, 4).join(' | '));
882
+ const unavailable = checkText({ ...missingHcs });
883
+ t.check(unavailable.includes('hcs cannot be used on this PC yet:') &&
884
+ unavailable.includes(' 1. computecore.dll is missing') &&
885
+ unavailable.includes(' To fix: Enable-WindowsOptionalFeature') &&
886
+ unavailable.includes('Nothing was changed.'), 'engine check text: the old words, numbered, with the fixes', unavailable);
887
+ t.check(checkText(readyHcs).includes('hcs can be used on this PC.') && checkText(readyHcs).includes('HCS is in alpha'), 'engine check text: an available hcs says it is in alpha');
888
+ t.check(checkEngine.cli?.exitCode?.(missingHcs) === 1 && checkEngine.cli?.exitCode?.(readyHcs) === 0, 'a CLI hint sets the exit code: engine check exits 1 when the engine cannot be used, 0 when it can');
889
+ t.check(definitionFor('engine', ['check'])?.id === 'engine.check' &&
890
+ definitionFor('engine', []) === undefined &&
891
+ ['show', 'check', 'leftovers', 'clean'].every((a) => commandSpec('engine')?.args[0]?.choices?.includes(a)), 'engine, check, leftovers and clean are the actions of the engine command; a bare engine is not an action');
892
+ const left = {
893
+ available: true,
894
+ message: '2 HCS systems of ours left running.',
895
+ systems: [
896
+ { id: 'agent-sandbox-1', kind: 'VirtualMachine', state: 'Running', since: '2026-01-01T10:00:00Z' },
897
+ { id: 'agent-sandbox-1-c', kind: 'Container', state: 'Running', since: null },
898
+ ],
899
+ };
900
+ const leftPage = enginePage(enginesNow, left);
901
+ t.check(ops(leftPage).join() === 'engine.clean' &&
902
+ pageActions(leftPage).some((a) => a.kind === 'operation' && a.danger) &&
903
+ pageLines(leftPage).some((l) => l.includes('agent-sandbox-1-c')) &&
904
+ ops(enginePage(enginesNow, { ...left, systems: [] })).length === 0 &&
905
+ menuView(leftPage).actions.some((a) => a.key === 'x') &&
906
+ definitionFor('engine', ['leftovers'])?.id === 'engine.leftovers' &&
907
+ definitionFor('engine', ['clean'])?.id === 'engine.clean', 'the engine page offers the (dangerous) clean action only when HCS systems of ours are left running');
908
+ t.section('web: CLI and menu only, never runnable from a browser');
909
+ const webDefs = DEFINITIONS.filter((d) => d.id.startsWith('web.'));
910
+ const webCtx = { storageRoot: t.dir('app-web') };
911
+ const webHost = createRpcHost(webCtx);
912
+ t.check(webDefs.length === 4 &&
913
+ webDefs.every((d) => exemption(d, 'web') === 'the page is the web UI') &&
914
+ webDefs.every((d) => webHost.kind(d.id) === undefined), 'every web definition is exempt from the web with its reason, and the RPC host refuses them all', webDefs.map((d) => d.id).join());
915
+ const webOff = webPage({ running: false, port: null, address: null });
916
+ const webOn = webPage({ running: true, port: 47800, address: 'http://127.0.0.1:47800/' });
917
+ const opsOf = (page) => pageActions(page)
918
+ .flatMap((a) => (a.kind === 'operation' ? [a.op] : []))
919
+ .join();
920
+ t.check(opsOf(webOff) === 'web.start,web.open' && opsOf(webOn) === 'web.open,web.stop', 'the web page offers start and open when stopped, open and stop when running', opsOf(webOff) + ' | ' + opsOf(webOn));
921
+ const webLines = pageLines(webOn);
922
+ t.check(webLines.some((l) => l.includes('Address') && l.includes('http://127.0.0.1:47800/')) &&
923
+ !webLines.some((l) => l.includes('#k=')), 'the web page shows the address without the login token', webLines.join(' / '));
924
+ t.check(menuView(webOn, { prefix: 'web:', refresh: false })
925
+ .actions.map((a) => a.key)
926
+ .join() === 'o,x', 'the Web UI category keys: o opens and x stops when running');
927
+ t.check(definitionFor('web', ['open'])?.id === 'web.open' &&
928
+ definitionFor('web', ['status'])?.id === 'web.status' &&
929
+ definitionFor('web', []) === undefined &&
930
+ commandSpec('web')?.args[0]?.choices?.join() === 'start,stop,status,open', 'the web actions are actions of the web command; sandbox web with no action is not one of them');
323
931
  },
324
932
  };