windows-development-sandbox 1.7.0 → 1.9.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 (413) hide show
  1. package/config.example.json +10 -0
  2. package/dist/app/context.d.ts +8 -0
  3. package/dist/app/context.js +12 -0
  4. package/dist/app/features/defs.d.ts +322 -0
  5. package/dist/app/features/defs.js +28 -0
  6. package/dist/app/features/sandbox/index.d.ts +2 -0
  7. package/dist/app/features/sandbox/index.js +3 -0
  8. package/dist/app/features/sandbox/sandbox.def.d.ts +193 -0
  9. package/dist/app/features/sandbox/sandbox.def.js +245 -0
  10. package/dist/app/features/sandbox/sandbox.run.d.ts +72 -0
  11. package/dist/app/features/sandbox/sandbox.run.js +98 -0
  12. package/dist/app/features/sessions/index.d.ts +2 -0
  13. package/dist/app/features/sessions/index.js +3 -0
  14. package/dist/app/features/sessions/sessions.def.d.ts +141 -0
  15. package/dist/app/features/sessions/sessions.def.js +176 -0
  16. package/dist/app/features/sessions/sessions.run.d.ts +39 -0
  17. package/dist/app/features/sessions/sessions.run.js +58 -0
  18. package/dist/{cli/menu/settings/categories.d.ts → app/features/settings/categories.def.d.ts} +2 -0
  19. package/dist/{cli/menu/settings/categories.js → app/features/settings/categories.def.js} +24 -5
  20. package/dist/app/features/settings/index.d.ts +5 -0
  21. package/dist/app/features/settings/index.js +6 -0
  22. package/dist/{cli/menu/settings/config-file.d.ts → app/features/settings/settings-file.d.ts} +1 -1
  23. package/dist/{cli/menu/settings/config-file.js → app/features/settings/settings-file.js} +1 -1
  24. package/dist/app/features/settings/settings-rows.d.ts +35 -0
  25. package/dist/app/features/settings/settings-rows.js +94 -0
  26. package/dist/app/features/settings/settings.def.d.ts +157 -0
  27. package/dist/app/features/settings/settings.def.js +179 -0
  28. package/dist/app/features/settings/settings.run.d.ts +67 -0
  29. package/dist/app/features/settings/settings.run.js +84 -0
  30. package/dist/app/kernel/define.d.ts +143 -0
  31. package/dist/app/kernel/define.js +42 -0
  32. package/dist/app/kernel/errors.d.ts +34 -0
  33. package/dist/app/kernel/errors.js +45 -0
  34. package/dist/app/kernel/fields.d.ts +70 -0
  35. package/dist/app/kernel/fields.js +243 -0
  36. package/dist/app/kernel/index.d.ts +8 -0
  37. package/dist/app/kernel/index.js +10 -0
  38. package/dist/app/kernel/io.d.ts +193 -0
  39. package/dist/app/kernel/io.js +126 -0
  40. package/dist/app/kernel/rpc.d.ts +156 -0
  41. package/dist/app/kernel/rpc.js +47 -0
  42. package/dist/app/kernel/text.d.ts +6 -0
  43. package/dist/app/kernel/text.js +57 -0
  44. package/dist/app/kernel/view.d.ts +146 -0
  45. package/dist/app/kernel/view.js +28 -0
  46. package/dist/app/registry.d.ts +10 -0
  47. package/dist/app/registry.js +50 -0
  48. package/dist/cli/args.d.ts +5 -0
  49. package/dist/cli/args.js +6 -0
  50. package/dist/cli/commands/engine.d.ts +2 -0
  51. package/dist/cli/commands/engine.js +60 -0
  52. package/dist/cli/commands/index.d.ts +1 -1
  53. package/dist/cli/commands/index.js +1 -1
  54. package/dist/cli/commands/launch/launch.js +47 -7
  55. package/dist/cli/commands/launch/prepare.js +3 -3
  56. package/dist/cli/commands/launch/running.js +2 -1
  57. package/dist/cli/commands/launch/summary.d.ts +2 -0
  58. package/dist/cli/commands/maintenance/config-share.d.ts +12 -0
  59. package/dist/cli/commands/maintenance/config-share.js +80 -0
  60. package/dist/cli/commands/maintenance/config.js +8 -82
  61. package/dist/cli/commands/maintenance/logs.js +14 -0
  62. package/dist/cli/commands/registry.js +3 -7
  63. package/dist/cli/commands/repair/checks/contained.d.ts +2 -0
  64. package/dist/cli/commands/repair/checks/contained.js +139 -0
  65. package/dist/cli/commands/repair/checks/hcs.d.ts +7 -0
  66. package/dist/cli/commands/repair/checks/hcs.js +28 -0
  67. package/dist/cli/commands/repair/checks/index.js +4 -0
  68. package/dist/cli/commands/repair/checks/pc.js +32 -1
  69. package/dist/cli/commands/repair/checks/sandbox.js +16 -1
  70. package/dist/cli/commands/repair/checks/vpn.js +34 -9
  71. package/dist/cli/commands/repair/env.js +2 -1
  72. package/dist/cli/commands/sessions/index.d.ts +0 -4
  73. package/dist/cli/commands/sessions/index.js +3 -7
  74. package/dist/cli/commands/shared-use.d.ts +5 -2
  75. package/dist/cli/commands/shared-use.js +11 -8
  76. package/dist/cli/commands/storage/contained.d.ts +3 -0
  77. package/dist/cli/commands/storage/contained.js +179 -0
  78. package/dist/cli/commands/storage/offer.js +7 -1
  79. package/dist/cli/commands/storage/storage.js +10 -2
  80. package/dist/cli/commands/user-config.js +1 -1
  81. package/dist/cli/commands/vpn.js +19 -3
  82. package/dist/cli/commands/web.js +2 -0
  83. package/dist/cli/index.js +2 -1
  84. package/dist/cli/manifest.d.ts +72 -104
  85. package/dist/cli/manifest.js +64 -39
  86. package/dist/cli/menu/add-agent.d.ts +1 -1
  87. package/dist/cli/menu/add-agent.js +1 -1
  88. package/dist/cli/menu/apps.d.ts +7 -1
  89. package/dist/cli/menu/apps.js +36 -0
  90. package/dist/cli/menu/command-fields.d.ts +1 -1
  91. package/dist/cli/menu/command-form.js +2 -2
  92. package/dist/cli/menu/io-menu.d.ts +29 -0
  93. package/dist/cli/menu/io-menu.js +126 -0
  94. package/dist/cli/menu/live.d.ts +27 -0
  95. package/dist/cli/menu/live.js +69 -0
  96. package/dist/cli/menu/memory.d.ts +19 -0
  97. package/dist/cli/menu/memory.js +53 -0
  98. package/dist/cli/menu/menu.d.ts +10 -1
  99. package/dist/cli/menu/menu.js +27 -9
  100. package/dist/cli/menu/pages/agents-page.d.ts +3 -1
  101. package/dist/cli/menu/pages/agents-page.js +12 -5
  102. package/dist/cli/menu/pages/apps-page.d.ts +3 -1
  103. package/dist/cli/menu/pages/apps-page.js +19 -2
  104. package/dist/cli/menu/pages/list-page.d.ts +27 -0
  105. package/dist/cli/menu/pages/list-page.js +109 -7
  106. package/dist/cli/menu/pages/maintenance-page.d.ts +0 -5
  107. package/dist/cli/menu/pages/maintenance-page.js +17 -19
  108. package/dist/cli/menu/pages/sandbox-page.d.ts +4 -6
  109. package/dist/cli/menu/pages/sandbox-page.js +26 -118
  110. package/dist/cli/menu/pages/scopes.d.ts +1 -1
  111. package/dist/cli/menu/pages/sessions-page.d.ts +5 -11
  112. package/dist/cli/menu/pages/sessions-page.js +19 -88
  113. package/dist/cli/menu/pages/settings-page.d.ts +2 -10
  114. package/dist/cli/menu/pages/settings-page.js +64 -94
  115. package/dist/cli/menu/render-view.d.ts +28 -0
  116. package/dist/cli/menu/render-view.js +112 -0
  117. package/dist/cli/menu/settings/{path-kind.d.ts → ask-path.d.ts} +1 -7
  118. package/dist/cli/menu/settings/ask-path.js +14 -0
  119. package/dist/cli/menu/settings/collections/collection-editor.d.ts +1 -1
  120. package/dist/cli/menu/settings/collections/string-list-editor.d.ts +1 -2
  121. package/dist/cli/menu/settings/collections/string-list-editor.js +1 -1
  122. package/dist/cli/menu/settings/draft.d.ts +1 -1
  123. package/dist/cli/menu/settings/format.d.ts +1 -3
  124. package/dist/cli/menu/settings/format.js +6 -36
  125. package/dist/cli/menu/settings/index.d.ts +1 -3
  126. package/dist/cli/menu/settings/index.js +3 -3
  127. package/dist/cli/menu/settings/object-editor.d.ts +1 -1
  128. package/dist/cli/menu/settings/share.d.ts +36 -0
  129. package/dist/cli/menu/settings/share.js +131 -0
  130. package/dist/cli/menu/settings/value-editor.d.ts +5 -1
  131. package/dist/cli/menu/settings/value-editor.js +57 -54
  132. package/dist/cli/menu/several.js +1 -1
  133. package/dist/cli/menu/share-settings.d.ts +21 -0
  134. package/dist/cli/menu/share-settings.js +112 -0
  135. package/dist/cli/menu/shell.d.ts +21 -5
  136. package/dist/cli/menu/shell.js +31 -6
  137. package/dist/cli/menu/views.d.ts +7 -0
  138. package/dist/cli/menu/views.js +11 -0
  139. package/dist/cli/menu/vpn-login.js +8 -0
  140. package/dist/cli/operations/handler.d.ts +6 -0
  141. package/dist/cli/operations/handler.js +61 -0
  142. package/dist/cli/operations/index.d.ts +4 -0
  143. package/dist/cli/operations/index.js +5 -0
  144. package/dist/cli/operations/input.d.ts +6 -0
  145. package/dist/cli/operations/input.js +37 -0
  146. package/dist/cli/operations/io-cli.d.ts +7 -0
  147. package/dist/cli/operations/io-cli.js +62 -0
  148. package/dist/cli/operations/spec.d.ts +18 -0
  149. package/dist/cli/operations/spec.js +108 -0
  150. package/dist/cli/output/colors.d.ts +2 -2
  151. package/dist/cli/output/colors.js +2 -11
  152. package/dist/cli/output/index.d.ts +1 -0
  153. package/dist/cli/output/index.js +1 -0
  154. package/dist/cli/output/print.d.ts +3 -1
  155. package/dist/cli/output/print.js +7 -3
  156. package/dist/cli/output/progress.d.ts +15 -0
  157. package/dist/cli/output/progress.js +44 -0
  158. package/dist/cli/output/render-view.d.ts +4 -0
  159. package/dist/cli/output/render-view.js +63 -0
  160. package/dist/cli/services/agent-commands-service.d.ts +2 -0
  161. package/dist/cli/services/agent-commands-service.js +11 -2
  162. package/dist/cli/services/logs-service.js +4 -3
  163. package/dist/cli/tui/app.d.ts +6 -1
  164. package/dist/cli/tui/app.js +26 -4
  165. package/dist/cli/tui/list/list-screen.d.ts +14 -0
  166. package/dist/cli/tui/list/list-screen.js +64 -36
  167. package/dist/cli/tui/list/list-types.d.ts +2 -0
  168. package/dist/cli/tui/terminal.d.ts +7 -0
  169. package/dist/cli/tui/terminal.js +30 -10
  170. package/dist/core/config/defaults.js +8 -0
  171. package/dist/core/schema/config.d.ts +13 -0
  172. package/dist/core/schema/config.js +16 -1
  173. package/dist/core/schema/harness.d.ts +1 -0
  174. package/dist/core/schema/harness.js +5 -0
  175. package/dist/core/schema/sandbox.d.ts +6 -0
  176. package/dist/core/schema/sandbox.js +20 -3
  177. package/dist/core/schema/vpn.d.ts +2 -1
  178. package/dist/core/schema/vpn.js +3 -3
  179. package/dist/core/types.d.ts +23 -5
  180. package/dist/core/util/fs.d.ts +2 -0
  181. package/dist/core/util/fs.js +9 -0
  182. package/dist/core/util/index.d.ts +1 -1
  183. package/dist/core/util/index.js +1 -1
  184. package/dist/guest/agents/agent-files.d.ts +5 -0
  185. package/dist/guest/agents/agent-files.js +4 -0
  186. package/dist/guest/agents/local-state.d.ts +2 -0
  187. package/dist/guest/agents/local-state.js +21 -1
  188. package/dist/guest/boot/boot.js +2 -1
  189. package/dist/guest/daemon/daemon.js +4 -0
  190. package/dist/guest/daemon/sessions.d.ts +5 -0
  191. package/dist/guest/daemon/sessions.js +17 -3
  192. package/dist/guest/entries/daemon.js +3 -0
  193. package/dist/guest/governor/decide.d.ts +61 -0
  194. package/dist/guest/governor/decide.js +182 -0
  195. package/dist/guest/governor/governor.d.ts +42 -0
  196. package/dist/guest/governor/governor.js +214 -0
  197. package/dist/guest/governor/index.d.ts +2 -0
  198. package/dist/guest/governor/index.js +2 -0
  199. package/dist/guest/runtime/csc.d.ts +1 -0
  200. package/dist/guest/runtime/csc.js +1 -0
  201. package/dist/guest/sessions/viewer.js +3 -1
  202. package/dist/guest/toolchains/native/rust-extras.js +4 -2
  203. package/dist/guest/vpn/vpn.js +69 -21
  204. package/dist/guest/vpn/wireguard.d.ts +32 -0
  205. package/dist/guest/vpn/wireguard.js +132 -0
  206. package/dist/host/agent-commands/commands.d.ts +4 -0
  207. package/dist/host/agent-commands/commands.js +31 -7
  208. package/dist/host/audit/log.d.ts +1 -1
  209. package/dist/host/channel/attach.d.ts +3 -1
  210. package/dist/host/channel/attach.js +5 -2
  211. package/dist/host/channel/channel.d.ts +2 -0
  212. package/dist/host/channel/channel.js +4 -0
  213. package/dist/host/engines/engine.d.ts +12 -0
  214. package/dist/host/engines/engine.js +1 -0
  215. package/dist/host/engines/hcs/check.d.ts +25 -0
  216. package/dist/host/engines/hcs/check.js +95 -0
  217. package/dist/host/engines/hcs/documents.d.ts +44 -0
  218. package/dist/host/engines/hcs/documents.js +155 -0
  219. package/dist/host/engines/hcs/engine.d.ts +39 -0
  220. package/dist/host/engines/hcs/engine.js +171 -0
  221. package/dist/host/engines/hcs/helper.d.ts +43 -0
  222. package/dist/host/engines/hcs/helper.js +173 -0
  223. package/dist/host/engines/hcs/images.d.ts +37 -0
  224. package/dist/host/engines/hcs/images.js +152 -0
  225. package/dist/host/engines/index.d.ts +7 -0
  226. package/dist/host/engines/index.js +5 -0
  227. package/dist/host/engines/registry.d.ts +16 -0
  228. package/dist/host/engines/registry.js +30 -0
  229. package/dist/host/engines/types.d.ts +58 -0
  230. package/dist/host/engines/types.js +1 -0
  231. package/dist/host/engines/windows-sandbox/engine.d.ts +16 -0
  232. package/dist/host/engines/windows-sandbox/engine.js +63 -0
  233. package/dist/host/engines/windows-sandbox/wsb-text.d.ts +2 -0
  234. package/dist/host/engines/windows-sandbox/wsb-text.js +29 -0
  235. package/dist/host/launch/generate.d.ts +5 -1
  236. package/dist/host/launch/generate.js +50 -8
  237. package/dist/host/launch/index.d.ts +3 -1
  238. package/dist/host/launch/index.js +3 -1
  239. package/dist/host/launch/launch-plan.d.ts +13 -0
  240. package/dist/host/launch/launch-plan.js +26 -0
  241. package/dist/host/launch/mappings.js +2 -1
  242. package/dist/host/launch/risk-notes.d.ts +10 -0
  243. package/dist/host/launch/risk-notes.js +18 -0
  244. package/dist/host/launch/session-file.d.ts +2 -0
  245. package/dist/host/launch/session-file.js +7 -1
  246. package/dist/host/launch/wsb-file.d.ts +28 -4
  247. package/dist/host/launch/wsb-file.js +61 -42
  248. package/dist/host/life/crash-report.d.ts +27 -1
  249. package/dist/host/life/crash-report.js +173 -7
  250. package/dist/host/life/host-memory.d.ts +40 -0
  251. package/dist/host/life/host-memory.js +137 -0
  252. package/dist/host/life/index.d.ts +2 -0
  253. package/dist/host/life/index.js +2 -0
  254. package/dist/host/life/stop-events.d.ts +2 -0
  255. package/dist/host/life/stop-events.js +20 -3
  256. package/dist/host/life/stop-warning.js +1 -1
  257. package/dist/host/life/watchdog.d.ts +54 -0
  258. package/dist/host/life/watchdog.js +108 -0
  259. package/dist/host/native.d.ts +3 -2
  260. package/dist/host/native.js +22 -3
  261. package/dist/host/profile/claude-login.js +3 -2
  262. package/dist/host/sandbox/contained-deps.d.ts +2 -0
  263. package/dist/host/sandbox/contained-deps.js +17 -0
  264. package/dist/host/sandbox/index.d.ts +2 -0
  265. package/dist/host/sandbox/index.js +2 -0
  266. package/dist/host/sandbox/platform.d.ts +2 -0
  267. package/dist/host/sandbox/platform.js +7 -7
  268. package/dist/host/sandbox/tasklist.d.ts +13 -0
  269. package/dist/host/sandbox/tasklist.js +47 -0
  270. package/dist/host/secrets/launch-secrets.js +2 -1
  271. package/dist/host/sharing/current.js +2 -6
  272. package/dist/host/storage/contained/check.d.ts +9 -0
  273. package/dist/host/storage/contained/check.js +37 -0
  274. package/dist/host/storage/contained/contained-storage.d.ts +66 -0
  275. package/dist/host/storage/contained/contained-storage.js +168 -0
  276. package/dist/host/storage/contained/disk-port.d.ts +43 -0
  277. package/dist/host/storage/contained/disk-port.js +122 -0
  278. package/dist/host/storage/contained/index.d.ts +9 -0
  279. package/dist/host/storage/contained/index.js +8 -0
  280. package/dist/host/storage/contained/layout.d.ts +28 -0
  281. package/dist/host/storage/contained/layout.js +45 -0
  282. package/dist/host/storage/contained/resolve.d.ts +4 -0
  283. package/dist/host/storage/contained/resolve.js +30 -0
  284. package/dist/host/storage/contained/setting.d.ts +1 -0
  285. package/dist/host/storage/contained/setting.js +5 -0
  286. package/dist/host/storage/contained/state.d.ts +89 -0
  287. package/dist/host/storage/contained/state.js +166 -0
  288. package/dist/host/storage/contained/transfer.d.ts +24 -0
  289. package/dist/host/storage/contained/transfer.js +121 -0
  290. package/dist/host/storage/index.d.ts +2 -0
  291. package/dist/host/storage/index.js +2 -0
  292. package/dist/host/storage/providers/contained.d.ts +15 -0
  293. package/dist/host/storage/providers/contained.js +39 -0
  294. package/dist/host/storage/providers/folder.d.ts +12 -0
  295. package/dist/host/storage/providers/folder.js +20 -0
  296. package/dist/host/storage/providers/index.d.ts +4 -0
  297. package/dist/host/storage/providers/index.js +3 -0
  298. package/dist/host/storage/providers/registry.d.ts +9 -0
  299. package/dist/host/storage/providers/registry.js +14 -0
  300. package/dist/host/storage/providers/types.d.ts +33 -0
  301. package/dist/host/storage/providers/types.js +1 -0
  302. package/dist/host/tools/conpty.js +3 -2
  303. package/dist/host/tools/windows-terminal.js +8 -7
  304. package/dist/host/vpn/index.d.ts +1 -1
  305. package/dist/host/vpn/index.js +1 -1
  306. package/dist/host/vpn/prepare.d.ts +2 -0
  307. package/dist/host/vpn/prepare.js +63 -2
  308. package/dist/host/web/browser.d.ts +2 -0
  309. package/dist/host/web/browser.js +14 -1
  310. package/dist/host/web/index.d.ts +3 -2
  311. package/dist/host/web/index.js +3 -2
  312. package/dist/host/web/rpc.d.ts +24 -0
  313. package/dist/host/web/rpc.js +149 -0
  314. package/dist/host/web/runner.d.ts +2 -0
  315. package/dist/host/web/runner.js +1 -2
  316. package/dist/host/web/server.d.ts +7 -0
  317. package/dist/host/web/server.js +23 -3
  318. package/dist/runtime/agent.js +69 -8
  319. package/dist/runtime/daemon.js +1058 -624
  320. package/dist/runtime/db.js +69 -8
  321. package/dist/runtime/main.js +624 -182
  322. package/dist/runtime/netproxy.js +69 -8
  323. package/dist/runtime/view.js +87 -13
  324. package/dist/selftest/agent-paths.test.js +11 -1
  325. package/dist/selftest/app.test.d.ts +2 -0
  326. package/dist/selftest/app.test.js +324 -0
  327. package/dist/selftest/channel.test.js +13 -0
  328. package/dist/selftest/cli.test.js +2 -1
  329. package/dist/selftest/config.test.js +59 -0
  330. package/dist/selftest/contained.test.d.ts +2 -0
  331. package/dist/selftest/contained.test.js +544 -0
  332. package/dist/selftest/credentials.test.js +1 -1
  333. package/dist/selftest/engine.test.d.ts +2 -0
  334. package/dist/selftest/engine.test.js +127 -0
  335. package/dist/selftest/hcs.test.d.ts +2 -0
  336. package/dist/selftest/hcs.test.js +482 -0
  337. package/dist/selftest/heavy-load.test.d.ts +2 -0
  338. package/dist/selftest/heavy-load.test.js +329 -0
  339. package/dist/selftest/index.js +12 -0
  340. package/dist/selftest/layout.test.js +12 -0
  341. package/dist/selftest/menu-perf.test.d.ts +2 -0
  342. package/dist/selftest/menu-perf.test.js +248 -0
  343. package/dist/selftest/menu-screens.js +4 -2
  344. package/dist/selftest/menu.test.js +36 -8
  345. package/dist/selftest/support/engine-cases.d.ts +8 -0
  346. package/dist/selftest/support/engine-cases.js +65 -0
  347. package/dist/selftest/support/engine-golden.d.ts +3 -0
  348. package/dist/selftest/support/engine-golden.js +66 -0
  349. package/dist/selftest/support/fixtures.d.ts +7 -1
  350. package/dist/selftest/support/hcs-probes.d.ts +17 -0
  351. package/dist/selftest/support/hcs-probes.js +41 -0
  352. package/dist/selftest/vpn.test.js +136 -3
  353. package/dist/selftest/web.test.js +101 -3
  354. package/dist/shared/channel/run-dir.d.ts +4 -1
  355. package/dist/shared/channel/run-dir.js +7 -1
  356. package/dist/shared/channel/schema.d.ts +35 -1
  357. package/dist/shared/channel/schema.js +27 -0
  358. package/dist/shared/channel/secrets.d.ts +1 -0
  359. package/dist/shared/channel/secrets.js +6 -0
  360. package/dist/shared/memory/index.d.ts +1 -0
  361. package/dist/shared/memory/index.js +1 -0
  362. package/dist/shared/memory/policy.d.ts +33 -0
  363. package/dist/shared/memory/policy.js +55 -0
  364. package/dist/shared/pty/output-filter.d.ts +14 -1
  365. package/dist/shared/pty/output-filter.js +22 -4
  366. package/dist/shared/pty/terminal.d.ts +4 -1
  367. package/dist/shared/pty/terminal.js +4 -2
  368. package/dist/shared/system.d.ts +1 -0
  369. package/dist/shared/system.js +1 -0
  370. package/dist/shared/vpn/download.d.ts +16 -0
  371. package/dist/shared/vpn/download.js +16 -0
  372. package/dist/shared/vpn/wireguard.d.ts +51 -0
  373. package/dist/shared/vpn/wireguard.js +227 -0
  374. package/dist/sync/sync-cycle.js +5 -5
  375. package/dist/sync/watcher.d.ts +13 -0
  376. package/dist/sync/watcher.js +92 -3
  377. package/dist/web/index.html +4 -3
  378. package/dist/web/main-NRRS5O6Z.js +157 -0
  379. package/dist/web/main-T5DMCHYV.css +1 -0
  380. package/native/csharp/HcsHost/AssemblyInfo.cs +14 -0
  381. package/native/csharp/HcsHost/Guard.cs +100 -0
  382. package/native/csharp/HcsHost/Hcs.cs +256 -0
  383. package/native/csharp/HcsHost/Json.cs +78 -0
  384. package/native/csharp/HcsHost/Program.cs +185 -0
  385. package/native/csharp/HcsHost/VirtualDisk.cs +95 -0
  386. package/native/csharp/HostTools/AssemblyInfo.cs +14 -0
  387. package/native/csharp/HostTools/MemoryStatus.cs +107 -0
  388. package/native/csharp/HostTools/Program.cs +58 -17
  389. package/native/scripts/check-contained.ps1 +28 -0
  390. package/native/scripts/contained-common.ps1 +241 -0
  391. package/native/scripts/dismount-contained.ps1 +66 -0
  392. package/native/scripts/memory-status.ps1 +34 -0
  393. package/native/scripts/mount-contained.ps1 +139 -0
  394. package/native/scripts/run-elevated.ps1 +17 -4
  395. package/native/scripts/stop-events.ps1 +23 -8
  396. package/package.json +9 -2
  397. package/sandbox/csharp/MemGov.cs +398 -0
  398. package/sandbox/csharp/PtyHost.cs +111 -5
  399. package/schemas/config.schema.json +60 -6
  400. package/schemas/sandbox-config.schema.json +3 -3
  401. package/dist/cli/commands/sessions/control.d.ts +0 -4
  402. package/dist/cli/commands/sessions/control.js +0 -66
  403. package/dist/cli/commands/sessions/list.d.ts +0 -2
  404. package/dist/cli/commands/sessions/list.js +0 -48
  405. package/dist/cli/commands/sessions/status.d.ts +0 -4
  406. package/dist/cli/commands/sessions/status.js +0 -114
  407. package/dist/cli/commands/sessions/window.d.ts +0 -3
  408. package/dist/cli/commands/sessions/window.js +0 -18
  409. package/dist/cli/menu/settings/path-kind.js +0 -44
  410. package/dist/web/main-CXESL347.js +0 -45
  411. package/dist/web/main-VLR7DVQ4.css +0 -1
  412. /package/dist/{cli/menu/settings → app/kernel}/schema-node.d.ts +0 -0
  413. /package/dist/{cli/menu/settings → app/kernel}/schema-node.js +0 -0
@@ -1,4 +1,2 @@
1
- export { CATEGORIES, categoryOf, nameOf, SETTINGS_ELSEWHERE } from './categories.js';
2
- export { ConfigFile, editConfigText, fileValue, type SettingChange, type SettingsTarget } from './config-file.js';
3
1
  export { Draft, type DraftResult } from './draft.js';
4
- export { SchemaNode } from './schema-node.js';
2
+ export { diffText, exportSettings, planImport, skippedLine, type DiffLine, type ImportPlan, type Pruned, type Skipped, } from './share.js';
@@ -1,4 +1,4 @@
1
- export { CATEGORIES, categoryOf, nameOf, SETTINGS_ELSEWHERE } from './categories.js';
2
- export { ConfigFile, editConfigText, fileValue } from './config-file.js';
1
+ // The menu's settings editing: staged changes (Ctrl+S saves them), sharing settings, and the editors for each kind of
2
+ // value. What the settings are, their files and rows live in the settings feature (src/app/features/settings).
3
3
  export { Draft } from './draft.js';
4
- export { SchemaNode } from './schema-node.js';
4
+ export { diffText, exportSettings, planImport, skippedLine, } from './share.js';
@@ -1,5 +1,5 @@
1
1
  import { type App } from '../../tui/index.js';
2
- import { type SchemaNode } from './schema-node.js';
2
+ import type { SchemaNode } from '../../../app/kernel/index.js';
3
3
  import type { ValueEditor, Edited } from './value-editor.js';
4
4
  /** One object as a form: each field with its value, Enter to change it, r to clear an optional one. */
5
5
  export declare class ObjectEditor {
@@ -0,0 +1,36 @@
1
+ import type { Config } from '../../../core/types.js';
2
+ import type { SettingChange } from '../../../app/features/settings/index.js';
3
+ /** Why something was left out. */
4
+ export interface Skipped {
5
+ readonly key: string;
6
+ readonly why: 'secret' | 'path' | 'machine';
7
+ }
8
+ export interface Pruned {
9
+ readonly data: Record<string, unknown>;
10
+ readonly skipped: Skipped[];
11
+ }
12
+ /** One line for a skipped setting. */
13
+ export declare function skippedLine(s: Skipped): string;
14
+ /** Your settings as a shareable object: only what differs from the defaults, without secrets or (by default) paths. */
15
+ export declare function exportSettings(config: Config, withPaths: boolean): Pruned;
16
+ /** One changed setting in a diff: old -> new. */
17
+ export interface DiffLine {
18
+ readonly key: string[];
19
+ readonly from: unknown;
20
+ readonly to: unknown;
21
+ }
22
+ export interface ImportPlan {
23
+ /** What would change (empty: nothing). */
24
+ readonly changes: SettingChange[];
25
+ readonly lines: DiffLine[];
26
+ readonly skipped: Skipped[];
27
+ /** Why the file cannot be imported at all (not JSON, not settings). */
28
+ readonly problems: string[];
29
+ }
30
+ /**
31
+ * Reads a shared settings file and works out what importing it would do to `current` (your settings with defaults).
32
+ * Pure: nothing is written.
33
+ */
34
+ export declare function planImport(text: string, current: Config): ImportPlan;
35
+ /** The diff as plain lines: `key: old -> new`. */
36
+ export declare function diffText(lines: readonly DiffLine[]): string[];
@@ -0,0 +1,131 @@
1
+ // Sharing your settings: the part of them that differs from the defaults, as a .json a team can pass around, and the
2
+ // reverse (read a file, show what it would change, apply the rest). Secrets (anything the schema marks), the data
3
+ // folder and folders or files of this PC never go into a file and are never taken from one.
4
+ import { parse } from 'jsonc-parser';
5
+ import _ from 'lodash';
6
+ import { DEFAULTS, mergeConfig } from '../../../core/config/index.js';
7
+ import { ConfigSchema, describeIssues, secretPaths } from '../../../core/schema/index.js';
8
+ import { pathKindOf } from '../../../app/kernel/index.js';
9
+ /** Settings that are about this PC and never travel (the data folder). */
10
+ const MACHINE_ONLY = ['storage.root'];
11
+ /** A folder or file of this PC: a drive path, a network path, a rooted path or a %VARIABLE% path. */
12
+ const ABSOLUTE = /^([a-zA-Z]:[\\/]|\\\\|\/|%)/;
13
+ const WHY_TEXT = {
14
+ secret: 'a secret, never shared',
15
+ path: 'a folder or file of this PC',
16
+ machine: 'this PC only',
17
+ };
18
+ /** One line for a skipped setting. */
19
+ export function skippedLine(s) {
20
+ return s.key + ' (' + WHY_TEXT[s.why] + ')';
21
+ }
22
+ const isSecret = (trail) => secretPaths().some((p) => p.length === trail.length && p.every((part, i) => part === '*' || part === trail[i]));
23
+ /**
24
+ * `value` without secrets, machine-only settings and (unless `withPaths`) folders and files of this PC. A list item
25
+ * that holds such a path is dropped whole. `trail` is the key path, with * for a list item.
26
+ */
27
+ function prune(value, trail, withPaths, skipped) {
28
+ const name = () => trail.join('.').replace(/\.\*/g, '[]');
29
+ if (isSecret(trail)) {
30
+ skipped.push({ key: name(), why: 'secret' });
31
+ return undefined;
32
+ }
33
+ if (MACHINE_ONLY.includes(trail.join('.'))) {
34
+ skipped.push({ key: name(), why: 'machine' });
35
+ return undefined;
36
+ }
37
+ const isPath = !withPaths && pathKindOf(trail.filter((p) => p !== '*')) !== undefined;
38
+ if (typeof value === 'string') {
39
+ if (isPath && ABSOLUTE.test(value)) {
40
+ skipped.push({ key: name(), why: 'path' });
41
+ return undefined;
42
+ }
43
+ return value;
44
+ }
45
+ if (Array.isArray(value)) {
46
+ const kept = [];
47
+ for (const item of value) {
48
+ const before = skipped.length;
49
+ const out = prune(item, [...trail, '*'], withPaths, skipped);
50
+ // A list item that lost a path (a folder entry without its folder) means nothing any more.
51
+ const lostPath = skipped.slice(before).some((x) => x.why !== 'secret');
52
+ if (out !== undefined && !lostPath)
53
+ kept.push(out);
54
+ }
55
+ return kept.length || value.length === 0 ? kept : undefined;
56
+ }
57
+ if (_.isPlainObject(value)) {
58
+ const out = {};
59
+ for (const [k, v] of Object.entries(value)) {
60
+ const next = prune(v, [...trail, k], withPaths, skipped);
61
+ if (next !== undefined)
62
+ out[k] = next;
63
+ }
64
+ return _.isEmpty(out) && !_.isEmpty(value) ? undefined : out;
65
+ }
66
+ return value;
67
+ }
68
+ /** What `value` has that `base` does not (undefined when nothing). Lists and plain values are compared whole. */
69
+ function differences(value, base) {
70
+ if (_.isPlainObject(value) && _.isPlainObject(base)) {
71
+ const out = {};
72
+ for (const [k, v] of Object.entries(value)) {
73
+ const d = differences(v, base[k]);
74
+ if (d !== undefined)
75
+ out[k] = d;
76
+ }
77
+ return _.isEmpty(out) ? undefined : out;
78
+ }
79
+ return _.isEqual(value, base) ? undefined : value;
80
+ }
81
+ /** Your settings as a shareable object: only what differs from the defaults, without secrets or (by default) paths. */
82
+ export function exportSettings(config, withPaths) {
83
+ const skipped = [];
84
+ const diff = differences(config, DEFAULTS) ?? {};
85
+ const data = (prune(diff, [], withPaths, skipped) ?? {});
86
+ return { data, skipped };
87
+ }
88
+ /** The leaves of an object as key paths; lists and plain values are leaves. */
89
+ function leaves(value, key = []) {
90
+ if (_.isPlainObject(value) && !_.isEmpty(value))
91
+ return Object.entries(value).flatMap(([k, v]) => leaves(v, [...key, k]));
92
+ return key.length ? [{ key, value }] : [];
93
+ }
94
+ /**
95
+ * Reads a shared settings file and works out what importing it would do to `current` (your settings with defaults).
96
+ * Pure: nothing is written.
97
+ */
98
+ export function planImport(text, current) {
99
+ const none = { changes: [], lines: [], skipped: [] };
100
+ const errors = [];
101
+ const parsed = parse(text, errors, { allowTrailingComma: true });
102
+ if (errors.length || !_.isPlainObject(parsed))
103
+ return { ...none, problems: ['The file is not a settings file (a JSON object is expected).'] };
104
+ const raw = _.omit(parsed, '$schema');
105
+ const skipped = [];
106
+ const safe = (prune(raw, [], false, skipped) ?? {});
107
+ const checked = ConfigSchema.safeParse(mergeConfig(DEFAULTS, safe));
108
+ if (!checked.success)
109
+ return { ...none, skipped, problems: describeIssues(checked.error) };
110
+ const changes = [];
111
+ const lines = [];
112
+ for (const { key, value } of leaves(safe)) {
113
+ const from = _.get(current, key);
114
+ if (_.isEqual(from, value))
115
+ continue;
116
+ changes.push({ key, value });
117
+ lines.push({ key, from, to: value });
118
+ }
119
+ return { changes, lines, skipped, problems: [] };
120
+ }
121
+ /** A value as one short piece of text for a diff. */
122
+ function show(value) {
123
+ if (value === undefined)
124
+ return '(not set)';
125
+ const text = JSON.stringify(value);
126
+ return text.length > 60 ? text.slice(0, 57) + '...' : text;
127
+ }
128
+ /** The diff as plain lines: `key: old -> new`. */
129
+ export function diffText(lines) {
130
+ return lines.map((l) => l.key.join('.') + ': ' + show(l.from) + ' -> ' + show(l.to));
131
+ }
@@ -1,5 +1,5 @@
1
1
  import { type App } from '../../tui/index.js';
2
- import { type SchemaNode } from './schema-node.js';
2
+ import { type SchemaNode } from '../../../app/kernel/index.js';
3
3
  /** What is wrong with the text typed for a number setting (true = fine), in words that name the allowed range. */
4
4
  export declare function numberProblem(node: SchemaNode, text: string): true | string;
5
5
  /** The new value, or undefined when you backed out without changing anything. */
@@ -15,6 +15,10 @@ export declare class ValueEditor {
15
15
  private readonly hints;
16
16
  constructor(app: App, hints?: EditorHints);
17
17
  edit(path: string[], key: string[], node: SchemaNode, current: unknown): Promise<Edited>;
18
+ /** A choice with "Custom…" for a value of your own (a number or a text besides the fixed values). */
19
+ private custom;
20
+ /** A list: ticks for fixed values, a form per entry for objects, otherwise text (or paths) one per entry. */
21
+ private list;
18
22
  private number;
19
23
  /** A folder or file on this PC: the path picker (typing a path still works). */
20
24
  private folderOrFile;
@@ -1,15 +1,16 @@
1
- // Editing one value with the screen that fits it: on/off flips, choices are picked from a list, numbers and text are
2
- // typed and checked as you type, lists and maps get an editor of their own (add, change, remove), and objects (a
3
- // mapped folder, an agent) open as a small form. JSON is only the last resort for shapes none of these fit.
1
+ // Editing one value with the screen that fits its Field (app/kernel/fields.ts, the same descriptor the web's inputs
2
+ // read): on/off flips, choices are picked from a list, numbers and text are typed and checked as you type, lists and
3
+ // maps get an editor of their own (add, change, remove), and objects (a mapped folder, an agent) open as a small form.
4
+ // JSON is only the last resort for shapes none of these fit.
4
5
  import _ from 'lodash';
5
6
  import { Text } from '../../tui/index.js';
6
- import { choiceLabel } from './format.js';
7
+ import { choiceLabel, fieldOf, pathLiterals } from '../../../app/kernel/index.js';
7
8
  import { StringListEditor } from './collections/string-list-editor.js';
8
9
  import { MapEditor } from './collections/map-editor.js';
9
10
  import { ObjectListEditor } from './collections/object-list-editor.js';
10
11
  import { ObjectMapEditor } from './collections/object-map-editor.js';
11
12
  import { ObjectEditor } from './object-editor.js';
12
- import { askPath, pathKindOf, pathLiterals } from './path-kind.js';
13
+ import { askPath } from './ask-path.js';
13
14
  /** What is wrong with the text typed for a number setting (true = fine), in words that name the allowed range. */
14
15
  export function numberProblem(node, text) {
15
16
  const n = Number(text);
@@ -31,77 +32,79 @@ export class ValueEditor {
31
32
  this.hints = hints;
32
33
  }
33
34
  async edit(path, key, node, current) {
34
- const about = node.description ? [node.description] : [];
35
+ const field = fieldOf(node, key);
35
36
  const suggestions = this.hints.suggestions?.(key);
36
37
  if (suggestions?.length && node.type === 'string')
37
38
  return this.suggest(path, key, node, current, suggestions);
38
- switch (node.editor) {
39
+ switch (field.kind) {
39
40
  case 'boolean':
40
41
  return { value: !(current ?? false) };
41
- case 'choice':
42
- return this.pick(path, node, current, node.choiceValues);
43
- case 'union': {
44
- const open = node.openOption;
45
- const custom = '@custom';
46
- const picked = await this.app.choose(path, node.description ?? '', [
47
- ...node.choiceValues.map((v) => ({ value: v, label: choiceLabel(v) })),
48
- ...(open
49
- ? [
50
- {
51
- value: custom,
52
- label: 'Custom ' + open.type + '…',
53
- description: 'Type a ' + open.type + ' of your own.',
54
- },
55
- ]
56
- : []),
57
- ], node.choiceValues.includes(current) ? current : open && current !== undefined ? custom : undefined);
58
- if (picked === undefined)
59
- return undefined;
60
- return picked === custom && open
61
- ? this.edit(path, key, open, node.choiceValues.includes(current) ? undefined : current)
62
- : { value: picked };
63
- }
42
+ case 'enum':
43
+ return field.open ? this.custom(path, key, node, current) : this.pick(path, node, current, node.choiceValues);
64
44
  case 'number':
65
45
  return this.number(path, key, node, current);
66
- case 'string': {
67
- const kind = pathKindOf(key);
68
- if (kind)
69
- return this.folderOrFile(path, key, node, current, kind);
46
+ case 'path':
47
+ return this.folderOrFile(path, key, node, current, field.pathKind ?? 'dir');
48
+ case 'text':
49
+ case 'secret': {
70
50
  const typed = await this.app.text({
71
51
  path,
72
52
  label: Text.humanize(key[key.length - 1]),
73
- about,
53
+ about: field.help ? [field.help] : [],
74
54
  initial: typeof current === 'string' ? current : '',
75
55
  allowEmpty: node.check('') === true,
76
- masked: node.secret,
56
+ masked: field.kind === 'secret',
77
57
  validate: (v) => node.check(v),
78
58
  });
79
59
  return typed === undefined ? undefined : { value: typed };
80
60
  }
81
- case 'choice-list': {
82
- const ticked = Array.isArray(current) ? current : [];
83
- const picked = await this.app.list({
84
- path,
85
- intro: node.description,
86
- multi: true,
87
- items: node.element.choices.map((v) => ({ value: v, label: String(v), checked: ticked.includes(v) })),
88
- });
89
- return picked.kind === 'multi' ? { value: picked.values } : undefined;
90
- }
91
- case 'string-list':
92
- return new StringListEditor(this.app, path, node, this, key, pathKindOf(key)).run(Array.isArray(current) ? current : []);
93
- case 'string-map':
94
- return new MapEditor(this.app, path, node, this, key).run(_.isPlainObject(current) ? current : {});
95
- case 'object-list':
96
- return new ObjectListEditor(this.app, path, node, this, key).run(Array.isArray(current) ? current : []);
97
- case 'object-map':
98
- return new ObjectMapEditor(this.app, path, node, this, key).run(_.isPlainObject(current) ? current : {});
61
+ case 'list':
62
+ return this.list(path, key, node, field, current);
63
+ case 'map':
64
+ return field.element?.kind === 'object'
65
+ ? new ObjectMapEditor(this.app, path, node, this, key).run(_.isPlainObject(current) ? current : {})
66
+ : new MapEditor(this.app, path, node, this, key).run(_.isPlainObject(current) ? current : {});
99
67
  case 'object':
100
68
  return new ObjectEditor(this.app, path, node, this, key).run(_.isPlainObject(current) ? current : node.initial());
101
69
  default:
102
70
  return this.json(path, key, node, current);
103
71
  }
104
72
  }
73
+ /** A choice with "Custom…" for a value of your own (a number or a text besides the fixed values). */
74
+ async custom(path, key, node, current) {
75
+ const open = node.openOption;
76
+ const custom = '@custom';
77
+ const picked = await this.app.choose(path, node.description ?? '', [
78
+ ...node.choiceValues.map((v) => ({ value: v, label: choiceLabel(v) })),
79
+ {
80
+ value: custom,
81
+ label: 'Custom ' + open.type + '…',
82
+ description: 'Type a ' + open.type + ' of your own.',
83
+ },
84
+ ], node.choiceValues.includes(current) ? current : current !== undefined ? custom : undefined);
85
+ if (picked === undefined)
86
+ return undefined;
87
+ return picked === custom
88
+ ? this.edit(path, key, open, node.choiceValues.includes(current) ? undefined : current)
89
+ : { value: picked };
90
+ }
91
+ /** A list: ticks for fixed values, a form per entry for objects, otherwise text (or paths) one per entry. */
92
+ async list(path, key, node, field, current) {
93
+ const kind = field.element?.kind;
94
+ if (kind === 'enum') {
95
+ const ticked = Array.isArray(current) ? current : [];
96
+ const picked = await this.app.list({
97
+ path,
98
+ intro: node.description,
99
+ multi: true,
100
+ items: node.element.choices.map((v) => ({ value: v, label: String(v), checked: ticked.includes(v) })),
101
+ });
102
+ return picked.kind === 'multi' ? { value: picked.values } : undefined;
103
+ }
104
+ if (kind === 'object')
105
+ return new ObjectListEditor(this.app, path, node, this, key).run(Array.isArray(current) ? current : []);
106
+ return new StringListEditor(this.app, path, node, this, key, field.element?.pathKind).run(Array.isArray(current) ? current : []);
107
+ }
105
108
  async number(path, key, node, current) {
106
109
  const range = node.rangeText();
107
110
  const typed = await this.app.text({
@@ -8,7 +8,7 @@ import { KnownProjects } from '../../host/projects/index.js';
8
8
  import { isWithin } from '../../shared/paths/index.js';
9
9
  import { commandLine } from '../history.js';
10
10
  import { Text, Theme } from '../tui/index.js';
11
- import { askPath } from './settings/path-kind.js';
11
+ import { askPath } from './settings/ask-path.js';
12
12
  /** Folders worth offering: this one, the projects started before, and the folders inside your roots. */
13
13
  export function candidateFolders(ctx, cwd = process.cwd()) {
14
14
  const roots = ctx.loaded.config.projects.roots.map((r) => expandPath(r, ctx.loaded.base));
@@ -0,0 +1,21 @@
1
+ import type { Context } from '../commands/context.js';
2
+ import { type App } from '../tui/index.js';
3
+ /** The rows' ids in the settings list. */
4
+ export declare const EXPORT_SETTINGS = "@export-settings";
5
+ export declare const IMPORT_SETTINGS = "@import-settings";
6
+ export declare function shareSettingsRows(): {
7
+ value: string;
8
+ label: string;
9
+ hint: string;
10
+ description: string;
11
+ }[];
12
+ export declare class ShareSettingsFlow {
13
+ private readonly app;
14
+ private readonly ctx;
15
+ constructor(app: App, ctx: Context);
16
+ /** Your settings as they are on disk now (the menu may have saved changes since it started). */
17
+ private fresh;
18
+ /** Returns the notice to show (undefined when cancelled). */
19
+ export(): Promise<string | undefined>;
20
+ import(): Promise<string | undefined>;
21
+ }
@@ -0,0 +1,112 @@
1
+ // The "Export settings…" and "Import settings…" rows in Settings > General: pick a file, then (for an import) read the
2
+ // diff of what would change and say yes once. Same rules as sandbox config export / import.
3
+ import fs from 'node:fs';
4
+ import path from 'node:path';
5
+ import { loadConfig } from '../../core/config/index.js';
6
+ import { errMsg } from '../../core/util/index.js';
7
+ import { applyImport, DEFAULT_EXPORT, readImport, writeExport } from '../commands/maintenance/config-share.js';
8
+ import { Theme } from '../tui/index.js';
9
+ import { askPath } from './settings/ask-path.js';
10
+ import { diffText, skippedLine } from './settings/index.js';
11
+ /** The rows' ids in the settings list. */
12
+ export const EXPORT_SETTINGS = '@export-settings';
13
+ export const IMPORT_SETTINGS = '@import-settings';
14
+ export function shareSettingsRows() {
15
+ return [
16
+ {
17
+ value: EXPORT_SETTINGS,
18
+ label: 'Export settings…',
19
+ hint: '.json',
20
+ description: 'Write the settings you changed to a .json file you can share with a team. Secrets, the data folder and folders or files of this PC are left out.',
21
+ },
22
+ {
23
+ value: IMPORT_SETTINGS,
24
+ label: 'Import settings…',
25
+ hint: '.json',
26
+ description: 'Read a settings file someone shared, see what it would change, and apply it once you say yes. Secrets, the data folder and paths in it are never taken.',
27
+ },
28
+ ];
29
+ }
30
+ export class ShareSettingsFlow {
31
+ app;
32
+ ctx;
33
+ constructor(app, ctx) {
34
+ this.app = app;
35
+ this.ctx = ctx;
36
+ }
37
+ /** Your settings as they are on disk now (the menu may have saved changes since it started). */
38
+ fresh() {
39
+ return { ...this.ctx, loaded: loadConfig(this.ctx.loaded.configPath ?? undefined) };
40
+ }
41
+ /** Returns the notice to show (undefined when cancelled). */
42
+ async export() {
43
+ const where = ['Settings', 'Export settings'];
44
+ try {
45
+ const file = await askPath(this.app, {
46
+ path: where,
47
+ title: 'Where to write the settings file',
48
+ about: ['Only what differs from the defaults is written; secrets and paths of this PC are left out.'],
49
+ initial: path.resolve(DEFAULT_EXPORT),
50
+ kind: 'file',
51
+ validate: (p) => (p.toLowerCase().endsWith('.json') ? true : 'The file name should end in .json.'),
52
+ });
53
+ if (!file)
54
+ return undefined;
55
+ const exists = path.resolve(file);
56
+ const replace = fs.existsSync(exists);
57
+ if (replace &&
58
+ !(await this.app.confirm({ path: where, question: exists + ' exists. Replace it?', initial: false })))
59
+ return undefined;
60
+ const skipped = writeExport(this.fresh(), file, false, replace);
61
+ return (Theme.ok(Theme.glyph.ok + ' Wrote ' + exists + '.') +
62
+ (skipped.length ? Theme.muted(' Left out: ' + skipped.join(', ') + '.') : ''));
63
+ }
64
+ catch (e) {
65
+ return Theme.error(Theme.glyph.cross + ' ' + errMsg(e));
66
+ }
67
+ }
68
+ async import() {
69
+ const where = ['Settings', 'Import settings'];
70
+ try {
71
+ const file = await askPath(this.app, {
72
+ path: where,
73
+ title: 'Which settings file',
74
+ about: ['A file made by Export settings (or sandbox config export).'],
75
+ kind: 'file',
76
+ });
77
+ if (!file)
78
+ return undefined;
79
+ const ctx = this.fresh();
80
+ const plan = readImport(ctx, file);
81
+ const lines = [
82
+ ...(plan.changes.length
83
+ ? [
84
+ plan.changes.length + ' setting' + (plan.changes.length === 1 ? '' : 's') + ' would change:',
85
+ '',
86
+ ...diffText(plan.lines),
87
+ ]
88
+ : ['Nothing to change: your settings already match the file.']),
89
+ ...(plan.skipped.length
90
+ ? ['', 'Not imported (never taken from a file):', ...plan.skipped.map((s) => skippedLine(s))]
91
+ : []),
92
+ ];
93
+ if (!plan.changes.length) {
94
+ await this.app.message({ path: where, lines });
95
+ return Theme.muted('Nothing changed.');
96
+ }
97
+ const yes = await this.app.confirm({
98
+ path: where,
99
+ question: 'Change your settings like this?',
100
+ about: lines,
101
+ initial: false,
102
+ });
103
+ if (!yes)
104
+ return Theme.muted('Nothing changed.');
105
+ const n = applyImport(ctx, plan);
106
+ return Theme.ok(Theme.glyph.ok + ' Imported ' + n + ' setting' + (n === 1 ? '' : 's') + '.');
107
+ }
108
+ catch (e) {
109
+ return Theme.error(Theme.glyph.cross + ' ' + errMsg(e));
110
+ }
111
+ }
112
+ }
@@ -14,10 +14,15 @@ export type ShellExit = {
14
14
  kind: 'view';
15
15
  file: string;
16
16
  };
17
- /** Which section and category the menu shows. */
17
+ /** Where one section was left: its open category and its search. */
18
+ export interface ScreenView {
19
+ readonly category?: string;
20
+ readonly search?: string;
21
+ }
22
+ /** Which section the menu shows, and where each section was left (by section name). */
18
23
  export interface MenuView {
19
24
  readonly section: number;
20
- readonly category: string | undefined;
25
+ readonly screens: Readonly<Record<string, ScreenView>>;
21
26
  }
22
27
  /** One section of the menu. */
23
28
  export interface Page {
@@ -30,6 +35,12 @@ export interface Page {
30
35
  categories(): readonly Category[];
31
36
  /** The id of the open category. */
32
37
  readonly category: string | undefined;
38
+ /** The data is still loading: its categories are not known yet (a category asked for opens when they are). */
39
+ readonly loading?: boolean;
40
+ /** The search typed in the list ('' or missing: none). */
41
+ readonly search?: string;
42
+ /** Types a search into the list (the menu opens on the one it was left on). */
43
+ restoreSearch?(text: string): void;
33
44
  /** Opens a category (the navigation moved onto it). */
34
45
  selectCategory(id: string): Promise<void>;
35
46
  /** Whether the list has the keyboard (its highlight is softer while the navigation has it). */
@@ -51,6 +62,9 @@ export declare class Shell implements Screen<ShellExit> {
51
62
  private active;
52
63
  private focus;
53
64
  private notice;
65
+ /** Where each section was left, by name; a section takes its entry the first time it opens in this shell. */
66
+ private readonly screens;
67
+ private readonly restored;
54
68
  constructor(app: App, pages: readonly Page[], draft: Draft,
55
69
  /** Told after a save (the menu applies settings that act on this PC) and returns a note to add. */
56
70
  afterSave?: (result: DraftResult) => string | undefined);
@@ -58,11 +72,13 @@ export declare class Shell implements Screen<ShellExit> {
58
72
  /** Which pane has the keyboard. */
59
73
  get focused(): 'nav' | 'list';
60
74
  nav(): NavState;
61
- /** The section and category that are open (the menu opens on them again later in the same run). */
75
+ /** Notes where the open section is (its category and search). */
76
+ private remember;
77
+ /** The section that is open and where every section was left (the menu opens there again, also in a later run). */
62
78
  view(): MenuView;
63
- /** Opens the section and category a view names (a category that is gone is ignored). */
79
+ /** Opens the section a view names and puts each section back where it was (what is gone is ignored). */
64
80
  restore(view: MenuView): Promise<void>;
65
- /** Opens a section by index (loads its data). */
81
+ /** Opens a section by index (loads its data); the first time, it goes back to the category and search it had. */
66
82
  openTab(index: number): Promise<void>;
67
83
  /** Puts the keyboard in the navigation or in the list. */
68
84
  setFocus(focus: 'nav' | 'list'): void;
@@ -16,6 +16,9 @@ export class Shell {
16
16
  active = 0;
17
17
  focus = 'list';
18
18
  notice;
19
+ /** Where each section was left, by name; a section takes its entry the first time it opens in this shell. */
20
+ screens = new Map();
21
+ restored = new Set();
19
22
  constructor(app, pages, draft,
20
23
  /** Told after a save (the menu applies settings that act on this PC) and returns a note to add. */
21
24
  afterSave = () => undefined) {
@@ -45,21 +48,43 @@ export class Shell {
45
48
  focused: this.focus === 'nav',
46
49
  };
47
50
  }
48
- /** The section and category that are open (the menu opens on them again later in the same run). */
51
+ /** Notes where the open section is (its category and search). */
52
+ remember() {
53
+ // A section that was never opened here keeps what the last run knew about it.
54
+ if (!this.restored.has(this.active))
55
+ return;
56
+ const { category, search } = this.page;
57
+ if (category || search)
58
+ this.screens.set(this.page.label, { category, search: search || undefined });
59
+ else
60
+ this.screens.delete(this.page.label);
61
+ }
62
+ /** The section that is open and where every section was left (the menu opens there again, also in a later run). */
49
63
  view() {
50
- return { section: this.active, category: this.page.category };
64
+ this.remember();
65
+ return { section: this.active, screens: Object.fromEntries(this.screens) };
51
66
  }
52
- /** Opens the section and category a view names (a category that is gone is ignored). */
67
+ /** Opens the section a view names and puts each section back where it was (what is gone is ignored). */
53
68
  async restore(view) {
69
+ this.restored.clear();
70
+ for (const [label, screen] of Object.entries(view.screens))
71
+ this.screens.set(label, screen);
54
72
  await this.openTab(view.section);
55
- if (view.category && this.page.categories().some((c) => c.id === view.category))
56
- await this.page.selectCategory(view.category);
57
73
  }
58
- /** Opens a section by index (loads its data). */
74
+ /** Opens a section by index (loads its data); the first time, it goes back to the category and search it had. */
59
75
  async openTab(index) {
76
+ this.remember();
60
77
  this.active = Math.max(0, Math.min(this.pages.length - 1, index));
61
78
  this.page.setFocus(this.focus === 'list');
62
79
  await this.page.open();
80
+ if (this.restored.has(this.active))
81
+ return;
82
+ this.restored.add(this.active);
83
+ const wanted = this.screens.get(this.page.label);
84
+ if (wanted?.category && (this.page.loading || this.page.categories().some((c) => c.id === wanted.category)))
85
+ await this.page.selectCategory(wanted.category);
86
+ if (wanted?.search)
87
+ this.page.restoreSearch?.(wanted.search);
63
88
  }
64
89
  /** Puts the keyboard in the navigation or in the list. */
65
90
  setFocus(focus) {
@@ -0,0 +1,7 @@
1
+ import type { AnyView } from '../../app/kernel/index.js';
2
+ import type { SECTION_LABELS } from './shell.js';
3
+ export interface MenuViewPlace {
4
+ readonly view: AnyView;
5
+ readonly section: (typeof SECTION_LABELS)[number];
6
+ }
7
+ export declare const MENU_VIEWS: readonly MenuViewPlace[];
@@ -0,0 +1,11 @@
1
+ // The shared core's views the menu shows, and in which section (the parity self-test reads this list: an operation or
2
+ // query is reachable in the menu when a view here offers it). Settings is drawn by its own page over the same rows,
3
+ // because the menu stages changes until Ctrl+S.
4
+ import { sandboxView } from '../../app/features/sandbox/index.js';
5
+ import { sessionsView } from '../../app/features/sessions/index.js';
6
+ import { settingsView } from '../../app/features/settings/index.js';
7
+ export const MENU_VIEWS = [
8
+ { view: sessionsView, section: 'Sessions' },
9
+ { view: sandboxView, section: 'Sandbox' },
10
+ { view: settingsView, section: 'Settings' },
11
+ ];