windows-development-sandbox 1.0.5 → 1.4.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 (453) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +25 -8
  3. package/config.example.json +55 -5
  4. package/dist/api.js +2 -1
  5. package/dist/cli/args.d.ts +23 -0
  6. package/dist/cli/args.js +38 -4
  7. package/dist/cli/colors.d.ts +24 -0
  8. package/dist/cli/colors.js +42 -0
  9. package/dist/cli/commands/accounts.js +2 -2
  10. package/dist/cli/commands/agent-commands.d.ts +6 -0
  11. package/dist/cli/commands/agent-commands.js +61 -0
  12. package/dist/cli/commands/context.d.ts +2 -0
  13. package/dist/cli/commands/context.js +5 -1
  14. package/dist/cli/commands/data/clean.d.ts +15 -0
  15. package/dist/cli/commands/data/clean.js +106 -0
  16. package/dist/cli/commands/data/collect.d.ts +11 -0
  17. package/dist/cli/commands/data/collect.js +54 -0
  18. package/dist/cli/commands/data/index.d.ts +4 -0
  19. package/dist/cli/commands/data/index.js +45 -0
  20. package/dist/cli/commands/data/remove.d.ts +2 -0
  21. package/dist/cli/commands/data/remove.js +30 -0
  22. package/dist/cli/commands/index.d.ts +6 -4
  23. package/dist/cli/commands/index.js +6 -4
  24. package/dist/cli/commands/launch.d.ts +26 -2
  25. package/dist/cli/commands/launch.js +256 -42
  26. package/dist/cli/commands/maintenance.d.ts +2 -0
  27. package/dist/cli/commands/maintenance.js +67 -5
  28. package/dist/cli/commands/project.d.ts +29 -0
  29. package/dist/cli/commands/project.js +63 -0
  30. package/dist/cli/commands/projects.d.ts +14 -0
  31. package/dist/cli/commands/projects.js +146 -0
  32. package/dist/cli/commands/repair/checks.d.ts +2 -0
  33. package/dist/cli/commands/{repair.js → repair/checks.js} +19 -181
  34. package/dist/cli/commands/{repair.d.ts → repair/finders.d.ts} +0 -29
  35. package/dist/cli/commands/repair/finders.js +108 -0
  36. package/dist/cli/commands/repair/index.d.ts +12 -0
  37. package/dist/cli/commands/repair/index.js +61 -0
  38. package/dist/cli/commands/repair/types.d.ts +26 -0
  39. package/dist/cli/commands/repair/types.js +3 -0
  40. package/dist/cli/commands/restart.d.ts +6 -0
  41. package/dist/cli/commands/restart.js +85 -0
  42. package/dist/cli/commands/sessions.d.ts +2 -1
  43. package/dist/cli/commands/sessions.js +189 -45
  44. package/dist/cli/commands/setup/checks.d.ts +13 -0
  45. package/dist/cli/commands/setup/checks.js +71 -0
  46. package/dist/cli/commands/{setup.d.ts → setup/facts.d.ts} +1 -15
  47. package/dist/cli/commands/setup/facts.js +60 -0
  48. package/dist/cli/commands/setup/index.d.ts +2 -0
  49. package/dist/cli/commands/setup/index.js +43 -0
  50. package/dist/cli/commands/user-config.d.ts +8 -0
  51. package/dist/cli/commands/user-config.js +41 -0
  52. package/dist/cli/history.d.ts +11 -0
  53. package/dist/cli/history.js +21 -0
  54. package/dist/cli/index.d.ts +7 -1
  55. package/dist/cli/index.js +42 -15
  56. package/dist/cli/manifest.d.ts +8 -0
  57. package/dist/cli/manifest.js +40 -15
  58. package/dist/cli/menu/apps.d.ts +21 -0
  59. package/dist/cli/menu/apps.js +122 -0
  60. package/dist/cli/menu/command-form.d.ts +28 -0
  61. package/dist/cli/menu/command-form.js +219 -0
  62. package/dist/cli/menu/index.d.ts +26 -0
  63. package/dist/cli/menu/index.js +180 -0
  64. package/dist/cli/menu/settings/categories.d.ts +9 -0
  65. package/dist/cli/menu/settings/categories.js +28 -0
  66. package/dist/cli/menu/settings/config-file.d.ts +40 -0
  67. package/dist/cli/menu/settings/config-file.js +71 -0
  68. package/dist/cli/menu/settings/editors.d.ts +30 -0
  69. package/dist/cli/menu/settings/editors.js +305 -0
  70. package/dist/cli/menu/settings/index.d.ts +32 -0
  71. package/dist/cli/menu/settings/index.js +136 -0
  72. package/dist/cli/menu/settings/schema-node.d.ts +62 -0
  73. package/dist/cli/menu/settings/schema-node.js +104 -0
  74. package/dist/cli/menu/several.d.ts +20 -0
  75. package/dist/cli/menu/several.js +102 -0
  76. package/dist/cli/progress.d.ts +9 -0
  77. package/dist/cli/progress.js +49 -0
  78. package/dist/cli/tui/app.d.ts +39 -0
  79. package/dist/cli/tui/app.js +52 -0
  80. package/dist/cli/tui/dialogs.d.ts +57 -0
  81. package/dist/cli/tui/dialogs.js +162 -0
  82. package/dist/cli/tui/frame.d.ts +18 -0
  83. package/dist/cli/tui/frame.js +27 -0
  84. package/dist/cli/tui/index.d.ts +7 -0
  85. package/dist/cli/tui/index.js +8 -0
  86. package/dist/cli/tui/list-screen.d.ts +101 -0
  87. package/dist/cli/tui/list-screen.js +299 -0
  88. package/dist/cli/tui/terminal.d.ts +37 -0
  89. package/dist/cli/tui/terminal.js +89 -0
  90. package/dist/cli/tui/text.d.ts +20 -0
  91. package/dist/cli/tui/text.js +105 -0
  92. package/dist/cli/tui/theme.d.ts +45 -0
  93. package/dist/cli/tui/theme.js +30 -0
  94. package/dist/cli/validate.d.ts +17 -0
  95. package/dist/cli/validate.js +89 -0
  96. package/dist/core/config.js +12 -6
  97. package/dist/core/disk.d.ts +16 -0
  98. package/dist/core/disk.js +55 -0
  99. package/dist/core/local-config.d.ts +23 -0
  100. package/dist/core/local-config.js +79 -7
  101. package/dist/core/project-policy.d.ts +15 -0
  102. package/dist/core/project-policy.js +35 -0
  103. package/dist/core/prompt.d.ts +4 -0
  104. package/dist/core/prompt.js +15 -0
  105. package/dist/core/safe-paths.d.ts +4 -0
  106. package/dist/core/safe-paths.js +37 -0
  107. package/dist/core/schema/apps.d.ts +40 -0
  108. package/dist/core/schema/apps.js +27 -0
  109. package/dist/core/schema/common.d.ts +17 -0
  110. package/dist/core/schema/common.js +15 -0
  111. package/dist/core/schema/config.d.ts +659 -0
  112. package/dist/core/schema/config.js +75 -0
  113. package/dist/core/schema/databases.d.ts +33 -0
  114. package/dist/core/schema/databases.js +35 -0
  115. package/dist/core/schema/harness.d.ts +12 -0
  116. package/dist/core/schema/harness.js +17 -0
  117. package/dist/core/schema/json-schema.d.ts +5 -0
  118. package/dist/core/schema/json-schema.js +25 -0
  119. package/dist/core/schema/profile.d.ts +32 -0
  120. package/dist/core/schema/profile.js +31 -0
  121. package/dist/core/schema/projects.d.ts +9 -0
  122. package/dist/core/schema/projects.js +11 -0
  123. package/dist/core/schema/sandbox.d.ts +78 -0
  124. package/dist/core/schema/sandbox.js +64 -0
  125. package/dist/core/schema/toolchains.d.ts +96 -0
  126. package/dist/core/schema/toolchains.js +88 -0
  127. package/dist/core/schema.d.ts +6 -604
  128. package/dist/core/schema.js +8 -264
  129. package/dist/core/types.d.ts +35 -1
  130. package/dist/core/util.d.ts +9 -0
  131. package/dist/core/util.js +14 -3
  132. package/dist/core/workspace.d.ts +0 -5
  133. package/dist/core/workspace.js +33 -50
  134. package/dist/guest/apps/index.d.ts +10 -0
  135. package/dist/guest/apps/index.js +206 -0
  136. package/dist/guest/apps/resolvers.d.ts +8 -0
  137. package/dist/guest/apps/resolvers.js +90 -0
  138. package/dist/guest/audit-parse.d.ts +7 -0
  139. package/dist/guest/audit-parse.js +24 -0
  140. package/dist/guest/audit.d.ts +4 -0
  141. package/dist/guest/audit.js +47 -0
  142. package/dist/guest/boot-env.d.ts +4 -0
  143. package/dist/guest/boot-env.js +25 -0
  144. package/dist/guest/cmdline.d.ts +12 -0
  145. package/dist/guest/cmdline.js +23 -0
  146. package/dist/guest/daemon/exec.d.ts +5 -0
  147. package/dist/guest/daemon/exec.js +32 -0
  148. package/dist/guest/daemon/project.d.ts +14 -0
  149. package/dist/guest/daemon/project.js +132 -0
  150. package/dist/guest/daemon/relays.d.ts +1 -0
  151. package/dist/guest/daemon/relays.js +18 -0
  152. package/dist/guest/daemon/session.d.ts +13 -0
  153. package/dist/guest/daemon/session.js +139 -0
  154. package/dist/guest/daemon/shutdown.d.ts +1 -0
  155. package/dist/guest/daemon/shutdown.js +21 -0
  156. package/dist/guest/daemon/state.d.ts +17 -0
  157. package/dist/guest/daemon/state.js +31 -0
  158. package/dist/guest/daemon.js +12 -141
  159. package/dist/guest/db.js +18 -3
  160. package/dist/guest/env.d.ts +3 -0
  161. package/dist/guest/env.js +7 -3
  162. package/dist/guest/guide.d.ts +6 -2
  163. package/dist/guest/guide.js +5 -4
  164. package/dist/guest/helpers.d.ts +3 -0
  165. package/dist/guest/helpers.js +26 -0
  166. package/dist/guest/main.js +31 -8
  167. package/dist/guest/netproxy.d.ts +1 -0
  168. package/dist/guest/netproxy.js +100 -0
  169. package/dist/guest/ports.d.ts +6 -0
  170. package/dist/guest/ports.js +22 -0
  171. package/dist/guest/project-context.d.ts +7 -0
  172. package/dist/guest/project-context.js +10 -0
  173. package/dist/guest/project-env.d.ts +20 -0
  174. package/dist/guest/project-env.js +20 -0
  175. package/dist/guest/project-status.d.ts +5 -0
  176. package/dist/guest/project-status.js +17 -0
  177. package/dist/guest/ptyhost.d.ts +0 -18
  178. package/dist/guest/ptyhost.js +17 -65
  179. package/dist/guest/setup/agents.d.ts +5 -2
  180. package/dist/guest/setup/agents.js +22 -8
  181. package/dist/guest/setup/claude.d.ts +0 -4
  182. package/dist/guest/setup/claude.js +59 -5
  183. package/dist/guest/setup/environment.d.ts +13 -3
  184. package/dist/guest/setup/environment.js +14 -10
  185. package/dist/guest/setup/hooks.d.ts +9 -1
  186. package/dist/guest/setup/hooks.js +28 -14
  187. package/dist/guest/setup/network.d.ts +9 -0
  188. package/dist/guest/setup/network.js +97 -4
  189. package/dist/guest/setup/user.js +4 -0
  190. package/dist/guest/toolchains/catalogue.d.ts +4 -0
  191. package/dist/guest/toolchains/catalogue.js +2 -2
  192. package/dist/guest/toolchains/cli.js +25 -17
  193. package/dist/guest/toolchains/common.d.ts +18 -1
  194. package/dist/guest/toolchains/common.js +34 -11
  195. package/dist/guest/toolchains/databases/dynamodb.d.ts +8 -0
  196. package/dist/guest/toolchains/databases/dynamodb.js +52 -0
  197. package/dist/guest/toolchains/{databases.d.ts → databases/index.d.ts} +2 -3
  198. package/dist/guest/toolchains/databases/index.js +62 -0
  199. package/dist/guest/toolchains/databases/mongodb.d.ts +3 -0
  200. package/dist/guest/toolchains/databases/mongodb.js +31 -0
  201. package/dist/guest/toolchains/databases/mysql.d.ts +3 -0
  202. package/dist/guest/toolchains/databases/mysql.js +38 -0
  203. package/dist/guest/toolchains/databases/postgres.d.ts +5 -0
  204. package/dist/guest/toolchains/databases/postgres.js +38 -0
  205. package/dist/guest/toolchains/databases/redis.d.ts +3 -0
  206. package/dist/guest/toolchains/databases/redis.js +16 -0
  207. package/dist/guest/toolchains/databases/server.d.ts +14 -0
  208. package/dist/guest/toolchains/databases/server.js +52 -0
  209. package/dist/guest/toolchains/git.js +25 -16
  210. package/dist/guest/toolchains/go.js +1 -1
  211. package/dist/guest/toolchains/index.d.ts +13 -9
  212. package/dist/guest/toolchains/index.js +40 -82
  213. package/dist/guest/toolchains/java.js +5 -5
  214. package/dist/guest/toolchains/leftovers.d.ts +2 -0
  215. package/dist/guest/toolchains/leftovers.js +26 -0
  216. package/dist/guest/toolchains/managers.js +12 -10
  217. package/dist/guest/toolchains/{binstall.js → native/binstall.js} +6 -9
  218. package/dist/guest/toolchains/native/cpp.d.ts +4 -0
  219. package/dist/guest/toolchains/native/cpp.js +38 -0
  220. package/dist/guest/toolchains/native/dotnet.d.ts +4 -0
  221. package/dist/guest/toolchains/native/dotnet.js +103 -0
  222. package/dist/guest/toolchains/native/mesa.d.ts +7 -0
  223. package/dist/guest/toolchains/native/mesa.js +56 -0
  224. package/dist/guest/toolchains/native/msvc/environment.d.ts +16 -0
  225. package/dist/guest/toolchains/native/msvc/environment.js +57 -0
  226. package/dist/guest/toolchains/native/msvc/index.d.ts +6 -0
  227. package/dist/guest/toolchains/native/msvc/index.js +48 -0
  228. package/dist/guest/toolchains/native/msvc/packages.d.ts +6 -0
  229. package/dist/guest/toolchains/native/msvc/packages.js +156 -0
  230. package/dist/guest/toolchains/native/msvc/tree.d.ts +15 -0
  231. package/dist/guest/toolchains/native/msvc/tree.js +21 -0
  232. package/dist/guest/toolchains/native/rust.d.ts +3 -0
  233. package/dist/guest/toolchains/native/rust.js +45 -0
  234. package/dist/guest/toolchains/node.js +26 -6
  235. package/dist/guest/toolchains/plan.d.ts +24 -0
  236. package/dist/guest/toolchains/plan.js +140 -0
  237. package/dist/guest/toolchains/pwsh.js +1 -1
  238. package/dist/guest/toolchains/python.js +43 -12
  239. package/dist/guest/toolchains/runtimes.d.ts +2 -0
  240. package/dist/guest/toolchains/runtimes.js +6 -8
  241. package/dist/guest/toolchains/scripting/php.d.ts +3 -0
  242. package/dist/guest/toolchains/scripting/php.js +71 -0
  243. package/dist/guest/toolchains/scripting/ruby.d.ts +3 -0
  244. package/dist/guest/toolchains/scripting/ruby.js +47 -0
  245. package/dist/guest/toolchains/scripting/vcruntime.d.ts +7 -0
  246. package/dist/guest/toolchains/scripting/vcruntime.js +45 -0
  247. package/dist/guest/toolchains/vscode-profile.d.ts +30 -0
  248. package/dist/guest/toolchains/vscode-profile.js +69 -0
  249. package/dist/guest/toolchains/vscode.d.ts +17 -0
  250. package/dist/guest/toolchains/vscode.js +87 -1
  251. package/dist/guest/view.js +21 -1
  252. package/dist/host/accounts.js +14 -5
  253. package/dist/host/agent-commands.d.ts +25 -0
  254. package/dist/host/agent-commands.js +145 -0
  255. package/dist/host/attach.d.ts +17 -0
  256. package/dist/host/attach.js +70 -0
  257. package/dist/host/audit/collect.d.ts +34 -0
  258. package/dist/host/audit/collect.js +126 -0
  259. package/dist/host/audit/log.d.ts +46 -0
  260. package/dist/host/audit/log.js +143 -0
  261. package/dist/host/channel.d.ts +23 -22
  262. package/dist/host/channel.js +66 -84
  263. package/dist/host/claude-profile.js +7 -3
  264. package/dist/host/cleanup.js +2 -11
  265. package/dist/host/database-plan.d.ts +12 -0
  266. package/dist/host/database-plan.js +52 -0
  267. package/dist/host/generate.d.ts +10 -12
  268. package/dist/host/generate.js +58 -135
  269. package/dist/host/join.d.ts +28 -0
  270. package/dist/host/join.js +76 -0
  271. package/dist/host/launch/mappings.d.ts +3 -0
  272. package/dist/host/launch/mappings.js +54 -0
  273. package/dist/host/launch/projects.d.ts +15 -0
  274. package/dist/host/launch/projects.js +56 -0
  275. package/dist/host/launch/stage.d.ts +11 -0
  276. package/dist/host/launch/stage.js +66 -0
  277. package/dist/host/launch/wsb.d.ts +10 -0
  278. package/dist/host/launch/wsb.js +34 -0
  279. package/dist/host/motw.d.ts +17 -0
  280. package/dist/host/motw.js +74 -0
  281. package/dist/host/pc-scan.d.ts +6 -0
  282. package/dist/host/pc-scan.js +37 -0
  283. package/dist/host/platform.d.ts +2 -0
  284. package/dist/host/platform.js +16 -1
  285. package/dist/host/port-forward.d.ts +8 -0
  286. package/dist/host/port-forward.js +37 -0
  287. package/dist/host/project-scan.d.ts +1 -17
  288. package/dist/host/project-scan.js +2 -86
  289. package/dist/host/projects.d.ts +100 -0
  290. package/dist/host/projects.js +147 -0
  291. package/dist/host/repo-guard.d.ts +25 -5
  292. package/dist/host/repo-guard.js +242 -23
  293. package/dist/host/stops.d.ts +67 -0
  294. package/dist/host/stops.js +126 -0
  295. package/dist/host/terminal.d.ts +8 -0
  296. package/dist/host/terminal.js +48 -1
  297. package/dist/host/test-guard.d.ts +6 -0
  298. package/dist/host/test-guard.js +15 -0
  299. package/dist/host/window.js +7 -3
  300. package/dist/host/workspace-view/index.d.ts +20 -0
  301. package/dist/host/workspace-view/index.js +53 -0
  302. package/dist/host/workspace-view/rules.d.ts +26 -0
  303. package/dist/host/workspace-view/rules.js +93 -0
  304. package/dist/host/workspace-view/view.d.ts +50 -0
  305. package/dist/host/workspace-view/view.js +281 -0
  306. package/dist/host/wsb.d.ts +37 -0
  307. package/dist/host/wsb.js +101 -0
  308. package/dist/runtime/daemon.js +11791 -2821
  309. package/dist/runtime/db.js +6585 -466
  310. package/dist/runtime/main.js +10238 -1724
  311. package/dist/runtime/netproxy.js +25936 -0
  312. package/dist/runtime/view.js +6183 -59
  313. package/dist/selftest/accounts.test.d.ts +2 -0
  314. package/dist/selftest/accounts.test.js +44 -0
  315. package/dist/selftest/agent-commands.test.d.ts +2 -0
  316. package/dist/selftest/agent-commands.test.js +47 -0
  317. package/dist/selftest/audit.test.d.ts +2 -0
  318. package/dist/selftest/audit.test.js +57 -0
  319. package/dist/selftest/channel.test.d.ts +2 -0
  320. package/dist/selftest/channel.test.js +44 -0
  321. package/dist/selftest/cli.test.d.ts +2 -2
  322. package/dist/selftest/cli.test.js +145 -27
  323. package/dist/selftest/cmdline.test.d.ts +2 -0
  324. package/dist/selftest/cmdline.test.js +45 -0
  325. package/dist/selftest/config.test.d.ts +2 -3
  326. package/dist/selftest/config.test.js +36 -72
  327. package/dist/selftest/data.test.d.ts +2 -2
  328. package/dist/selftest/data.test.js +77 -70
  329. package/dist/selftest/detect.test.d.ts +2 -2
  330. package/dist/selftest/detect.test.js +54 -38
  331. package/dist/selftest/fixtures.d.ts +89 -4
  332. package/dist/selftest/fixtures.js +43 -24
  333. package/dist/selftest/generate.test.d.ts +2 -0
  334. package/dist/selftest/generate.test.js +68 -0
  335. package/dist/selftest/git-access.test.d.ts +2 -0
  336. package/dist/selftest/git-access.test.js +26 -0
  337. package/dist/selftest/guest-boot.test.d.ts +2 -0
  338. package/dist/selftest/guest-boot.test.js +76 -0
  339. package/dist/selftest/harness.d.ts +44 -7
  340. package/dist/selftest/harness.js +62 -21
  341. package/dist/selftest/host-sim.test.d.ts +2 -0
  342. package/dist/selftest/host-sim.test.js +93 -0
  343. package/dist/selftest/hostile-input.test.d.ts +2 -0
  344. package/dist/selftest/hostile-input.test.js +54 -0
  345. package/dist/selftest/index.d.ts +8 -1
  346. package/dist/selftest/index.js +137 -35
  347. package/dist/selftest/links.test.d.ts +2 -0
  348. package/dist/selftest/links.test.js +50 -0
  349. package/dist/selftest/live-daemon.test.d.ts +2 -0
  350. package/dist/selftest/live-daemon.test.js +91 -0
  351. package/dist/selftest/live-git.test.d.ts +2 -0
  352. package/dist/selftest/live-git.test.js +60 -0
  353. package/dist/selftest/live-network.test.d.ts +2 -0
  354. package/dist/selftest/live-network.test.js +63 -0
  355. package/dist/selftest/live-projects.test.d.ts +2 -0
  356. package/dist/selftest/live-projects.test.js +127 -0
  357. package/dist/selftest/live.d.ts +65 -0
  358. package/dist/selftest/live.js +109 -0
  359. package/dist/selftest/local-config.test.d.ts +2 -0
  360. package/dist/selftest/local-config.test.js +120 -0
  361. package/dist/selftest/mirror.test.d.ts +2 -3
  362. package/dist/selftest/mirror.test.js +92 -65
  363. package/dist/selftest/network.test.d.ts +2 -0
  364. package/dist/selftest/network.test.js +40 -0
  365. package/dist/selftest/personalization.test.d.ts +2 -0
  366. package/dist/selftest/personalization.test.js +53 -0
  367. package/dist/selftest/projects.test.d.ts +2 -0
  368. package/dist/selftest/projects.test.js +254 -0
  369. package/dist/selftest/repair.test.d.ts +2 -0
  370. package/dist/selftest/repair.test.js +42 -0
  371. package/dist/selftest/repo-guard.test.d.ts +2 -0
  372. package/dist/selftest/repo-guard.test.js +121 -0
  373. package/dist/selftest/secrets.test.d.ts +2 -0
  374. package/dist/selftest/secrets.test.js +71 -0
  375. package/dist/selftest/setup.test.d.ts +2 -2
  376. package/dist/selftest/setup.test.js +45 -22
  377. package/dist/selftest/suite.d.ts +13 -0
  378. package/dist/selftest/suite.js +1 -0
  379. package/dist/selftest/sync.test.d.ts +2 -3
  380. package/dist/selftest/sync.test.js +130 -78
  381. package/dist/selftest/test-guard.test.d.ts +2 -0
  382. package/dist/selftest/test-guard.test.js +49 -0
  383. package/dist/selftest/toolchains.test.d.ts +2 -0
  384. package/dist/selftest/toolchains.test.js +133 -0
  385. package/dist/selftest/workspace-view.test.d.ts +2 -0
  386. package/dist/selftest/workspace-view.test.js +89 -0
  387. package/dist/selftest/workspace.test.d.ts +2 -0
  388. package/dist/selftest/workspace.test.js +53 -0
  389. package/dist/shared/apps.d.ts +49 -0
  390. package/dist/shared/apps.js +114 -0
  391. package/dist/shared/download.d.ts +3 -1
  392. package/dist/shared/download.js +19 -4
  393. package/dist/shared/netpolicy.d.ts +27 -0
  394. package/dist/shared/netpolicy.js +51 -0
  395. package/dist/shared/paths.d.ts +5 -0
  396. package/dist/shared/paths.js +6 -0
  397. package/dist/shared/projects.d.ts +53 -0
  398. package/dist/shared/projects.js +67 -0
  399. package/dist/shared/pty-client.d.ts +9 -4
  400. package/dist/shared/pty-client.js +139 -17
  401. package/dist/shared/rundir.d.ts +12 -1
  402. package/dist/shared/rundir.js +14 -4
  403. package/dist/shared/safe-remove.js +11 -0
  404. package/dist/shared/schema.d.ts +453 -0
  405. package/dist/shared/schema.js +43 -2
  406. package/dist/shared/system.d.ts +1 -0
  407. package/dist/shared/system.js +1 -0
  408. package/dist/shared/test-guard.d.ts +4 -0
  409. package/dist/shared/test-guard.js +37 -0
  410. package/dist/sync/claude-mem.js +6 -3
  411. package/dist/sync/history.d.ts +29 -12
  412. package/dist/sync/history.js +72 -25
  413. package/dist/sync/watcher.d.ts +8 -0
  414. package/dist/sync/watcher.js +99 -8
  415. package/package.json +7 -6
  416. package/sandbox/bootstrap.ps1 +258 -258
  417. package/sandbox/cargoshim.cs +56 -0
  418. package/sandbox/cpucap.cs +33 -0
  419. package/sandbox/cpucap.ps1 +20 -0
  420. package/sandbox/logon.cmd +24 -16
  421. package/sandbox/memlog.ps1 +34 -0
  422. package/sandbox/openssl.cs +42 -0
  423. package/sandbox/pagefile.cs +31 -0
  424. package/sandbox/pagefile.ps1 +21 -0
  425. package/sandbox/ptyhost.cs +120 -11
  426. package/sandbox/trust.cmd +35 -35
  427. package/schemas/config.schema.json +403 -3
  428. package/schemas/sandbox-config.schema.json +143 -13
  429. package/dist/cli/commands/data.d.ts +0 -46
  430. package/dist/cli/commands/data.js +0 -249
  431. package/dist/cli/commands/setup.js +0 -152
  432. package/dist/cli/menu.d.ts +0 -22
  433. package/dist/cli/menu.js +0 -187
  434. package/dist/cli/settings.d.ts +0 -11
  435. package/dist/cli/settings.js +0 -156
  436. package/dist/guest/toolchains/databases.js +0 -226
  437. package/dist/guest/toolchains/msvc.d.ts +0 -10
  438. package/dist/guest/toolchains/msvc.js +0 -216
  439. package/dist/guest/toolchains/native.d.ts +0 -12
  440. package/dist/guest/toolchains/native.js +0 -220
  441. package/dist/guest/toolchains/scripting.d.ts +0 -10
  442. package/dist/guest/toolchains/scripting.js +0 -137
  443. package/dist/selftest/guest.test.d.ts +0 -3
  444. package/dist/selftest/guest.test.js +0 -61
  445. package/dist/selftest/helpers.test.d.ts +0 -2
  446. package/dist/selftest/helpers.test.js +0 -125
  447. package/dist/selftest/host.test.d.ts +0 -2
  448. package/dist/selftest/host.test.js +0 -71
  449. package/dist/selftest/live.test.d.ts +0 -7
  450. package/dist/selftest/live.test.js +0 -127
  451. package/dist/selftest/security.test.d.ts +0 -3
  452. package/dist/selftest/security.test.js +0 -143
  453. /package/dist/guest/toolchains/{binstall.d.ts → native/binstall.d.ts} +0 -0
@@ -2,8 +2,9 @@
2
2
  // logon.cmd can open it the moment the sandbox logs in (no PowerShell window first).
3
3
  import fs from 'node:fs';
4
4
  import path from 'node:path';
5
- import { githubAsset, installArchive } from '../shared/download.js';
5
+ import { download, extractArchive, fetchJson, githubAsset, installArchive, moveIntoPlace, stagingFolder } 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;
@@ -86,9 +88,54 @@ export const ensureTerminal = async (dataPath, refresh, options = {}) => {
86
88
  }
87
89
  return result;
88
90
  };
91
+ // ---- conpty.dll ------------------------------------------------------------------------------------------------------
92
+ /** Where the sandbox finds conpty.dll + OpenConsole.exe (guest/ptyhost.ts points ptyhost at it). */
93
+ export const conptyDir = (dataPath) => path.join(dataPath, 'tools', 'conpty');
94
+ const NUGET_CONPTY = 'https://api.nuget.org/v3-flatcontainer/microsoft.windows.console.conpty/';
95
+ const NugetVersions = z.object({ versions: z.array(z.string()) });
96
+ const NugetLeaf = z.object({ catalogEntry: z.string().url() });
97
+ const NugetCatalog = z.object({ packageHash: z.string(), packageHashAlgorithm: z.literal('SHA512') });
98
+ /**
99
+ * Microsoft's newer pseudo console (the Microsoft.Windows.Console.ConPTY package: conpty.dll and OpenConsole.exe, the
100
+ * same one Windows Terminal and VS Code use), downloaded once and verified against the SHA-512 NuGet publishes. Agent
101
+ * sessions use it instead of the one built into Windows, which re-renders the screen and garbles it after resizes.
102
+ */
103
+ export const ensureConpty = async (dataPath, refresh) => {
104
+ const dir = conptyDir(dataPath);
105
+ assertTestWritable(dir);
106
+ assertNoLinks(dataPath, dir);
107
+ if (fileExists(path.join(dir, 'conpty.dll')) && fileExists(path.join(dir, 'OpenConsole.exe')) && !refresh)
108
+ return 'cached';
109
+ const arch = process.arch === 'arm64' ? 'arm64' : 'x64';
110
+ const work = stagingFolder(dir);
111
+ try {
112
+ const version = NugetVersions.parse(await fetchJson(NUGET_CONPTY + 'index.json')).versions.filter((v) => !v.includes('-')).pop();
113
+ if (!version)
114
+ throw new Error('no release found');
115
+ const leaf = NugetLeaf.parse(await fetchJson('https://api.nuget.org/v3/registration5-gz-semver2/microsoft.windows.console.conpty/' + version + '.json'));
116
+ const sha512 = Buffer.from(NugetCatalog.parse(await fetchJson(leaf.catalogEntry)).packageHash, 'base64').toString('hex');
117
+ const pkg = path.join(work, 'conpty.nupkg');
118
+ await download(NUGET_CONPTY + version + '/microsoft.windows.console.conpty.' + version + '.nupkg', pkg, { sha512 });
119
+ const files = { 'conpty.dll': 'runtimes/win-' + arch + '/native/conpty.dll', 'OpenConsole.exe': 'build/native/runtimes/' + arch + '/OpenConsole.exe' };
120
+ await extractArchive(pkg, path.join(work, 'x'), Object.values(files));
121
+ fs.mkdirSync(path.join(work, 'out'));
122
+ for (const [name, member] of Object.entries(files))
123
+ fs.copyFileSync(path.join(work, 'x', ...member.split('/')), path.join(work, 'out', name));
124
+ fs.writeFileSync(path.join(work, 'out', 'VERSION'), version);
125
+ moveIntoPlace(path.join(work, 'out'), dir);
126
+ return 'downloaded ' + version;
127
+ }
128
+ catch (e) {
129
+ return 'not downloaded, sessions use Windows\' own pseudo console (' + errMsg(e) + ')';
130
+ }
131
+ finally {
132
+ safeRemove(work, path.dirname(work));
133
+ }
134
+ };
89
135
  const installTerminal = async (dataPath, refresh) => {
90
136
  const exe = terminalExe(dataPath);
91
137
  const dir = terminalDir(dataPath);
138
+ assertTestWritable(dir);
92
139
  const settings = path.join(dir, 'settings');
93
140
  // installArchive replaces tools\terminal: the portable settings folder waits here meanwhile (and goes back also when
94
141
  // 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,20 @@
1
+ import type { Config } from '../../core/types.js';
2
+ import { type ViewOptions } from './view.js';
3
+ export { IgnoreRules } from './rules.js';
4
+ export { WorkspaceView, type SyncStats, type ViewOptions } from './view.js';
5
+ /** The project's .git as the sandbox sees it: next to the workspace (C:\Workspace\app -> C:\Workspace\app.git). */
6
+ export declare const guestGitDir: (guestWorkspace: string) => string;
7
+ export interface ViewPlan {
8
+ options: ViewOptions; /** The project's real .git folder, mapped at options.guestGitDir (null: none). */
9
+ hostGit: string | null;
10
+ note?: string;
11
+ }
12
+ /**
13
+ * The view for a launch, or null when the folder itself is mapped: workspaceFilter off, or a git worktree (its .git is
14
+ * a file pointing elsewhere, which a view cannot follow; then the folder is mapped as before and a note says so).
15
+ */
16
+ export declare const planView: (config: Config, storageRoot: string, workspaceHost: string) => ViewPlan | null;
17
+ /** Why a filtered launch maps the folder itself after all (for the launch notes). */
18
+ export declare const viewSkipped: (config: Config, workspaceHost: string) => string | null;
19
+ export declare const saveActiveViews: (storageRoot: string, dataPath: string, views: ViewOptions[]) => void;
20
+ export declare const readActiveViews: (storageRoot: string, dataPath: string) => ViewOptions[];
@@ -0,0 +1,53 @@
1
+ // workspaceFilter in practice: which view a launch maps instead of the project, the project's .git mapped beside it,
2
+ // and the host-only list of views the background watcher keeps in step while the sandbox runs.
3
+ import fs from 'node:fs';
4
+ import path from 'node:path';
5
+ import { z } from 'zod';
6
+ import { readValid, writeJsonAtomic } from '../../shared/rundir.js';
7
+ import { WorkspaceView } from './view.js';
8
+ export { IgnoreRules } from './rules.js';
9
+ export { WorkspaceView } from './view.js';
10
+ /** The project's .git as the sandbox sees it: next to the workspace (C:\Workspace\app -> C:\Workspace\app.git). */
11
+ export const guestGitDir = (guestWorkspace) => guestWorkspace.replace(/[\\/]+$/, '') + '.git';
12
+ /**
13
+ * The view for a launch, or null when the folder itself is mapped: workspaceFilter off, or a git worktree (its .git is
14
+ * a file pointing elsewhere, which a view cannot follow; then the folder is mapped as before and a note says so).
15
+ */
16
+ export const planView = (config, storageRoot, workspaceHost) => {
17
+ if (!config.workspaceFilter.enabled)
18
+ return null;
19
+ const git = path.join(workspaceHost, '.git');
20
+ let gitIsDir = false;
21
+ try {
22
+ const s = fs.lstatSync(git);
23
+ if (s.isFile())
24
+ return null;
25
+ gitIsDir = s.isDirectory() && !s.isSymbolicLink();
26
+ }
27
+ catch { /* no .git */ }
28
+ // git access "none": the agent gets no .git at all.
29
+ const hostGit = gitIsDir && config.provisioning.git.enabled && config.provisioning.git.access !== 'none' ? git : null;
30
+ return {
31
+ options: { ...WorkspaceView.pathsFor(storageRoot, workspaceHost), exclude: config.workspaceFilter.exclude, include: config.workspaceFilter.include, guestGitDir: hostGit ? guestGitDir(config.guestWorkspace) : null },
32
+ hostGit,
33
+ };
34
+ };
35
+ /** Why a filtered launch maps the folder itself after all (for the launch notes). */
36
+ export const viewSkipped = (config, workspaceHost) => {
37
+ if (!config.workspaceFilter.enabled)
38
+ return null;
39
+ try {
40
+ if (fs.lstatSync(path.join(workspaceHost, '.git')).isFile())
41
+ return 'workspaceFilter: ' + workspaceHost + ' is a git worktree (.git is a file); the folder is mapped as it is, git-ignored files included.';
42
+ }
43
+ catch { /* fine */ }
44
+ return null;
45
+ };
46
+ // ---- the views the watcher keeps in step (host-only, next to the sync state) ------------------------------------------
47
+ const ActiveView = z.object({
48
+ host: z.string().max(1024), view: z.string().max(1024), state: z.string().max(1024),
49
+ exclude: z.array(z.string().max(500)).max(500), include: z.array(z.string().max(500)).max(500), guestGitDir: z.string().max(1024).nullable(),
50
+ });
51
+ const activeFile = (storageRoot, dataPath) => path.join(storageRoot, 'sync', path.basename(dataPath) + '-views.json');
52
+ export const saveActiveViews = (storageRoot, dataPath, views) => writeJsonAtomic(activeFile(storageRoot, dataPath), views.map((v) => ({ host: v.host, view: v.view, state: v.state, exclude: v.exclude ?? [], include: v.include ?? [], guestGitDir: v.guestGitDir ?? null })));
53
+ export const readActiveViews = (storageRoot, dataPath) => readValid(z.array(ActiveView).max(64), activeFile(storageRoot, dataPath)) ?? [];
@@ -0,0 +1,26 @@
1
+ export declare class IgnoreRules {
2
+ private readonly root;
3
+ private readonly include;
4
+ /** One matcher per folder that has a .gitignore (null = the folder has none), by its path relative to the root. */
5
+ private readonly perFolder;
6
+ private readonly extra;
7
+ private readonly forced;
8
+ /** `root`: the project folder on this PC (its .gitignore files are the rules); `exclude` / `include`: your patterns. */
9
+ constructor(root: string, exclude?: string[], include?: string[]);
10
+ /** One of your include patterns lets this path in, whatever .gitignore says (even inside an ignored folder). */
11
+ isForced(rel: string): boolean;
12
+ /** Could one of your include patterns match something inside this ignored folder (so a walk must look inside)? */
13
+ mayIncludeInside(dir: string): boolean;
14
+ /** Forget the .gitignore files read so far (one of them changed). */
15
+ reset(): void;
16
+ private matcher;
17
+ /**
18
+ * Is this one entry ignored, given that its parent folder is not? `rel` uses / and has no leading slash. Your include
19
+ * patterns win over everything, your exclude patterns over .gitignore.
20
+ */
21
+ private ignoredHere;
22
+ /** Is `rel` (a file, or a folder with isDir) ignored, counting its folders: inside an ignored folder is ignored. */
23
+ isIgnored(rel: string, isDir?: boolean): boolean;
24
+ /** For a walk that already checked the parent folder: is this child ignored? */
25
+ isIgnoredChild(rel: string, isDir: boolean): boolean;
26
+ }
@@ -0,0 +1,93 @@
1
+ // Which files of a project git ignores, read the way git reads them: the .gitignore in every folder (deeper files win,
2
+ // `!pattern` lets a file back in), .git/info/exclude, and nothing inside an ignored folder ever comes back. Plus your
3
+ // own extra patterns (workspaceFilter.exclude) and exceptions (workspaceFilter.include). Bounded reads: a .gitignore
4
+ // over 256 KB is treated as empty.
5
+ import fs from 'node:fs';
6
+ import path from 'node:path';
7
+ import ignore from 'ignore';
8
+ const MAX_RULES_BYTES = 256 * 1024;
9
+ const readRules = (file) => {
10
+ try {
11
+ const stat = fs.lstatSync(file);
12
+ return stat.isFile() && stat.size <= MAX_RULES_BYTES ? fs.readFileSync(file, 'utf8') : '';
13
+ }
14
+ catch {
15
+ return '';
16
+ }
17
+ };
18
+ export class IgnoreRules {
19
+ root;
20
+ include;
21
+ /** One matcher per folder that has a .gitignore (null = the folder has none), by its path relative to the root. */
22
+ perFolder = new Map();
23
+ extra;
24
+ forced;
25
+ /** `root`: the project folder on this PC (its .gitignore files are the rules); `exclude` / `include`: your patterns. */
26
+ constructor(root, exclude = [], include = []) {
27
+ this.root = root;
28
+ this.include = include;
29
+ this.extra = ignore().add(exclude);
30
+ this.forced = ignore().add(include);
31
+ }
32
+ /** One of your include patterns lets this path in, whatever .gitignore says (even inside an ignored folder). */
33
+ isForced(rel) { return this.include.length > 0 && this.forced.ignores(rel); }
34
+ /** Could one of your include patterns match something inside this ignored folder (so a walk must look inside)? */
35
+ mayIncludeInside(dir) {
36
+ return this.include.some((p) => { const clean = p.replace(/^\//, ''); return clean.startsWith('**') || clean.startsWith(dir + '/'); });
37
+ }
38
+ /** Forget the .gitignore files read so far (one of them changed). */
39
+ reset() { this.perFolder.clear(); }
40
+ matcher(folder) {
41
+ if (this.perFolder.has(folder))
42
+ return this.perFolder.get(folder);
43
+ const dir = path.join(this.root, ...folder.split('/').filter(Boolean));
44
+ let text = readRules(path.join(dir, '.gitignore'));
45
+ if (folder === '')
46
+ text += '\n' + readRules(path.join(this.root, '.git', 'info', 'exclude'));
47
+ const matcher = text.trim() ? ignore().add(text) : null;
48
+ this.perFolder.set(folder, matcher);
49
+ return matcher;
50
+ }
51
+ /**
52
+ * Is this one entry ignored, given that its parent folder is not? `rel` uses / and has no leading slash. Your include
53
+ * patterns win over everything, your exclude patterns over .gitignore.
54
+ */
55
+ ignoredHere(rel, isDir) {
56
+ const probe = isDir ? rel + '/' : rel;
57
+ if (this.forced.ignores(probe))
58
+ return false;
59
+ if (this.extra.ignores(probe))
60
+ return true;
61
+ const parts = rel.split('/');
62
+ let ignored = false;
63
+ for (let i = 0; i < parts.length; i++) {
64
+ const folder = parts.slice(0, i).join('/');
65
+ const m = this.matcher(folder);
66
+ if (!m)
67
+ continue;
68
+ const below = parts.slice(i).join('/') + (isDir ? '/' : '');
69
+ const result = m.test(below);
70
+ if (result.ignored)
71
+ ignored = true;
72
+ else if (result.unignored)
73
+ ignored = false;
74
+ }
75
+ return ignored;
76
+ }
77
+ /** Is `rel` (a file, or a folder with isDir) ignored, counting its folders: inside an ignored folder is ignored. */
78
+ isIgnored(rel, isDir = false) {
79
+ const parts = rel.split('/').filter(Boolean);
80
+ if (parts[0] === '.git')
81
+ return true;
82
+ if (!isDir && this.isForced(parts.join('/')))
83
+ return false;
84
+ for (let i = 1; i < parts.length; i++)
85
+ if (this.ignoredHere(parts.slice(0, i).join('/'), true))
86
+ return true;
87
+ return this.ignoredHere(parts.join('/'), isDir);
88
+ }
89
+ /** For a walk that already checked the parent folder: is this child ignored? */
90
+ isIgnoredChild(rel, isDir) {
91
+ return rel.split('/')[0] === '.git' || this.ignoredHere(rel, isDir);
92
+ }
93
+ }
@@ -0,0 +1,50 @@
1
+ import { IgnoreRules } from './rules.js';
2
+ export interface ViewPaths {
3
+ /** The project on this PC. */
4
+ host: string;
5
+ /** Its filtered copy (mapped into the sandbox as the workspace). */
6
+ view: string;
7
+ /** Host-only: what both sides looked like after the last sync. */
8
+ state: string;
9
+ }
10
+ export interface ViewOptions extends ViewPaths {
11
+ exclude?: string[];
12
+ include?: string[];
13
+ /** The project's .git as the sandbox sees it (written into the view's .git file); null = the project has no .git. */
14
+ guestGitDir?: string | null;
15
+ note?: (message: string) => void;
16
+ }
17
+ export interface SyncStats {
18
+ toView: number;
19
+ toHost: number;
20
+ deleted: number;
21
+ conflicts: number;
22
+ skipped: number;
23
+ }
24
+ export declare class WorkspaceView {
25
+ private readonly o;
26
+ readonly rules: IgnoreRules;
27
+ private seen;
28
+ constructor(o: ViewOptions);
29
+ /** Where a project's view and its sync state live: <storage>\views\<id> and <storage>\views\<id>.json. */
30
+ static pathsFor(storageRoot: string, host: string): ViewPaths;
31
+ private abs;
32
+ /** Creates the view (with its .git file) and brings it fully in step with the project. */
33
+ prepare(): SyncStats;
34
+ /** Every file not ignored, on one side (bounded, links skipped, ignored folders not entered). */
35
+ private walk;
36
+ /** A full pass over both sides and everything synced before. */
37
+ syncAll(): SyncStats;
38
+ /** Paths that changed (from file events): files, or folders whose contents are walked. */
39
+ syncChanged(rels: Iterable<string>): SyncStats;
40
+ private syncPaths;
41
+ private syncOne;
42
+ /** Copies (keeping the file time) or deletes on the other side. */
43
+ private carry;
44
+ private pruneEmpty;
45
+ /**
46
+ * Keeps both sides in step while it runs: file events from both, batched every 300 ms, plus a full pass every
47
+ * `fullEveryMs` to catch anything an event missed. Returns a function that stops it.
48
+ */
49
+ watch(fullEveryMs?: number): () => void;
50
+ }
@@ -0,0 +1,281 @@
1
+ // A project as the sandbox sees it with workspaceFilter on: a copy of the project on this PC (in storage, outside the
2
+ // project) that holds only what git does not ignore, kept in step with the project both ways. The sandbox maps the view
3
+ // instead of the project, and the project's real .git separately (a "gitdir:" file in the view points at it), so git
4
+ // works as usual. What the sandbox creates that is ignored (node_modules, build output) stays in the view; what is
5
+ // ignored in your project (.env, your node_modules) never enters the sandbox.
6
+ //
7
+ // Each file is compared on both sides with what both looked like after the last sync (size + time): a change on one
8
+ // side is copied to the other, a deletion is repeated, and a file changed on both sides keeps yours in the project and
9
+ // the sandbox's next to it as <name>.sandbox-conflict-<time><ext>. Copies keep the file time, so an unchanged file is
10
+ // never copied again. Links are never followed or created on either side.
11
+ import fs from 'node:fs';
12
+ import path from 'node:path';
13
+ import crypto from 'node:crypto';
14
+ import { z } from 'zod';
15
+ import { readValid, writeJsonAtomic } from '../../shared/rundir.js';
16
+ import { IgnoreRules } from './rules.js';
17
+ const Manifest = z.record(z.string().max(1024), z.tuple([z.number(), z.number(), z.number(), z.number()]));
18
+ /** Most entries one walk looks at: a sandbox can create files faster than they are worth syncing. */
19
+ const MAX_ENTRIES = 300000;
20
+ const stamp = (file) => {
21
+ try {
22
+ const s = fs.lstatSync(file);
23
+ return s.isFile() ? [s.size, Math.round(s.mtimeMs)] : [-1, -1];
24
+ }
25
+ catch {
26
+ return [-1, -1];
27
+ }
28
+ };
29
+ const sameContent = (a, b) => {
30
+ const hash = (f) => crypto.createHash('sha256').update(fs.readFileSync(f)).digest('hex');
31
+ try {
32
+ return fs.statSync(a).size === fs.statSync(b).size && hash(a) === hash(b);
33
+ }
34
+ catch {
35
+ return false;
36
+ }
37
+ };
38
+ /** A path from an event or the manifest: relative, forward slashes, no way out of the root. */
39
+ const safeRel = (rel) => {
40
+ const clean = rel.replace(/\\/g, '/').replace(/^\/+/, '');
41
+ if (!clean || clean.length > 1000 || clean.split('/').some((p) => p === '..' || p === '' || p.includes(':')))
42
+ return null;
43
+ return clean;
44
+ };
45
+ export class WorkspaceView {
46
+ o;
47
+ rules;
48
+ seen;
49
+ constructor(o) {
50
+ this.o = o;
51
+ this.rules = new IgnoreRules(o.host, o.exclude, o.include);
52
+ this.seen = readValid(Manifest, o.state) ?? {};
53
+ }
54
+ /** Where a project's view and its sync state live: <storage>\views\<id> and <storage>\views\<id>.json. */
55
+ static pathsFor(storageRoot, host) {
56
+ const id = path.basename(host).replace(/[^A-Za-z0-9._-]/g, '_') + '-' + crypto.createHash('sha256').update(path.resolve(host).toLowerCase()).digest('hex').slice(0, 8);
57
+ const view = path.join(storageRoot, 'views', id);
58
+ return { host, view, state: view + '.json' };
59
+ }
60
+ abs(root, rel) { return path.join(root, ...rel.split('/')); }
61
+ /** Creates the view (with its .git file) and brings it fully in step with the project. */
62
+ prepare() {
63
+ fs.mkdirSync(this.o.view, { recursive: true });
64
+ const gitFile = path.join(this.o.view, '.git');
65
+ if (this.o.guestGitDir)
66
+ fs.writeFileSync(gitFile, 'gitdir: ' + this.o.guestGitDir.replace(/\\/g, '/') + '\n');
67
+ else
68
+ fs.rmSync(gitFile, { force: true });
69
+ return this.syncAll();
70
+ }
71
+ /** Every file not ignored, on one side (bounded, links skipped, ignored folders not entered). */
72
+ walk(root, under = '') {
73
+ const files = [];
74
+ // An ignored folder is only entered when one of your include patterns could match inside it; then only files those
75
+ // patterns let in are taken.
76
+ const queue = [{ rel: under, ignored: under !== '' && this.rules.isIgnored(under, true) }];
77
+ let count = 0;
78
+ while (queue.length) {
79
+ const { rel, ignored } = queue.shift();
80
+ let entries;
81
+ try {
82
+ entries = fs.readdirSync(this.abs(root, rel), { withFileTypes: true });
83
+ }
84
+ catch {
85
+ continue;
86
+ }
87
+ for (const e of entries) {
88
+ if (++count > MAX_ENTRIES) {
89
+ this.o.note?.('stopped listing ' + root + ' after ' + MAX_ENTRIES + ' entries');
90
+ return files;
91
+ }
92
+ const child = rel ? rel + '/' + e.name : e.name;
93
+ if (e.isSymbolicLink() || child === '.git')
94
+ continue;
95
+ if (e.isDirectory()) {
96
+ const childIgnored = ignored || this.rules.isIgnoredChild(child, true);
97
+ if (!childIgnored || this.rules.mayIncludeInside(child))
98
+ queue.push({ rel: child, ignored: childIgnored });
99
+ }
100
+ else if (e.isFile() && ((!ignored && !this.rules.isIgnoredChild(child, false)) || this.rules.isForced(child)))
101
+ files.push(child);
102
+ }
103
+ }
104
+ return files;
105
+ }
106
+ /** A full pass over both sides and everything synced before. */
107
+ syncAll() {
108
+ this.rules.reset();
109
+ const all = new Set([...this.walk(this.o.host), ...this.walk(this.o.view), ...Object.keys(this.seen)]);
110
+ return this.syncPaths(all);
111
+ }
112
+ /** Paths that changed (from file events): files, or folders whose contents are walked. */
113
+ syncChanged(rels) {
114
+ const files = new Set();
115
+ for (const raw of rels) {
116
+ const rel = safeRel(raw);
117
+ if (!rel)
118
+ continue;
119
+ if (rel.endsWith('.gitignore')) {
120
+ this.rules.reset();
121
+ return this.syncAll();
122
+ }
123
+ const isDir = [this.o.host, this.o.view].some((root) => { try {
124
+ return fs.lstatSync(this.abs(root, rel)).isDirectory();
125
+ }
126
+ catch {
127
+ return false;
128
+ } });
129
+ if (isDir) {
130
+ if (this.rules.isIgnored(rel, true) && !this.rules.mayIncludeInside(rel))
131
+ continue;
132
+ for (const f of [...this.walk(this.o.host, rel), ...this.walk(this.o.view, rel)])
133
+ files.add(f);
134
+ for (const k of Object.keys(this.seen))
135
+ if (k.startsWith(rel + '/'))
136
+ files.add(k);
137
+ }
138
+ else
139
+ files.add(rel);
140
+ }
141
+ return this.syncPaths(files);
142
+ }
143
+ syncPaths(rels) {
144
+ const stats = { toView: 0, toHost: 0, deleted: 0, conflicts: 0, skipped: 0 };
145
+ for (const rel of rels) {
146
+ if (this.rules.isIgnored(rel)) {
147
+ delete this.seen[rel];
148
+ continue;
149
+ }
150
+ try {
151
+ this.syncOne(rel, stats);
152
+ }
153
+ catch (e) {
154
+ stats.skipped++;
155
+ this.o.note?.('could not sync ' + rel + ': ' + (e instanceof Error ? e.message : String(e)));
156
+ }
157
+ }
158
+ writeJsonAtomic(this.o.state, this.seen);
159
+ return stats;
160
+ }
161
+ syncOne(rel, stats) {
162
+ const hostFile = this.abs(this.o.host, rel);
163
+ const viewFile = this.abs(this.o.view, rel);
164
+ const [hs, hm] = stamp(hostFile);
165
+ const [vs, vm] = stamp(viewFile);
166
+ const before = this.seen[rel];
167
+ const record = () => { const h = stamp(hostFile); const v = stamp(viewFile); if (h[0] < 0 && v[0] < 0)
168
+ delete this.seen[rel];
169
+ else
170
+ this.seen[rel] = [h[0], h[1], v[0], v[1]]; };
171
+ if (hs < 0 && vs < 0) {
172
+ delete this.seen[rel];
173
+ return;
174
+ }
175
+ const hostChanged = !before || before[0] !== hs || before[1] !== hm;
176
+ const viewChanged = !before || before[2] !== vs || before[3] !== vm;
177
+ if (!hostChanged && !viewChanged)
178
+ return;
179
+ if (hostChanged && !viewChanged) {
180
+ this.carry(hostFile, viewFile, hs < 0, stats, 'toView');
181
+ record();
182
+ return;
183
+ }
184
+ if (viewChanged && !hostChanged) {
185
+ this.carry(viewFile, hostFile, vs < 0, stats, 'toHost');
186
+ record();
187
+ return;
188
+ }
189
+ // Changed on both sides (or never synced): the same content is no conflict.
190
+ if (hs >= 0 && vs >= 0 && sameContent(hostFile, viewFile)) {
191
+ record();
192
+ return;
193
+ }
194
+ if (hs < 0) {
195
+ this.carry(viewFile, hostFile, false, stats, 'toHost');
196
+ record();
197
+ return;
198
+ }
199
+ if (vs < 0) {
200
+ this.carry(hostFile, viewFile, false, stats, 'toView');
201
+ record();
202
+ return;
203
+ }
204
+ // Yours stays in the project; the sandbox's version is kept next to it, and the view gets yours.
205
+ const ext = path.extname(hostFile);
206
+ const conflict = hostFile.slice(0, hostFile.length - ext.length) + '.sandbox-conflict-' + new Date().toISOString().replace(/\D/g, '').slice(0, 14) + ext;
207
+ fs.copyFileSync(viewFile, conflict);
208
+ this.carry(hostFile, viewFile, false, stats, 'toView');
209
+ stats.conflicts++;
210
+ this.o.note?.('both sides changed ' + rel + ': kept yours, the sandbox\'s version is ' + path.basename(conflict));
211
+ record();
212
+ }
213
+ /** Copies (keeping the file time) or deletes on the other side. */
214
+ carry(from, to, deleted, stats, way) {
215
+ if (deleted) {
216
+ if (stamp(to)[0] < 0)
217
+ return;
218
+ fs.rmSync(to, { force: true });
219
+ this.pruneEmpty(path.dirname(to), way === 'toView' ? this.o.view : this.o.host);
220
+ stats.deleted++;
221
+ return;
222
+ }
223
+ // Never write through a link that is already there.
224
+ try {
225
+ if (fs.lstatSync(to).isSymbolicLink())
226
+ throw new Error('a link is in the way');
227
+ }
228
+ catch (e) {
229
+ if (e.code !== 'ENOENT')
230
+ throw e;
231
+ }
232
+ fs.mkdirSync(path.dirname(to), { recursive: true });
233
+ fs.copyFileSync(from, to);
234
+ const t = fs.statSync(from).mtime;
235
+ fs.utimesSync(to, t, t);
236
+ stats[way]++;
237
+ }
238
+ pruneEmpty(dir, root) {
239
+ for (let d = dir; d.length > root.length && d.startsWith(root); d = path.dirname(d)) {
240
+ try {
241
+ if (fs.readdirSync(d).length)
242
+ return;
243
+ fs.rmdirSync(d);
244
+ }
245
+ catch {
246
+ return;
247
+ }
248
+ }
249
+ }
250
+ /**
251
+ * Keeps both sides in step while it runs: file events from both, batched every 300 ms, plus a full pass every
252
+ * `fullEveryMs` to catch anything an event missed. Returns a function that stops it.
253
+ */
254
+ watch(fullEveryMs = 60000) {
255
+ const pending = new Set();
256
+ let timer;
257
+ const flush = () => {
258
+ timer = undefined;
259
+ const batch = [...pending];
260
+ pending.clear();
261
+ const s = this.syncChanged(batch);
262
+ if (s.toHost || s.toView || s.deleted || s.conflicts)
263
+ this.o.note?.('synced: ' + s.toView + ' in, ' + s.toHost + ' out, ' + s.deleted + ' deleted, ' + s.conflicts + ' conflicts');
264
+ };
265
+ const onEvent = (_event, file) => {
266
+ if (!file)
267
+ return;
268
+ pending.add(String(file));
269
+ timer ??= setTimeout(flush, 300);
270
+ };
271
+ const watchers = [this.o.host, this.o.view].map((dir) => fs.watch(dir, { recursive: true }, onEvent));
272
+ const full = setInterval(() => { try {
273
+ this.syncAll();
274
+ }
275
+ catch (e) {
276
+ this.o.note?.('full sync failed: ' + (e instanceof Error ? e.message : String(e)));
277
+ } }, fullEveryMs);
278
+ return () => { watchers.forEach((w) => w.close()); clearInterval(full); clearTimeout(timer); if (pending.size)
279
+ flush(); };
280
+ }
281
+ }