windows-development-sandbox 1.0.5 → 1.1.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 (368) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +22 -6
  3. package/config.example.json +17 -0
  4. package/dist/api.js +2 -1
  5. package/dist/cli/args.d.ts +20 -0
  6. package/dist/cli/args.js +35 -3
  7. package/dist/cli/colors.d.ts +24 -0
  8. package/dist/cli/colors.js +42 -0
  9. package/dist/cli/commands/agent-commands.d.ts +6 -0
  10. package/dist/cli/commands/agent-commands.js +61 -0
  11. package/dist/cli/commands/context.d.ts +2 -0
  12. package/dist/cli/commands/context.js +3 -0
  13. package/dist/cli/commands/data/clean.d.ts +15 -0
  14. package/dist/cli/commands/data/clean.js +101 -0
  15. package/dist/cli/commands/data/collect.d.ts +11 -0
  16. package/dist/cli/commands/data/collect.js +54 -0
  17. package/dist/cli/commands/data/index.d.ts +4 -0
  18. package/dist/cli/commands/data/index.js +45 -0
  19. package/dist/cli/commands/data/remove.d.ts +2 -0
  20. package/dist/cli/commands/data/remove.js +30 -0
  21. package/dist/cli/commands/index.d.ts +5 -3
  22. package/dist/cli/commands/index.js +5 -3
  23. package/dist/cli/commands/launch.d.ts +26 -2
  24. package/dist/cli/commands/launch.js +220 -39
  25. package/dist/cli/commands/maintenance.js +33 -4
  26. package/dist/cli/commands/project.d.ts +29 -0
  27. package/dist/cli/commands/project.js +63 -0
  28. package/dist/cli/commands/projects.d.ts +14 -0
  29. package/dist/cli/commands/projects.js +146 -0
  30. package/dist/cli/commands/repair/checks.d.ts +2 -0
  31. package/dist/cli/commands/{repair.js → repair/checks.js} +19 -181
  32. package/dist/cli/commands/{repair.d.ts → repair/finders.d.ts} +0 -29
  33. package/dist/cli/commands/repair/finders.js +108 -0
  34. package/dist/cli/commands/repair/index.d.ts +12 -0
  35. package/dist/cli/commands/repair/index.js +61 -0
  36. package/dist/cli/commands/repair/types.d.ts +26 -0
  37. package/dist/cli/commands/repair/types.js +3 -0
  38. package/dist/cli/commands/restart.d.ts +6 -0
  39. package/dist/cli/commands/restart.js +85 -0
  40. package/dist/cli/commands/sessions.d.ts +2 -1
  41. package/dist/cli/commands/sessions.js +189 -45
  42. package/dist/cli/commands/setup/checks.d.ts +13 -0
  43. package/dist/cli/commands/setup/checks.js +71 -0
  44. package/dist/cli/commands/{setup.d.ts → setup/facts.d.ts} +1 -15
  45. package/dist/cli/commands/setup/facts.js +60 -0
  46. package/dist/cli/commands/setup/index.d.ts +2 -0
  47. package/dist/cli/commands/setup/index.js +43 -0
  48. package/dist/cli/commands/user-config.d.ts +8 -0
  49. package/dist/cli/commands/user-config.js +41 -0
  50. package/dist/cli/history.d.ts +11 -0
  51. package/dist/cli/history.js +21 -0
  52. package/dist/cli/index.d.ts +7 -1
  53. package/dist/cli/index.js +38 -13
  54. package/dist/cli/manifest.d.ts +8 -0
  55. package/dist/cli/manifest.js +35 -15
  56. package/dist/cli/menu-multi.d.ts +12 -0
  57. package/dist/cli/menu-multi.js +82 -0
  58. package/dist/cli/menu.d.ts +5 -13
  59. package/dist/cli/menu.js +149 -46
  60. package/dist/cli/progress.d.ts +9 -0
  61. package/dist/cli/progress.js +49 -0
  62. package/dist/cli/settings/edit.d.ts +13 -0
  63. package/dist/cli/settings/edit.js +19 -0
  64. package/dist/cli/settings/index.d.ts +8 -0
  65. package/dist/cli/settings/index.js +131 -0
  66. package/dist/cli/settings/prompts.d.ts +7 -0
  67. package/dist/cli/settings/prompts.js +48 -0
  68. package/dist/cli/settings/schema.d.ts +33 -0
  69. package/dist/cli/settings/schema.js +29 -0
  70. package/dist/cli/validate.d.ts +17 -0
  71. package/dist/cli/validate.js +89 -0
  72. package/dist/core/config.js +4 -1
  73. package/dist/core/disk.d.ts +16 -0
  74. package/dist/core/disk.js +55 -0
  75. package/dist/core/local-config.d.ts +8 -0
  76. package/dist/core/local-config.js +18 -5
  77. package/dist/core/project-policy.d.ts +15 -0
  78. package/dist/core/project-policy.js +35 -0
  79. package/dist/core/prompt.d.ts +4 -0
  80. package/dist/core/prompt.js +15 -0
  81. package/dist/core/safe-paths.d.ts +4 -0
  82. package/dist/core/safe-paths.js +37 -0
  83. package/dist/core/schema/common.d.ts +17 -0
  84. package/dist/core/schema/common.js +15 -0
  85. package/dist/core/schema/config.d.ts +594 -0
  86. package/dist/core/schema/config.js +63 -0
  87. package/dist/core/schema/databases.d.ts +33 -0
  88. package/dist/core/schema/databases.js +35 -0
  89. package/dist/core/schema/harness.d.ts +12 -0
  90. package/dist/core/schema/harness.js +17 -0
  91. package/dist/core/schema/json-schema.d.ts +5 -0
  92. package/dist/core/schema/json-schema.js +25 -0
  93. package/dist/core/schema/profile.d.ts +27 -0
  94. package/dist/core/schema/profile.js +29 -0
  95. package/dist/core/schema/projects.d.ts +9 -0
  96. package/dist/core/schema/projects.js +11 -0
  97. package/dist/core/schema/sandbox.d.ts +50 -0
  98. package/dist/core/schema/sandbox.js +36 -0
  99. package/dist/core/schema/toolchains.d.ts +93 -0
  100. package/dist/core/schema/toolchains.js +83 -0
  101. package/dist/core/schema.d.ts +6 -604
  102. package/dist/core/schema.js +8 -264
  103. package/dist/core/types.d.ts +29 -1
  104. package/dist/core/util.d.ts +7 -0
  105. package/dist/core/util.js +12 -3
  106. package/dist/core/workspace.d.ts +0 -5
  107. package/dist/core/workspace.js +12 -46
  108. package/dist/guest/boot-env.d.ts +4 -0
  109. package/dist/guest/boot-env.js +25 -0
  110. package/dist/guest/cmdline.d.ts +12 -0
  111. package/dist/guest/cmdline.js +23 -0
  112. package/dist/guest/daemon/exec.d.ts +5 -0
  113. package/dist/guest/daemon/exec.js +32 -0
  114. package/dist/guest/daemon/project.d.ts +14 -0
  115. package/dist/guest/daemon/project.js +132 -0
  116. package/dist/guest/daemon/session.d.ts +13 -0
  117. package/dist/guest/daemon/session.js +139 -0
  118. package/dist/guest/daemon/shutdown.d.ts +1 -0
  119. package/dist/guest/daemon/shutdown.js +21 -0
  120. package/dist/guest/daemon/state.d.ts +17 -0
  121. package/dist/guest/daemon/state.js +31 -0
  122. package/dist/guest/daemon.js +8 -141
  123. package/dist/guest/db.js +18 -3
  124. package/dist/guest/env.d.ts +2 -0
  125. package/dist/guest/env.js +4 -2
  126. package/dist/guest/guide.d.ts +6 -2
  127. package/dist/guest/guide.js +5 -4
  128. package/dist/guest/helpers.d.ts +3 -0
  129. package/dist/guest/helpers.js +26 -0
  130. package/dist/guest/main.js +17 -7
  131. package/dist/guest/ports.d.ts +6 -0
  132. package/dist/guest/ports.js +22 -0
  133. package/dist/guest/project-context.d.ts +7 -0
  134. package/dist/guest/project-context.js +10 -0
  135. package/dist/guest/project-env.d.ts +20 -0
  136. package/dist/guest/project-env.js +20 -0
  137. package/dist/guest/project-status.d.ts +5 -0
  138. package/dist/guest/project-status.js +17 -0
  139. package/dist/guest/ptyhost.d.ts +0 -18
  140. package/dist/guest/ptyhost.js +5 -63
  141. package/dist/guest/setup/agents.d.ts +5 -2
  142. package/dist/guest/setup/agents.js +22 -8
  143. package/dist/guest/setup/claude.js +11 -3
  144. package/dist/guest/setup/environment.d.ts +13 -3
  145. package/dist/guest/setup/environment.js +14 -10
  146. package/dist/guest/setup/hooks.d.ts +9 -1
  147. package/dist/guest/setup/hooks.js +28 -14
  148. package/dist/guest/toolchains/catalogue.d.ts +4 -0
  149. package/dist/guest/toolchains/catalogue.js +2 -2
  150. package/dist/guest/toolchains/cli.js +25 -17
  151. package/dist/guest/toolchains/common.d.ts +18 -1
  152. package/dist/guest/toolchains/common.js +34 -11
  153. package/dist/guest/toolchains/databases/dynamodb.d.ts +8 -0
  154. package/dist/guest/toolchains/databases/dynamodb.js +52 -0
  155. package/dist/guest/toolchains/{databases.d.ts → databases/index.d.ts} +2 -3
  156. package/dist/guest/toolchains/databases/index.js +62 -0
  157. package/dist/guest/toolchains/databases/mongodb.d.ts +3 -0
  158. package/dist/guest/toolchains/databases/mongodb.js +31 -0
  159. package/dist/guest/toolchains/databases/mysql.d.ts +3 -0
  160. package/dist/guest/toolchains/databases/mysql.js +38 -0
  161. package/dist/guest/toolchains/databases/postgres.d.ts +5 -0
  162. package/dist/guest/toolchains/databases/postgres.js +38 -0
  163. package/dist/guest/toolchains/databases/redis.d.ts +3 -0
  164. package/dist/guest/toolchains/databases/redis.js +16 -0
  165. package/dist/guest/toolchains/databases/server.d.ts +14 -0
  166. package/dist/guest/toolchains/databases/server.js +52 -0
  167. package/dist/guest/toolchains/git.js +25 -16
  168. package/dist/guest/toolchains/go.js +1 -1
  169. package/dist/guest/toolchains/index.d.ts +13 -9
  170. package/dist/guest/toolchains/index.js +40 -82
  171. package/dist/guest/toolchains/java.js +5 -5
  172. package/dist/guest/toolchains/leftovers.d.ts +2 -0
  173. package/dist/guest/toolchains/leftovers.js +26 -0
  174. package/dist/guest/toolchains/managers.js +12 -10
  175. package/dist/guest/toolchains/{binstall.js → native/binstall.js} +6 -9
  176. package/dist/guest/toolchains/native/cpp.d.ts +4 -0
  177. package/dist/guest/toolchains/native/cpp.js +38 -0
  178. package/dist/guest/toolchains/native/dotnet.d.ts +4 -0
  179. package/dist/guest/toolchains/native/dotnet.js +103 -0
  180. package/dist/guest/toolchains/native/mesa.d.ts +7 -0
  181. package/dist/guest/toolchains/native/mesa.js +46 -0
  182. package/dist/guest/toolchains/native/msvc/environment.d.ts +16 -0
  183. package/dist/guest/toolchains/native/msvc/environment.js +57 -0
  184. package/dist/guest/toolchains/native/msvc/index.d.ts +6 -0
  185. package/dist/guest/toolchains/native/msvc/index.js +48 -0
  186. package/dist/guest/toolchains/native/msvc/packages.d.ts +6 -0
  187. package/dist/guest/toolchains/native/msvc/packages.js +156 -0
  188. package/dist/guest/toolchains/native/msvc/tree.d.ts +15 -0
  189. package/dist/guest/toolchains/native/msvc/tree.js +21 -0
  190. package/dist/guest/toolchains/native/rust.d.ts +3 -0
  191. package/dist/guest/toolchains/native/rust.js +45 -0
  192. package/dist/guest/toolchains/node.js +26 -6
  193. package/dist/guest/toolchains/plan.d.ts +22 -0
  194. package/dist/guest/toolchains/plan.js +96 -0
  195. package/dist/guest/toolchains/pwsh.js +1 -1
  196. package/dist/guest/toolchains/python.js +17 -10
  197. package/dist/guest/toolchains/runtimes.d.ts +2 -0
  198. package/dist/guest/toolchains/runtimes.js +6 -8
  199. package/dist/guest/toolchains/scripting/php.d.ts +3 -0
  200. package/dist/guest/toolchains/scripting/php.js +71 -0
  201. package/dist/guest/toolchains/scripting/ruby.d.ts +3 -0
  202. package/dist/guest/toolchains/scripting/ruby.js +47 -0
  203. package/dist/guest/toolchains/scripting/vcruntime.d.ts +7 -0
  204. package/dist/guest/toolchains/scripting/vcruntime.js +45 -0
  205. package/dist/guest/view.js +21 -1
  206. package/dist/host/accounts.js +14 -5
  207. package/dist/host/agent-commands.d.ts +25 -0
  208. package/dist/host/agent-commands.js +145 -0
  209. package/dist/host/attach.d.ts +17 -0
  210. package/dist/host/attach.js +69 -0
  211. package/dist/host/channel.d.ts +23 -22
  212. package/dist/host/channel.js +66 -84
  213. package/dist/host/claude-profile.js +7 -3
  214. package/dist/host/cleanup.js +2 -11
  215. package/dist/host/database-plan.d.ts +12 -0
  216. package/dist/host/database-plan.js +52 -0
  217. package/dist/host/generate.d.ts +4 -12
  218. package/dist/host/generate.js +48 -135
  219. package/dist/host/join.d.ts +28 -0
  220. package/dist/host/join.js +76 -0
  221. package/dist/host/launch/mappings.d.ts +3 -0
  222. package/dist/host/launch/mappings.js +50 -0
  223. package/dist/host/launch/projects.d.ts +15 -0
  224. package/dist/host/launch/projects.js +56 -0
  225. package/dist/host/launch/stage.d.ts +11 -0
  226. package/dist/host/launch/stage.js +66 -0
  227. package/dist/host/launch/wsb.d.ts +10 -0
  228. package/dist/host/launch/wsb.js +34 -0
  229. package/dist/host/pc-scan.d.ts +6 -0
  230. package/dist/host/pc-scan.js +37 -0
  231. package/dist/host/platform.d.ts +2 -0
  232. package/dist/host/platform.js +13 -1
  233. package/dist/host/project-scan.d.ts +1 -17
  234. package/dist/host/project-scan.js +2 -86
  235. package/dist/host/projects.d.ts +100 -0
  236. package/dist/host/projects.js +147 -0
  237. package/dist/host/repo-guard.d.ts +15 -2
  238. package/dist/host/repo-guard.js +103 -12
  239. package/dist/host/terminal.js +3 -0
  240. package/dist/host/test-guard.d.ts +6 -0
  241. package/dist/host/test-guard.js +15 -0
  242. package/dist/host/window.js +7 -3
  243. package/dist/host/wsb.d.ts +37 -0
  244. package/dist/host/wsb.js +101 -0
  245. package/dist/runtime/daemon.js +10997 -2794
  246. package/dist/runtime/db.js +6428 -464
  247. package/dist/runtime/main.js +7359 -1135
  248. package/dist/runtime/view.js +5922 -47
  249. package/dist/selftest/accounts.test.d.ts +2 -0
  250. package/dist/selftest/accounts.test.js +44 -0
  251. package/dist/selftest/agent-commands.test.d.ts +2 -0
  252. package/dist/selftest/agent-commands.test.js +47 -0
  253. package/dist/selftest/channel.test.d.ts +2 -0
  254. package/dist/selftest/channel.test.js +37 -0
  255. package/dist/selftest/cli.test.d.ts +2 -2
  256. package/dist/selftest/cli.test.js +115 -27
  257. package/dist/selftest/cmdline.test.d.ts +2 -0
  258. package/dist/selftest/cmdline.test.js +45 -0
  259. package/dist/selftest/config.test.d.ts +2 -3
  260. package/dist/selftest/config.test.js +36 -72
  261. package/dist/selftest/data.test.d.ts +2 -2
  262. package/dist/selftest/data.test.js +77 -70
  263. package/dist/selftest/detect.test.d.ts +2 -2
  264. package/dist/selftest/detect.test.js +54 -38
  265. package/dist/selftest/fixtures.d.ts +44 -4
  266. package/dist/selftest/fixtures.js +40 -23
  267. package/dist/selftest/generate.test.d.ts +2 -0
  268. package/dist/selftest/generate.test.js +63 -0
  269. package/dist/selftest/git-access.test.d.ts +2 -0
  270. package/dist/selftest/git-access.test.js +26 -0
  271. package/dist/selftest/guest-boot.test.d.ts +2 -0
  272. package/dist/selftest/guest-boot.test.js +76 -0
  273. package/dist/selftest/harness.d.ts +44 -7
  274. package/dist/selftest/harness.js +62 -21
  275. package/dist/selftest/host-sim.test.d.ts +2 -0
  276. package/dist/selftest/host-sim.test.js +93 -0
  277. package/dist/selftest/hostile-input.test.d.ts +2 -0
  278. package/dist/selftest/hostile-input.test.js +54 -0
  279. package/dist/selftest/index.d.ts +8 -1
  280. package/dist/selftest/index.js +134 -35
  281. package/dist/selftest/links.test.d.ts +2 -0
  282. package/dist/selftest/links.test.js +50 -0
  283. package/dist/selftest/live-daemon.test.d.ts +2 -0
  284. package/dist/selftest/live-daemon.test.js +91 -0
  285. package/dist/selftest/live-git.test.d.ts +2 -0
  286. package/dist/selftest/live-git.test.js +60 -0
  287. package/dist/selftest/live-projects.test.d.ts +2 -0
  288. package/dist/selftest/live-projects.test.js +127 -0
  289. package/dist/selftest/live.d.ts +65 -0
  290. package/dist/selftest/live.js +109 -0
  291. package/dist/selftest/local-config.test.d.ts +2 -0
  292. package/dist/selftest/local-config.test.js +107 -0
  293. package/dist/selftest/mirror.test.d.ts +2 -3
  294. package/dist/selftest/mirror.test.js +92 -65
  295. package/dist/selftest/network.test.d.ts +2 -0
  296. package/dist/selftest/network.test.js +28 -0
  297. package/dist/selftest/personalization.test.d.ts +2 -0
  298. package/dist/selftest/personalization.test.js +53 -0
  299. package/dist/selftest/projects.test.d.ts +2 -0
  300. package/dist/selftest/projects.test.js +254 -0
  301. package/dist/selftest/repair.test.d.ts +2 -0
  302. package/dist/selftest/repair.test.js +42 -0
  303. package/dist/selftest/repo-guard.test.d.ts +2 -0
  304. package/dist/selftest/repo-guard.test.js +63 -0
  305. package/dist/selftest/secrets.test.d.ts +2 -0
  306. package/dist/selftest/secrets.test.js +71 -0
  307. package/dist/selftest/setup.test.d.ts +2 -2
  308. package/dist/selftest/setup.test.js +45 -22
  309. package/dist/selftest/suite.d.ts +13 -0
  310. package/dist/selftest/suite.js +1 -0
  311. package/dist/selftest/sync.test.d.ts +2 -3
  312. package/dist/selftest/sync.test.js +90 -78
  313. package/dist/selftest/test-guard.test.d.ts +2 -0
  314. package/dist/selftest/test-guard.test.js +49 -0
  315. package/dist/selftest/toolchains.test.d.ts +2 -0
  316. package/dist/selftest/toolchains.test.js +103 -0
  317. package/dist/selftest/workspace.test.d.ts +2 -0
  318. package/dist/selftest/workspace.test.js +53 -0
  319. package/dist/shared/download.d.ts +3 -1
  320. package/dist/shared/download.js +19 -4
  321. package/dist/shared/paths.d.ts +5 -0
  322. package/dist/shared/paths.js +6 -0
  323. package/dist/shared/projects.d.ts +53 -0
  324. package/dist/shared/projects.js +67 -0
  325. package/dist/shared/rundir.d.ts +10 -1
  326. package/dist/shared/rundir.js +12 -4
  327. package/dist/shared/safe-remove.js +11 -0
  328. package/dist/shared/schema.d.ts +443 -0
  329. package/dist/shared/schema.js +41 -2
  330. package/dist/shared/test-guard.d.ts +4 -0
  331. package/dist/shared/test-guard.js +37 -0
  332. package/dist/sync/claude-mem.js +3 -1
  333. package/dist/sync/history.d.ts +23 -12
  334. package/dist/sync/history.js +52 -20
  335. package/dist/sync/watcher.d.ts +8 -0
  336. package/dist/sync/watcher.js +23 -4
  337. package/package.json +6 -6
  338. package/sandbox/bootstrap.ps1 +258 -258
  339. package/sandbox/cpucap.ps1 +51 -0
  340. package/sandbox/logon.cmd +18 -16
  341. package/sandbox/openssl.cs +42 -0
  342. package/sandbox/ptyhost.cs +15 -4
  343. package/sandbox/trust.cmd +35 -35
  344. package/schemas/config.schema.json +100 -1
  345. package/schemas/sandbox-config.schema.json +17 -1
  346. package/dist/cli/commands/data.d.ts +0 -46
  347. package/dist/cli/commands/data.js +0 -249
  348. package/dist/cli/commands/setup.js +0 -152
  349. package/dist/cli/settings.d.ts +0 -11
  350. package/dist/cli/settings.js +0 -156
  351. package/dist/guest/toolchains/databases.js +0 -226
  352. package/dist/guest/toolchains/msvc.d.ts +0 -10
  353. package/dist/guest/toolchains/msvc.js +0 -216
  354. package/dist/guest/toolchains/native.d.ts +0 -12
  355. package/dist/guest/toolchains/native.js +0 -220
  356. package/dist/guest/toolchains/scripting.d.ts +0 -10
  357. package/dist/guest/toolchains/scripting.js +0 -137
  358. package/dist/selftest/guest.test.d.ts +0 -3
  359. package/dist/selftest/guest.test.js +0 -61
  360. package/dist/selftest/helpers.test.d.ts +0 -2
  361. package/dist/selftest/helpers.test.js +0 -125
  362. package/dist/selftest/host.test.d.ts +0 -2
  363. package/dist/selftest/host.test.js +0 -71
  364. package/dist/selftest/live.test.d.ts +0 -7
  365. package/dist/selftest/live.test.js +0 -127
  366. package/dist/selftest/security.test.d.ts +0 -3
  367. package/dist/selftest/security.test.js +0 -143
  368. /package/dist/guest/toolchains/{binstall.d.ts → native/binstall.d.ts} +0 -0
@@ -11,7 +11,10 @@ import crypto from 'node:crypto';
11
11
  import fs from 'node:fs';
12
12
  import path from 'node:path';
13
13
  import _ from 'lodash';
14
- import { dirExists, fileExists, isLink, tryReadJson, writeJson } from '../core/util.js';
14
+ import { z } from 'zod';
15
+ import { readValid, writeFileAtomic, writeJsonAtomic } from '../shared/rundir.js';
16
+ import { assertTestWritable } from './test-guard.js';
17
+ import { dirExists, fileExists, isLink } from '../core/util.js';
15
18
  /** Config keys that make git run a program, or load more config. */
16
19
  const DANGEROUS_KEY = /^(fsmonitor|hookspath|sshcommand|pager|editor|askpass|program|gpgprogram|textconv|command|cmd|clean|smudge|process|driver|helper|uploadpack|receivepack|path|attributesfile|excludesfile|worktree|worktreeconfig|gitproxy|proxycommand|sendemailvalidate|tool)\s*=/i;
17
20
  const SECTION = /^\s*\[([^\]]*)\]\s*/;
@@ -30,7 +33,30 @@ const COMMANDS = new RegExp('^(' + [
30
33
  'make', 'cmake', 'ninja', 'gcc', 'g\\+\\+', 'clang', 'psql', 'mysql', 'mariadb', 'mongosh', 'redis-cli', 'docker', 'kubectl', 'terraform', 'az', 'aws',
31
34
  'powershell', 'pwsh', 'cmd', 'where', 'tasklist', 'tar', 'curl', 'ssh', 'scp',
32
35
  ].join('|') + ')\\.(exe|com|bat|cmd|vbs|vbe|js|jse|wsf|wsh|msc|ps1)$', 'i');
33
- const hash = (file) => (isLink(file) ? 'link' : crypto.createHash('sha256').update(fs.readFileSync(file)).digest('hex'));
36
+ /** SHA-256 in small chunks: a sandbox can plant a multi-gigabyte "node.exe" to exhaust this PC's memory. */
37
+ const hash = (file) => {
38
+ if (isLink(file))
39
+ return 'link';
40
+ const sum = crypto.createHash('sha256');
41
+ const buffer = Buffer.alloc(1 << 20);
42
+ let fd;
43
+ // A file that cannot be read (locked by a program in the sandbox) never matches the snapshot, so it is dealt with.
44
+ try {
45
+ fd = fs.openSync(file, 'r');
46
+ }
47
+ catch {
48
+ return 'unreadable-' + crypto.randomBytes(4).toString('hex');
49
+ }
50
+ try {
51
+ for (let n = fs.readSync(fd, buffer, 0, buffer.length, null); n > 0; n = fs.readSync(fd, buffer, 0, buffer.length, null))
52
+ sum.update(buffer.subarray(0, n));
53
+ }
54
+ finally {
55
+ fs.closeSync(fd);
56
+ }
57
+ return sum.digest('hex');
58
+ };
59
+ const MAX_CONFIG_BYTES = 4 * 1024 * 1024;
34
60
  const plainGitDir = (workspace) => {
35
61
  const dir = path.join(workspace, '.git');
36
62
  return dirExists(dir) && !isLink(dir) ? dir : null;
@@ -40,7 +66,7 @@ const filesIn = (dir, accept) => dirExists(dir) && !isLink(dir)
40
66
  : {};
41
67
  const configLines = (dir) => {
42
68
  const file = path.join(dir, 'config');
43
- return fileExists(file) && !isLink(file) ? fs.readFileSync(file, 'utf8').split(/\r?\n/) : [];
69
+ return fileExists(file) && !isLink(file) && fs.statSync(file).size <= MAX_CONFIG_BYTES ? fs.readFileSync(file, 'utf8').split(/\r?\n/) : [];
44
70
  };
45
71
  export const snapshotRepo = (workspace) => {
46
72
  const dir = plainGitDir(workspace);
@@ -59,7 +85,12 @@ const quarantine = (from, names, into, kind) => {
59
85
  const target = path.join(into, 'agent-sandbox-quarantine', String(Date.now()));
60
86
  fs.mkdirSync(target, { recursive: true });
61
87
  return names.map((name) => {
62
- fs.renameSync(path.join(from, name), path.join(target, name));
88
+ try {
89
+ fs.renameSync(path.join(from, name), path.join(target, name));
90
+ }
91
+ catch (e) {
92
+ return { kind, detail: name + ' (could not be moved: ' + e.code + '; delete it yourself before running anything here)' };
93
+ }
63
94
  return { kind, detail: name + ' (moved to ' + target + ')' };
64
95
  });
65
96
  };
@@ -85,7 +116,12 @@ const disableConfig = (dir, before) => {
85
116
  return MARK + line.trim();
86
117
  });
87
118
  if (findings.length)
88
- fs.writeFileSync(path.join(dir, 'config'), fixed.join('\n'), 'utf8');
119
+ writeFileAtomic(path.join(dir, 'config'), fixed.join('\n'));
120
+ const file = path.join(dir, 'config');
121
+ // A link or a huge file is not read at all, so say so instead of letting it pass as "nothing found".
122
+ const unchecked = isLink(file) ? '.git/config is a link' : lines.length === 0 && fileExists(file) && fs.statSync(file).size > MAX_CONFIG_BYTES ? '.git/config is over 4 MB' : null;
123
+ if (unchecked)
124
+ findings.push({ kind: 'git', detail: unchecked + ', so it cannot be checked. Do not run git in this folder until you have looked at it.' });
89
125
  return findings;
90
126
  };
91
127
  /**
@@ -93,6 +129,7 @@ const disableConfig = (dir, before) => {
93
129
  * Returns what was found (and fixed). Never touches what was already there before the sandbox ran.
94
130
  */
95
131
  export const guardRepo = (workspace, before) => {
132
+ assertTestWritable(workspace);
96
133
  const findings = [];
97
134
  const commandsNow = filesIn(workspace, (n) => COMMANDS.test(n));
98
135
  findings.push(...quarantine(workspace, Object.keys(commandsNow).filter((n) => before.commands[n] !== commandsNow[n]), workspace, 'command'));
@@ -114,15 +151,69 @@ export const guardRepo = (workspace, before) => {
114
151
  const syncDir = (dataPath) => path.join(path.dirname(path.dirname(dataPath)), 'sync');
115
152
  const snapshotFile = (dataPath) => path.join(syncDir(dataPath), path.basename(dataPath) + '-repo.json');
116
153
  const findingsFile = (dataPath) => path.join(syncDir(dataPath), path.basename(dataPath) + '-repo-findings.json');
117
- export const saveRepoSnapshot = (dataPath, workspace) => writeJson(snapshotFile(dataPath), { workspace, snapshot: snapshotRepo(workspace) });
118
- /** Re-checks the workspace of the current/last launch; findings are kept in a host-only file for `status`. */
154
+ const Finding = z.object({ kind: z.enum(['hook', 'config', 'git', 'command']), detail: z.string().max(4096), at: z.string().max(64).optional() });
155
+ const Saved = z.object({
156
+ workspace: z.string().max(1024).refine((p) => path.isAbsolute(p)),
157
+ snapshot: z.object({ gitDir: z.boolean(), config: z.array(z.string()), hooks: z.record(z.string(), z.string()), redirects: z.array(z.string()), commands: z.record(z.string(), z.string()) }),
158
+ });
159
+ const Store = z.strictObject({ projects: z.array(Saved).max(4096), roots: z.array(z.string().max(1024).refine((p) => path.isAbsolute(p))).max(64) });
160
+ const StoreAny = z.union([Store, Saved.transform((s) => ({ projects: [s], roots: [] }))]);
161
+ const readStore = (dataPath) => readValid(StoreAny, snapshotFile(dataPath)) ?? { projects: [], roots: [] };
162
+ const EMPTY = { gitDir: false, config: [], hooks: {}, redirects: [], commands: {} };
163
+ const MAX_ROOT_PROJECTS = 2000;
164
+ /** The folders directly inside a project root (plain folders only; links are never followed). */
165
+ const rootProjects = (root) => {
166
+ if (!dirExists(root) || isLink(root))
167
+ return [];
168
+ return fs.readdirSync(root, { withFileTypes: true }).filter((e) => e.isDirectory() && !e.isSymbolicLink()).slice(0, MAX_ROOT_PROJECTS).map((e) => path.join(root, e.name));
169
+ };
170
+ const has = (projects, workspace) => projects.some((s) => s.workspace.toLowerCase() === workspace.toLowerCase());
171
+ /**
172
+ * At launch: every folder the sandbox will be able to write. The launch folder, projects mapped with it and every
173
+ * project already inside a project root are snapshotted as they are now, before the sandbox can touch them.
174
+ */
175
+ export const saveRepoSnapshot = (dataPath, workspace, more = [], roots = []) => {
176
+ const folders = _.uniqBy([workspace, ...more, ...roots.flatMap((r) => [r, ...rootProjects(r)])], (w) => w.toLowerCase());
177
+ writeJsonAtomic(snapshotFile(dataPath), { projects: folders.map((w) => ({ workspace: w, snapshot: snapshotRepo(w) })), roots });
178
+ };
179
+ /**
180
+ * A project that joins the running sandbox: snapshotted before its first session, then guarded like the launch
181
+ * folder. A project already guarded keeps its first snapshot (what was there before the sandbox could touch it).
182
+ */
183
+ export const addRepoSnapshot = (dataPath, workspace, o = {}) => {
184
+ const store = readStore(dataPath);
185
+ // A folder inside one that is guarded already is covered by it only for its root files; it gets its own entry.
186
+ if (has(store.projects, workspace))
187
+ return;
188
+ writeJsonAtomic(snapshotFile(dataPath), { ...store, projects: [...store.projects, { workspace, snapshot: o.empty ? EMPTY : snapshotRepo(workspace) }] });
189
+ };
190
+ /**
191
+ * Folders that appeared inside a project root after the launch: the sandbox could have written them from the start,
192
+ * so they get an EMPTY baseline: any git hook, git config that runs programs (core.hooksPath, core.fsmonitor, ...)
193
+ * or planted command in them counts as added inside the sandbox and is neutralised.
194
+ */
195
+ const discoverRootProjects = (dataPath) => {
196
+ const store = readStore(dataPath);
197
+ const found = store.roots.flatMap(rootProjects).filter((w) => !has(store.projects, w)).map((workspace) => ({ workspace, snapshot: EMPTY }));
198
+ if (!found.length)
199
+ return store.projects;
200
+ const projects = [...store.projects, ...found].slice(0, 4096);
201
+ writeJsonAtomic(snapshotFile(dataPath), { ...store, projects });
202
+ return projects;
203
+ };
204
+ /** The folders being guarded (for status). */
205
+ export const guardedProjects = (dataPath) => readStore(dataPath).projects.map((s) => s.workspace);
206
+ /** Re-checks every project of the current/last launch; findings are kept in a host-only file for `status`. Returns the new ones. */
119
207
  export const checkRepo = (dataPath) => {
120
- const saved = tryReadJson(snapshotFile(dataPath));
121
- if (!saved?.snapshot?.commands)
208
+ const saved = discoverRootProjects(dataPath);
209
+ if (!saved.length)
122
210
  return [];
123
- const findings = guardRepo(saved.workspace, saved.snapshot);
211
+ const old = repoFindings(dataPath);
212
+ // A finding that cannot be fixed (a file locked in the sandbox) is reported once, not every minute.
213
+ const findings = saved.flatMap((s) => guardRepo(s.workspace, s.snapshot).map((f) => (saved.length > 1 ? { ...f, detail: s.workspace + ': ' + f.detail } : f)))
214
+ .filter((f) => !old.some((o) => o.kind === f.kind && o.detail === f.detail));
124
215
  if (findings.length)
125
- writeJson(findingsFile(dataPath), [...repoFindings(dataPath), ...findings.map((f) => ({ ...f, at: new Date().toISOString() }))]);
216
+ writeJsonAtomic(findingsFile(dataPath), [...old, ...findings.map((f) => ({ ...f, at: new Date().toISOString() }))]);
126
217
  return findings;
127
218
  };
128
- export const repoFindings = (dataPath) => tryReadJson(findingsFile(dataPath)) ?? [];
219
+ export const repoFindings = (dataPath) => readValid(z.array(Finding).max(10000), findingsFile(dataPath)) ?? [];
@@ -4,6 +4,7 @@ import fs from 'node:fs';
4
4
  import path from 'node:path';
5
5
  import { githubAsset, installArchive } from '../shared/download.js';
6
6
  import { safeRemove } from '../shared/safe-remove.js';
7
+ import { assertTestWritable } from './test-guard.js';
7
8
  import { z } from 'zod';
8
9
  import { assertNoLinks, errMsg, fileExists, parseJsonc, writeJson } from '../core/util.js';
9
10
  export const terminalDir = (dataPath) => path.join(dataPath, 'tools', 'terminal');
@@ -55,6 +56,7 @@ export const withPwshProfile = (settings, visible) => {
55
56
  */
56
57
  export const syncPwshProfile = (dataPath, allowed) => {
57
58
  const file = path.join(terminalDir(dataPath), 'settings', 'settings.json');
59
+ assertTestWritable(file);
58
60
  assertNoLinks(dataPath, file);
59
61
  if (!fileExists(file) || fs.statSync(file).size > MAX_SETTINGS_BYTES)
60
62
  return;
@@ -89,6 +91,7 @@ export const ensureTerminal = async (dataPath, refresh, options = {}) => {
89
91
  const installTerminal = async (dataPath, refresh) => {
90
92
  const exe = terminalExe(dataPath);
91
93
  const dir = terminalDir(dataPath);
94
+ assertTestWritable(dir);
92
95
  const settings = path.join(dir, 'settings');
93
96
  // installArchive replaces tools\terminal: the portable settings folder waits here meanwhile (and goes back also when
94
97
  // the download fails). One still parked from an interrupted earlier run is the one to keep.
@@ -0,0 +1,6 @@
1
+ import { selfTestRunning } from '../shared/test-guard.js';
2
+ export { selfTestRunning };
3
+ /** Refuses an action that reaches outside the test's own temp folder (a no-op outside the self-test). */
4
+ export declare const refuseInSelfTest: (what: string) => void;
5
+ /** A write, copy or delete on the host: during the self-test only the temp folder is allowed. */
6
+ export declare const assertTestWritable: (target: string) => void;
@@ -0,0 +1,15 @@
1
+ // Host side of the self-test guard (see shared/test-guard.ts): outward actions refuse, with a message the user can read.
2
+ import { testWriteRefusal, selfTestRunning } from '../shared/test-guard.js';
3
+ import { fail } from '../core/util.js';
4
+ export { selfTestRunning };
5
+ /** Refuses an action that reaches outside the test's own temp folder (a no-op outside the self-test). */
6
+ export const refuseInSelfTest = (what) => {
7
+ if (selfTestRunning())
8
+ fail('The self-test does not ' + what + '.', 'It only works in temp folders, so it can never change this PC.');
9
+ };
10
+ /** A write, copy or delete on the host: during the self-test only the temp folder is allowed. */
11
+ export const assertTestWritable = (target) => {
12
+ const refusal = testWriteRefusal(target);
13
+ if (refusal)
14
+ fail(refusal);
15
+ };
@@ -1,7 +1,9 @@
1
1
  // Headless mode: Windows Sandbox always opens its client window, so we hide it (ShowWindow SW_HIDE) as soon as it
2
2
  // appears. The VM and the logged-in session keep running; `sbx-show` brings the window back.
3
3
  import path from 'node:path';
4
- import { isWin, runPowerShell, runPowerShellAsync, tryReadJson, writeJson } from '../core/util.js';
4
+ import { writeJsonAtomic } from '../shared/rundir.js';
5
+ import { refuseInSelfTest } from './test-guard.js';
6
+ import { isWin, runPowerShell, runPowerShellAsync, tryReadJson } from '../core/util.js';
5
7
  const PRELUDE = `
6
8
  $ErrorActionPreference = 'Stop'
7
9
  Add-Type -Namespace AgentSandbox -Name Win -MemberDefinition @'
@@ -34,6 +36,7 @@ function Get-SandboxWindows([bool]$visibleOnly) {
34
36
  const stateFile = (storageRoot) => path.join(storageRoot, 'run', 'hidden-windows.json');
35
37
  /** Waits for the sandbox window and hides it; keeps watching briefly in case Windows re-shows it while connecting. */
36
38
  export const hideSandboxWindow = async (storageRoot, waitSeconds = 180) => {
39
+ refuseInSelfTest('hide windows');
37
40
  if (!isWin)
38
41
  return false;
39
42
  const script = PRELUDE + `
@@ -48,11 +51,12 @@ while ((Get-Date) -lt $deadline) {
48
51
  ConvertTo-Json -Compress @($hidden)`;
49
52
  const stdout = await runPowerShellAsync(script);
50
53
  const handles = JSON.parse(stdout.trim() || '[]').filter((h) => Number.isSafeInteger(h));
51
- writeJson(stateFile(storageRoot), { handles });
54
+ writeJsonAtomic(stateFile(storageRoot), { handles });
52
55
  return handles.length > 0;
53
56
  };
54
57
  /** Shows the sandbox window again (the ones we hid, or any hidden Windows Sandbox window). */
55
58
  export const showSandboxWindow = (storageRoot) => {
59
+ refuseInSelfTest('show windows');
56
60
  if (!isWin)
57
61
  return false;
58
62
  // Only integers go into the script, whatever the file contains.
@@ -63,6 +67,6 @@ if (-not $handles) { $handles = Get-SandboxWindows $false }
63
67
  foreach ($h in $handles) { [void][AgentSandbox.Win]::ShowWindow([IntPtr]$h, 5); [void][AgentSandbox.Win]::ShowWindow([IntPtr]$h, 9); [void][AgentSandbox.Win]::SetForegroundWindow([IntPtr]$h) }
64
68
  @($handles).Count`;
65
69
  const { stdout } = runPowerShell(script, true);
66
- writeJson(stateFile(storageRoot), { handles: [] });
70
+ writeJsonAtomic(stateFile(storageRoot), { handles: [] });
67
71
  return Number(stdout.trim()) > 0;
68
72
  };
@@ -0,0 +1,37 @@
1
+ /** Where wsb.exe can be: the Store app's alias, then System32 (never by bare name; cwd is often the workspace). */
2
+ export declare const wsbCandidates: (env?: NodeJS.ProcessEnv) => string[];
3
+ export declare const findWsb: (env?: NodeJS.ProcessEnv, exists?: (p: string) => boolean) => string | null;
4
+ /** The version from `wsb --version` ("0.1.32.0", "wsb 1.2.3+abc"). */
5
+ export declare const parseVersion: (out: string) => string | null;
6
+ /** Subcommand names from `wsb --help` (the lines under "Commands:"). */
7
+ export declare const parseCommands: (help: string) => string[];
8
+ export interface WsbSandbox {
9
+ id: string;
10
+ status: string;
11
+ }
12
+ /** `wsb list --raw` (JSON: an array, or an object holding one) or the plain table: the sandboxes and their status. */
13
+ export declare const parseList: (out: string) => WsbSandbox[];
14
+ /** `wsb ip --id <id>`: the first IPv4 address in the output. */
15
+ export declare const parseIp: (out: string) => string | null;
16
+ export interface WsbResult {
17
+ ok: boolean;
18
+ stdout: string;
19
+ stderr: string;
20
+ error?: string;
21
+ }
22
+ export interface Wsb {
23
+ exe: string;
24
+ version: string | null;
25
+ commands: string[];
26
+ has: (command: string) => boolean;
27
+ }
28
+ /** The CLI and what it offers, or null on Windows versions without it. */
29
+ export declare const detectWsb: (env?: NodeJS.ProcessEnv) => Wsb | null;
30
+ /** The running sandbox's id: the only running one (with several, none is guessed). */
31
+ export declare const runningSandboxId: (wsb: Wsb) => {
32
+ id: string | null;
33
+ error?: string;
34
+ };
35
+ /** Shares a folder of this PC into the running sandbox at `sandboxPath` (read/write unless readOnly). */
36
+ export declare const shareFolder: (wsb: Wsb, id: string, hostPath: string, sandboxPath: string, readOnly?: boolean) => WsbResult;
37
+ export declare const sandboxIp: (wsb: Wsb, id: string) => string | null;
@@ -0,0 +1,101 @@
1
+ // The Windows Sandbox command line (wsb.exe), Windows 11 24H2+ with the updated Windows Sandbox. Used only where it
2
+ // adds something the .wsb file cannot: `wsb share` puts a folder into the RUNNING sandbox. Launching always stays on
3
+ // the .wsb path, which works on every Windows version. UNTESTED: the machines this was built on predate the CLI, so
4
+ // every call is checked and any failure falls back (the caller offers a restart instead), with the error shown.
5
+ // Syntax from Microsoft's docs (learn.microsoft.com, "Windows Sandbox command line"):
6
+ // wsb start [--id <id>] [-c|--config <xml>] wsb list [--raw] wsb ip --id <id> wsb stop --id <id>
7
+ // wsb share --id <id> -f|--host-path <host> -s|--sandbox-path <sandbox> [-w|--allow-write]
8
+ // wsb exec --id <id> -c <command> -r System|ExistingLogin [-d <dir>] wsb connect --id <id> --raw = JSON output
9
+ import fs from 'node:fs';
10
+ import path from 'node:path';
11
+ import { spawnSync } from 'node:child_process';
12
+ import { refuseInSelfTest } from './test-guard.js';
13
+ const GUID = /\b[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}\b/i;
14
+ const MAX_OUTPUT = 1024 * 1024;
15
+ /** Where wsb.exe can be: the Store app's alias, then System32 (never by bare name; cwd is often the workspace). */
16
+ export const wsbCandidates = (env = process.env) => [
17
+ env.LOCALAPPDATA ? path.join(env.LOCALAPPDATA, 'Microsoft', 'WindowsApps', 'wsb.exe') : '',
18
+ path.join(env.SystemRoot ?? env.WINDIR ?? 'C:\\Windows', 'System32', 'wsb.exe'),
19
+ ].filter(Boolean);
20
+ export const findWsb = (env = process.env, exists = fs.existsSync) => process.platform === 'win32' || exists !== fs.existsSync ? wsbCandidates(env).find((p) => exists(p)) ?? null : null;
21
+ // ---- parsing (pure; the tests feed it recorded output) ---------------------------------------------------------
22
+ /** The version from `wsb --version` ("0.1.32.0", "wsb 1.2.3+abc"). */
23
+ export const parseVersion = (out) => /\d+\.\d+(?:\.\d+){0,2}/.exec(out)?.[0] ?? null;
24
+ /** Subcommand names from `wsb --help` (the lines under "Commands:"). */
25
+ export const parseCommands = (help) => {
26
+ const lines = help.split(/\r?\n/);
27
+ const start = lines.findIndex((l) => /^\s*commands:\s*$/i.test(l));
28
+ if (start < 0)
29
+ return [];
30
+ const names = [];
31
+ for (const line of lines.slice(start + 1)) {
32
+ if (/^\S/.test(line) && line.trim())
33
+ break;
34
+ const m = /^\s{1,8}([a-z][a-z-]*)\b/.exec(line);
35
+ if (m)
36
+ names.push(m[1].toLowerCase());
37
+ }
38
+ return names;
39
+ };
40
+ /** `wsb list --raw` (JSON: an array, or an object holding one) or the plain table: the sandboxes and their status. */
41
+ export const parseList = (out) => {
42
+ const fromObject = (o) => {
43
+ if (!o || typeof o !== 'object')
44
+ return null;
45
+ const entries = Object.entries(o);
46
+ const id = entries.find(([k, v]) => /^id$/i.test(k) && typeof v === 'string' && GUID.test(v))?.[1];
47
+ const status = entries.find(([k]) => /^(status|state)$/i.test(k))?.[1];
48
+ return id ? { id: GUID.exec(id)[0], status: typeof status === 'string' ? status : 'unknown' } : null;
49
+ };
50
+ try {
51
+ const parsed = JSON.parse(out);
52
+ const list = Array.isArray(parsed) ? parsed : Object.values(parsed).find(Array.isArray) ?? [parsed];
53
+ return list.map(fromObject).filter((s) => s !== null);
54
+ }
55
+ catch { /* the table */ }
56
+ return out.split(/\r?\n/).flatMap((line) => {
57
+ const id = GUID.exec(line)?.[0];
58
+ if (!id)
59
+ return [];
60
+ return [{ id, status: /\brunning\b/i.test(line) ? 'Running' : /\bstopped\b/i.test(line) ? 'Stopped' : 'unknown' }];
61
+ });
62
+ };
63
+ /** `wsb ip --id <id>`: the first IPv4 address in the output. */
64
+ export const parseIp = (out) => /\b(?:\d{1,3}\.){3}\d{1,3}\b/.exec(out)?.[0] ?? null;
65
+ const call = (exe, args, timeoutMs = 60000) => {
66
+ const r = spawnSync(exe, args, { encoding: 'utf8', windowsHide: true, timeout: timeoutMs, maxBuffer: MAX_OUTPUT });
67
+ const stdout = String(r.stdout ?? '').slice(0, MAX_OUTPUT);
68
+ const stderr = String(r.stderr ?? '').slice(0, MAX_OUTPUT);
69
+ if (r.error)
70
+ return { ok: false, stdout, stderr, error: r.error.message };
71
+ return { ok: r.status === 0, stdout, stderr, ...(r.status === 0 ? {} : { error: (stderr || stdout).trim().split(/\r?\n/).slice(-3).join(' ') || 'exit code ' + r.status }) };
72
+ };
73
+ /** The CLI and what it offers, or null on Windows versions without it. */
74
+ export const detectWsb = (env = process.env) => {
75
+ const exe = findWsb(env);
76
+ if (!exe)
77
+ return null;
78
+ const version = parseVersion(call(exe, ['--version'], 15000).stdout);
79
+ const help = call(exe, ['--help'], 15000);
80
+ const commands = parseCommands(help.stdout + '\n' + help.stderr);
81
+ // An unreadable help text is not proof of anything: assume the documented commands.
82
+ const known = commands.length ? commands : ['start', 'list', 'exec', 'stop', 'share', 'connect', 'ip'];
83
+ return { exe, version, commands: known, has: (c) => known.includes(c) };
84
+ };
85
+ /** The running sandbox's id: the only running one (with several, none is guessed). */
86
+ export const runningSandboxId = (wsb) => {
87
+ const listed = call(wsb.exe, ['list', '--raw'], 20000);
88
+ const all = parseList(listed.ok ? listed.stdout : call(wsb.exe, ['list'], 20000).stdout);
89
+ const running = all.filter((s) => !/stopped/i.test(s.status));
90
+ if (running.length === 1)
91
+ return { id: running[0].id };
92
+ return { id: null, error: running.length ? running.length + ' sandboxes are running (wsb list); cannot tell which one is ours' : 'wsb list shows no running sandbox (one started from a .wsb file may not be listed)' };
93
+ };
94
+ /** Shares a folder of this PC into the running sandbox at `sandboxPath` (read/write unless readOnly). */
95
+ export const shareFolder = (wsb, id, hostPath, sandboxPath, readOnly = false) => {
96
+ refuseInSelfTest('share a folder into Windows Sandbox');
97
+ if (!GUID.test(id))
98
+ return { ok: false, stdout: '', stderr: '', error: 'not a sandbox id: ' + id };
99
+ return call(wsb.exe, ['share', '--id', id, '-f', hostPath, '-s', sandboxPath, ...(readOnly ? [] : ['--allow-write'])]);
100
+ };
101
+ export const sandboxIp = (wsb, id) => (GUID.test(id) ? parseIp(call(wsb.exe, ['ip', '--id', id], 15000).stdout) : null);