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
@@ -0,0 +1,2 @@
1
+ import type { Suite } from './suite.js';
2
+ export declare const accountsSuite: Suite;
@@ -0,0 +1,44 @@
1
+ // Claude accounts: saving logins from the sandbox or this PC, switching, and the names that are refused.
2
+ import fs from 'node:fs';
3
+ import path from 'node:path';
4
+ import { activateAccount, activeAccount, defaultAccount, listAccounts, readAccount, removeAccount, saveAccount, setDefaultAccount } from '../host/accounts.js';
5
+ export const accountsSuite = {
6
+ name: 'accounts',
7
+ title: 'Claude accounts (save, switch, remove, safe names)',
8
+ run: async (t, f) => {
9
+ t.section('saving and switching');
10
+ const storage = t.at('acct-storage');
11
+ const dataA = path.join(storage, 'data', 'x');
12
+ const sandboxClaude = path.join(dataA, 'state', 'claude');
13
+ t.write(path.join(sandboxClaude, '.credentials.json'), '{"claudeAiOauth":{"refreshToken":"personal-1"}}');
14
+ t.write(path.join(sandboxClaude, '.claude.json'), '{"oauthAccount":{"emailAddress":"me@personal"},"other":1}');
15
+ saveAccount(storage, dataA, 'personal', null);
16
+ t.write(t.at('host-login', '.credentials.json'), '{"claudeAiOauth":{"refreshToken":"work-1"}}');
17
+ t.write(t.at('host-login.json'), '{"oauthAccount":{"emailAddress":"me@work"}}');
18
+ saveAccount(storage, dataA, 'work', { claudeDir: t.at('host-login'), globalStateFile: t.at('host-login.json') });
19
+ t.equal(listAccounts(storage, dataA).length, 2, 'accounts can be saved from the sandbox and from this PC');
20
+ t.equal(defaultAccount(storage), 'personal', 'the first account saved becomes the default');
21
+ t.equal(activeAccount(dataA), 'personal', 'the first account saved is the active one');
22
+ t.check(!fs.existsSync(path.join(sandboxClaude, '.agent-sandbox-account')), 'which account is active is not recorded inside the sandbox...');
23
+ t.check(fs.existsSync(path.join(storage, 'accounts', 'index.json')), '... it is recorded host-side');
24
+ t.write(path.join(sandboxClaude, '.credentials.json'), '{"claudeAiOauth":{"refreshToken":"personal-2"}}');
25
+ activateAccount(storage, dataA, 'work');
26
+ const stateAfter = f.readJson(path.join(sandboxClaude, '.claude.json'));
27
+ t.includes(fs.readFileSync(path.join(sandboxClaude, '.credentials.json'), 'utf8'), 'work-1', 'switching swaps in the other account\'s credentials');
28
+ t.equal(stateAfter.oauthAccount.emailAddress, 'me@work', 'switching swaps the signed-in account');
29
+ t.equal(stateAfter.other, 1, 'switching keeps the rest of the sandbox state');
30
+ t.equal(activeAccount(dataA), 'work', 'the new account is active');
31
+ t.includes(JSON.stringify(readAccount(storage, 'personal')?.credentials), 'personal-2', 'the outgoing account\'s refreshed tokens are saved back before switching');
32
+ t.section('removing and refusing');
33
+ setDefaultAccount(storage, 'work');
34
+ removeAccount(storage, 'work');
35
+ t.equal(defaultAccount(storage), null, 'removing the default account clears the default');
36
+ await t.throws(() => activateAccount(storage, dataA, 'nope'), 'Unknown account', 'an unknown account cannot be activated');
37
+ await t.throws(() => saveAccount(storage, dataA, '../x', null), 'Invalid account name', 'a path-like account name is rejected');
38
+ const victim = t.dir('victim');
39
+ const otherData = t.at('acct-storage', 'data', 'd');
40
+ const fromHost = { claudeDir: victim, globalStateFile: victim };
41
+ await t.throws(() => saveAccount(t.at('acct-storage'), otherData, 'index', fromHost), 'Invalid account name', 'an account cannot be called "index" (it would overwrite the index)');
42
+ await t.throws(() => saveAccount(t.at('acct-storage'), otherData, 'con', fromHost), 'Invalid account name', 'an account cannot be named after a device (it would open a device)');
43
+ },
44
+ };
@@ -0,0 +1,2 @@
1
+ import type { Suite } from './suite.js';
2
+ export declare const agentCommandsSuite: Suite;
@@ -0,0 +1,47 @@
1
+ // Agent commands (Beta): which agents get a forwarding command, what the files contain, and that "off" changes nothing.
2
+ // Everything happens in the test's temp folder with a made-up PATH; your real PATH is never read or written.
3
+ import fs from 'node:fs';
4
+ import path from 'node:path';
5
+ import { DEFAULTS } from '../core/config.js';
6
+ import { agentCommands, commandsDir, forwardingCmd, forwardingSh, realAgent, reconcileAgentCommands } from '../host/agent-commands.js';
7
+ export const agentCommandsSuite = {
8
+ name: 'agent-commands',
9
+ title: 'agent commands (Beta): claude, codex ... forwarded into a hidden sandbox',
10
+ run: async (t) => {
11
+ const storage = t.at('agent-cmd-storage');
12
+ const npmDir = t.at('agent-cmd-npm');
13
+ const localBin = t.at('agent-cmd-local');
14
+ t.write(path.join(npmDir, 'codex.cmd'), '@echo codex');
15
+ t.write(path.join(npmDir, 'gemini.cmd'), '@echo gemini');
16
+ t.write(path.join(localBin, 'claude.exe'), 'MZ');
17
+ // A forwarding file of our own, first on PATH, must never count as the real agent.
18
+ t.write(path.join(commandsDir(storage), 'claude.cmd'), '@rem windows-development-sandbox agent command');
19
+ const PATH = [commandsDir(storage), npmDir, localBin].join(path.delimiter);
20
+ const on = { ...DEFAULTS, agentCommands: { enabled: true, agents: 'auto' } };
21
+ t.section('finding agents');
22
+ t.equal(realAgent('claude', commandsDir(storage), PATH), path.join(localBin, 'claude.exe'), 'the real agent is found on PATH, skipping this tool\'s own forwarding files');
23
+ t.equal(realAgent('nope', commandsDir(storage), PATH), null, 'an agent that is not installed is not found');
24
+ const commandsOf = (config, searchPath) => agentCommands(config, storage, searchPath).map((a) => a.command).sort();
25
+ t.equal(commandsOf(on, PATH), ['claude', 'codex', 'gemini'], '"auto": every agent installed on this PC gets a command, agents not installed none');
26
+ t.equal(commandsOf({ ...on, agentCommands: { enabled: true, agents: ['codex'] } }, PATH), ['codex'], 'a list limits it to the named agents');
27
+ const unsafeDir = t.at('agent%cmd^dir');
28
+ t.write(path.join(unsafeDir, 'aider.cmd'), '@echo');
29
+ t.check(!agentCommands(on, storage, unsafeDir).some((a) => a.command === 'aider'), 'an agent in a folder whose path has % or ^ (which cmd.exe would expand) is left alone');
30
+ t.section('the forwarding files');
31
+ const claude = { harness: 'claude', command: 'claude', real: path.join(localBin, 'claude.exe') };
32
+ const cmd = forwardingCmd(claude, 'C:\\node\\node.exe', 'C:\\tool\\dist\\bin\\sandbox.js');
33
+ t.includes(cmd, '"C:\\node\\node.exe" "C:\\tool\\dist\\bin\\sandbox.js" launch --harness claude --headless --terminal -- %*', 'the .cmd starts the agent in a hidden sandbox attached to this terminal...');
34
+ t.includes(cmd, 'if "%SANDBOX_AGENT_DIRECT%"=="1" goto direct', '... unless SANDBOX_AGENT_DIRECT=1...');
35
+ t.includes(cmd, 'call "' + claude.real + '" %*', '... which runs the real agent');
36
+ const sh = forwardingSh({ ...claude, real: 'C:\\Users\\me\\it\'s\\claude.exe' }, 'C:\\node\\node.exe', 'C:\\tool\\sandbox.js');
37
+ t.check(sh.startsWith('#!/bin/sh'), 'Git Bash gets a shell script');
38
+ t.includes(sh, "exec '/c/node/node.exe' '/c/tool/sandbox.js' launch --harness claude --headless --terminal -- \"$@\"", 'the shell script starts the same sandbox, with POSIX paths');
39
+ t.includes(sh, "'/c/Users/me/it'\\''s/claude.exe'", 'the shell script quotes awkward paths safely');
40
+ t.section('off changes nothing');
41
+ // Off with nothing set up: nothing to do, no PATH edit (which would be refused during the self-test anyway).
42
+ const fresh = t.at('agent-cmd-fresh');
43
+ const off = reconcileAgentCommands(DEFAULTS, fresh, 'C:\\node\\node.exe', 'C:\\tool\\sandbox.js');
44
+ t.equal({ on: off.on, changed: off.changed, wrote: fs.existsSync(commandsDir(fresh)) }, { on: false, changed: false, wrote: false }, 'off (the default) writes nothing and leaves your PATH alone');
45
+ await t.throws(() => reconcileAgentCommands(on, t.at('agent-cmd-on'), 'C:\\node\\node.exe', 'C:\\tool\\sandbox.js'), /self-test|PATH/i, 'turning it on edits your PATH, which the self-test refuses (it never changes this PC)');
46
+ },
47
+ };
@@ -0,0 +1,2 @@
1
+ import type { Suite } from './suite.js';
2
+ export declare const auditSuite: Suite;
@@ -0,0 +1,57 @@
1
+ // The audit log: kept small (repeats folded, a cap per minute, old days compressed, limits on days and size), read back
2
+ // filtered, and what the sandbox reports is checked like everything else that comes from it.
3
+ import crypto from 'node:crypto';
4
+ import fs from 'node:fs';
5
+ import path from 'node:path';
6
+ import zlib from 'node:zlib';
7
+ import { AuditCollector } from '../host/audit/collect.js';
8
+ import { AuditLog, fold } from '../host/audit/log.js';
9
+ import { parseProcessEvents } from '../guest/audit-parse.js';
10
+ import { runPaths } from '../shared/rundir.js';
11
+ const ev = (t, extra = {}) => ({ t, k: 'proc', p: 'C:\\x\\cargo.exe', c: 'cargo build', ...extra });
12
+ export const auditSuite = {
13
+ name: 'audit',
14
+ title: 'audit log (folding, compression, limits, untrusted reports)',
15
+ run: (t) => {
16
+ t.section('small');
17
+ const folded = fold([ev('2026-01-01T10:00:01Z'), ev('2026-01-01T10:00:02Z'), ev('2026-01-01T10:00:03Z', { c: 'cargo test' }), ev('2026-01-01T10:01:00Z')]);
18
+ t.equal(folded.map((e) => [e.c, e.n ?? 1]), [['cargo build', 2], ['cargo test', 1], ['cargo build', 1]], 'repeats within a minute become one line with a count');
19
+ const flood = fold(Array.from({ length: 50 }, (_, i) => ev('2026-01-01T10:00:00Z', { c: 'step ' + i })), 10);
20
+ t.check(flood.length === 11 && flood[10].k === 'note' && flood[10].p.startsWith('40 more'), 'past the per-minute cap the rest is one note');
21
+ const dir = t.at('audit-log');
22
+ const log = new AuditLog(dir, { days: 3, maxMb: 5 });
23
+ log.append([ev('2026-01-01T10:00:00Z'), ev('2026-01-02T10:00:00Z'), ev('2026-01-04T10:00:00Z', { k: 'file', p: 'src/a.ts', a: 'change', w: 'C:\\app' })]);
24
+ log.maintain('2026-01-04');
25
+ t.equal(fs.readdirSync(dir).sort(), ['2026-01-02.jsonl.gz', '2026-01-04.jsonl'], 'finished days are compressed and days past audit.days are deleted');
26
+ t.includes(zlib.gunzipSync(fs.readFileSync(path.join(dir, '2026-01-02.jsonl.gz'))).toString(), 'cargo build', 'a compressed day still holds its events');
27
+ const big = new AuditLog(t.at('audit-big'), { days: 30, maxMb: 5 });
28
+ // Random text: compression must not make the test data fit after all.
29
+ const line = (day) => ev(day + 'T00:00:00Z', { c: crypto.randomBytes(900).toString('hex') });
30
+ for (const day of ['2026-02-01', '2026-02-02', '2026-02-03'])
31
+ big.append(Array.from({ length: 1400 }, () => line(day)));
32
+ big.maintain('2026-02-03');
33
+ t.check(big.bytes() <= 5 * 1024 * 1024 && fs.readdirSync(big.dir).length < 3, 'past audit.maxMb the oldest days go', String(big.bytes()));
34
+ t.section('reading');
35
+ t.equal(log.read({ days: 10000, kinds: ['file'] }).map((e) => e.p), ['src/a.ts'], 'events can be filtered by kind');
36
+ t.equal(log.read({ days: 10000, grep: 'SRC/A.TS' }).length, 1, '... and by text (any case)');
37
+ t.section('what the sandbox reports');
38
+ const xml = "<Event><System><TimeCreated SystemTime='2026-01-05T09:00:00.000Z'/><EventRecordID>41</EventRecordID></System><EventData><Data Name='NewProcessName'>C:\\AgentData\\tools\\git\\cmd\\git.exe</Data><Data Name='CommandLine'>git push --force &quot;origin&quot;</Data><Data Name='ParentProcessName'>C:\\nvm\\nodejs\\node.exe</Data></EventData></Event>"
39
+ + "<Event><System><EventRecordID>42</EventRecordID></System><EventData><Data Name='NewProcessName'>C:\\Windows\\System32\\conhost.exe</Data></EventData></Event>";
40
+ const parsed = parseProcessEvents(xml);
41
+ t.equal({ last: parsed.last, lines: parsed.lines.map((l) => JSON.parse(l)).map((e) => [e.p, e.c, e.pp]) }, { last: 42, lines: [['C:\\AgentData\\tools\\git\\cmd\\git.exe', 'git push --force "origin"', 'C:\\nvm\\nodejs\\node.exe']] }, 'Windows process events become audit lines; Windows\' own background programs are left out');
42
+ const data = t.at('audit-data', 'data', 'd');
43
+ const storage = t.at('audit-data');
44
+ const reports = runPaths(data).audit;
45
+ t.write(path.join(reports, '1-1.jsonl'), [
46
+ JSON.stringify({ t: '2026-01-05T09:00:00Z', k: 'guard', p: 'fake: all clear' }),
47
+ JSON.stringify({ t: '2026-01-05T09:00:01Z', k: 'proc', p: 'C:\\x.exe', c: 'x --y', w: 'C:\\Users\\me' }),
48
+ 'not json', JSON.stringify({ t: '2026-01-05T09:00:02Z', k: 'proc', p: 'x'.repeat(5000) }),
49
+ ].join('\n'));
50
+ t.write(path.join(reports, 'not-a-report.txt'), 'ignored');
51
+ const collector = new AuditCollector(data, storage, { enabled: true, days: 14, maxMb: 50, processes: true, files: false, projects: [] });
52
+ collector.flush();
53
+ const got = new AuditLog(AuditLog.dirFor(storage, data), { days: 14, maxMb: 50 }).read({ days: 100000 });
54
+ t.equal(got.map((e) => [e.k, e.p, e.w]), [['proc', 'fake: all clear', undefined], ['proc', 'C:\\x.exe', undefined]], 'sandbox reports can only be programs: other kinds are recorded as such, fields it may not set are dropped, oversized lines are refused');
55
+ t.check(!fs.existsSync(path.join(reports, '1-1.jsonl')), 'reports are deleted once taken in');
56
+ },
57
+ };
@@ -0,0 +1,2 @@
1
+ import type { Suite } from './suite.js';
2
+ export declare const channelSuite: Suite;
@@ -0,0 +1,44 @@
1
+ // The host <-> sandbox channel and the terminal protocol: everything arriving from the sandbox is validated, sized and
2
+ // filtered before this PC acts on it.
3
+ import { safeId } from '../shared/rundir.js';
4
+ import { GuestRequest, SessionStatus } from '../shared/schema.js';
5
+ import { encodeFrame, FrameReader, FrameType, fixWindowsKeys, helloFrame, MAX_FRAME_BYTES, OutputFilter } from '../shared/pty-client.js';
6
+ export const channelSuite = {
7
+ name: 'channel',
8
+ title: 'request/status validation and the terminal protocol (frames, OSC 52, keys)',
9
+ run: async (t) => {
10
+ t.section('requests and session status');
11
+ const validRequest = (r) => GuestRequest.safeParse(r).success;
12
+ const validSession = (s) => SessionStatus.safeParse(s).success;
13
+ await t.throws(() => safeId('../evil'), 'invalid id', 'a path-like id is rejected');
14
+ t.check(!validRequest({ kind: 'shell', id: 'a' }), 'unknown request kinds are rejected');
15
+ t.check(!validRequest({ kind: 'exec', id: 'a/b', command: ['x'], cwd: 'C:\\', timeoutSeconds: 1 }), 'request ids must be safe file names');
16
+ t.check(validRequest({ kind: 'ping', id: 'abc', createdAt: 'x' }), 'a known request kind with a safe id is accepted');
17
+ t.check(!validRequest({ kind: 'ping', id: 'abc', createdAt: 'x', extra: 1 }), 'requests with unknown keys are rejected');
18
+ const goodSession = { id: 's1', launchId: 'l', harness: 'claude', cwd: 'C:\\Workspace', message: 'ok', phase: 'running', updatedAt: 'x', startedAt: 'x', attach: { addresses: ['172.20.1.5'], port: 5000, token: 't' } };
19
+ t.check(validSession(goodSession), 'a well-formed session status is accepted');
20
+ t.check(!validSession({ ...goodSession, attach: { ...goodSession.attach, port: 70000 } }), 'a session status with an impossible port is rejected');
21
+ t.check(!validSession({ ...goodSession, phase: 'pwned' }), 'a session status with an unknown phase is rejected');
22
+ t.section('terminal frames');
23
+ const filter = new OutputFilter();
24
+ const filtered = Buffer.concat([
25
+ filter.push(Buffer.from('a\x1b]52;c;ZXZpbA==\x07b\x1b]0;title\x07')),
26
+ filter.push(Buffer.from('c\x1b]5')),
27
+ filter.push(Buffer.from('2;c;cGF5\x1b\\d')),
28
+ ]).toString('latin1');
29
+ t.equal(filtered, 'a' + 'b\x1b]0;title\x07' + 'c' + 'd', 'clipboard writes (OSC 52) from the sandbox are stripped, even split across chunks; titles pass');
30
+ const through = (...chunks) => { const f = new OutputFilter(); return Buffer.concat(chunks.map((s) => f.push(Buffer.from(s, 'latin1')))).toString('latin1'); };
31
+ t.equal(through('x\x1b]52;c;QUFB\x1b', '\\y'), 'xy', 'an OSC 52 whose terminator is split between chunks is still stripped');
32
+ t.equal(through('x\x1b]052;c;QUFB\x07y'), 'xy', 'OSC 052 (leading zero) is stripped too');
33
+ t.equal(through('x\x1b]52;c;' + 'A'.repeat(70000), 'B'.repeat(100), '\x07y'), 'xy', 'a huge OSC 52 stays stripped across chunks');
34
+ t.equal(through('x\xc2\x9d52;c;QUFB\xc2\x9cy\x1bP1$qm\x1b\\z'), 'xyz', 'C1 (UTF-8) OSC and DCS strings are stripped');
35
+ t.equal(through('\x1b]8;;https://a.b\x1b\\link\x1b]8;;\x1b\\ \x1b[31mred\x1b[0m é\xc2\xa0'), '\x1b]8;;https://a.b\x1b\\link\x1b]8;;\x1b\\ \x1b[31mred\x1b[0m é\xc2\xa0', 'links, colours and ordinary UTF-8 (incl. U+00A0) pass unchanged');
36
+ t.equal(through('\x1b]9;9;macro\x07\x1b]9;4;1;50\x07'), '\x1b]9;4;1;50\x07', 'only the progress form of OSC 9 passes (ConEmu macros do not)');
37
+ const oversizedHeader = Buffer.concat([Buffer.from([16]), Buffer.from([0, 0, 0, 0x7f])]);
38
+ await t.throws(() => new FrameReader().push(oversizedHeader), 'frame too large', 'a frame announcing too much data is rejected instead of buffered');
39
+ t.equal(new FrameReader().push(encodeFrame(16, Buffer.alloc(MAX_FRAME_BYTES))).length, 1, 'a frame of exactly the maximum size is accepted');
40
+ const hello = new FrameReader().push(helloFrame('tok', 100, 30, 2))[0];
41
+ t.equal({ type: hello.type, cols: hello.payload.readInt16LE(0), rows: hello.payload.readInt16LE(2), flags: hello.payload[4], token: hello.payload.subarray(5).toString() }, { type: FrameType.Hello, cols: 100, rows: 30, flags: 2, token: 'tok' }, 'a hello frame carries size, flags and token');
42
+ t.equal(fixWindowsKeys(Buffer.from([0x61, 0x08, 0x7f, 0x1b])), Buffer.from([0x61, 0x7f, 0x08, 0x1b]), 'Windows console Backspace (^H) reaches the agent as DEL, so it deletes one character, not a word');
43
+ },
44
+ };
@@ -1,2 +1,2 @@
1
- import type { Harness } from './harness.js';
2
- export declare const cliTests: (t: Harness) => void;
1
+ import type { Suite } from './suite.js';
2
+ export declare const cliSuite: Suite;
@@ -1,30 +1,148 @@
1
- // The command manifest, parser and menu helpers: one definition drives both the flags and the menu.
1
+ // The command manifest, the argument parser, the interactive menu helpers and the settings screen. One definition
2
+ // (cli/manifest.ts) drives both the flags and the menu.
3
+ import fs from 'node:fs';
4
+ import path from 'node:path';
2
5
  import { buildProgram, parseCli } from '../cli/args.js';
3
6
  import { COMMANDS } from '../cli/manifest.js';
4
- import { commandLine, readHistory, recordHistory, splitArgs } from '../cli/menu.js';
5
- import { editConfigText } from '../cli/settings.js';
6
- export const cliTests = (t) => {
7
- const { check } = t;
8
- t.section('CLI: manifest, parser, menu');
9
- const program = buildProgram(() => undefined);
10
- const visible = COMMANDS.filter((c) => !c.hidden).map((c) => c.name);
11
- check(visible.every((name) => program.commands.some((c) => c.name() === name)) && program.commands.find((c) => c.name() === 'launch').options.some((o) => o.long === '--terminal'), 'every manifest command and option exists in the parser');
12
- const start = parseCli(['start', '--headless']);
13
- check(start.command === 'launch' && start.noAgent && start.headless === true, '`start` is launch without an agent');
14
- const clean = parseCli(['data', 'clean', 'tools/go', 'logs', '--yes']);
15
- check(clean.command === 'data' && clean.positionals.join() === 'clean,tools/go,logs' && clean.yes, 'data clean takes targets and --yes');
16
- check(parseCli(['start', '--database', 'postgres,redis', '--database', 'mongodb']).databases.join() === 'postgres,redis,mongodb' && parseCli(['db', 'start', 'redis']).positionals.join() === 'start,redis', '--database takes lists and repeats; `db` takes an action and a name');
17
- check(parseCli(['launch', '--harness', 'opencode', '--', '--continue']).passthrough.join() === '--continue', 'agent arguments after -- reach the agent');
18
- check(JSON.stringify(splitArgs('--model "big one" -c \'x y\'')) === '["--model","big one","-c","x y"]' && commandLine(['exec', 'echo', 'a b']) === 'sandbox exec echo "a b"', 'menu: arguments split like a shell, command lines quoted');
19
- const original = '{\n // my notes stay\n "sandbox": { "headless": true }\n}\n';
20
- const set = editConfigText(original, ['attach', 'auto'], true);
21
- const nested = 'text' in set ? editConfigText(set.text, ['databases', 'postgres', 'port'], 6543) : set;
22
- const reset = 'text' in nested ? editConfigText(nested.text, ['sandbox', 'headless'], undefined) : nested;
23
- const invalid = editConfigText(original, ['sandbox', 'memoryMb'], 12);
24
- check('text' in reset && reset.text.includes('// my notes stay') && reset.text.includes('"auto": true') && reset.text.includes('"port": 6543') && !reset.text.includes('headless') && 'problems' in invalid && invalid.problems.join().includes('memoryMb'), 'settings screen: changes keep the file\'s comments, nested keys are created, reset removes a key, invalid values are not saved');
25
- recordHistory(['launch', '--harness', 'claude']);
26
- recordHistory(['status']);
27
- recordHistory(['launch', '--harness', 'claude']);
28
- recordHistory(['menu']);
29
- check(readHistory().map((h) => h.argv.join(' ')).join('|') === 'launch --harness claude|status', 'history: newest first, no duplicates, the menu itself is not recorded');
7
+ import { commandLine, readHistory, recordHistory } from '../cli/history.js';
8
+ import { splitArgs } from '../cli/args.js';
9
+ import { optionProblem } from '../cli/menu/index.js';
10
+ import { cleanTargetOf } from '../cli/commands/data/collect.js';
11
+ import { ago, colorEnabled, shorten, shortenMiddle } from '../cli/colors.js';
12
+ import { hoistConfig, withoutConfig } from '../cli/index.js';
13
+ import { closest, validNodeSpec, validPythonSpec } from '../cli/validate.js';
14
+ import { CATEGORIES, editConfigText, nameOf, SchemaNode } from '../cli/menu/settings/index.js';
15
+ import { Frame, ListScreen, Text, TextScreen } from '../cli/tui/index.js';
16
+ import { Config, LocalOverrides } from '../core/schema.js';
17
+ export const cliSuite = {
18
+ name: 'cli',
19
+ title: 'command manifest, argument parser, menu helpers, settings screen',
20
+ run: async (t) => {
21
+ t.section('manifest and parser');
22
+ const program = buildProgram(() => undefined);
23
+ const visible = COMMANDS.filter((c) => !c.hidden).map((c) => c.name);
24
+ const missingFromParser = visible.filter((name) => !program.commands.some((c) => c.name() === name));
25
+ t.equal(missingFromParser, [], 'every manifest command exists in the parser');
26
+ t.check(program.commands.find((c) => c.name() === 'launch').options.some((o) => o.long === '--terminal'), 'every manifest option exists in the parser (launch --terminal)');
27
+ const packageJson = JSON.parse(fs.readFileSync(path.join(t.packageRoot, 'package.json'), 'utf8'));
28
+ t.equal(program.version(), packageJson.version, '--version prints the package version');
29
+ t.check(program.options.some((o) => o.short === '-v' && o.long === '--version'), '-v is a short form of --version');
30
+ const start = parseCli(['start', '--headless']);
31
+ t.equal({ command: start.command, noAgent: start.noAgent, headless: start.headless }, { command: 'launch', noAgent: true, headless: true }, '`start` is launch without an agent');
32
+ const clean = parseCli(['data', 'clean', 'tools/go', 'logs', '--yes']);
33
+ t.equal({ command: clean.command, positionals: clean.positionals, yes: clean.yes }, { command: 'data', positionals: ['clean', 'tools/go', 'logs'], yes: true }, 'data clean takes targets and --yes');
34
+ t.equal(parseCli(['start', '--database', 'postgres,redis', '--database', 'mongodb']).databases, ['postgres', 'redis', 'mongodb'], '--database takes lists and can repeat');
35
+ t.equal(parseCli(['db', 'start', 'redis']).positionals, ['start', 'redis'], '`db` takes an action and a name');
36
+ t.equal(parseCli(['launch', '--harness', 'opencode', '--', '--continue']).passthrough, ['--continue'], 'agent arguments after -- reach the agent');
37
+ t.section('parser: agent wrappers (claude-sbx ...) and agent-sandbox');
38
+ const wrapped = parseCli(['--terminal', '--continue', '--headless', '--node', '20', '--', '--terminal'], true);
39
+ t.equal({ command: wrapped.command, terminal: wrapped.terminal, headless: wrapped.headless, node: wrapped.node, passthrough: wrapped.passthrough }, { command: 'launch', terminal: true, headless: true, node: '20', passthrough: ['--continue', '--terminal'] }, 'claude-sbx: sandbox flags are consumed, everything else goes to the agent');
40
+ await t.throws(() => parseCli(['status', '--bogus']), 'unknown option', 'agent-sandbox: unknown options are errors');
41
+ t.equal(parseCli(['account', 'use', 'work', '--no-restart']).positionals, ['use', 'work'], 'agent-sandbox: positionals are collected');
42
+ t.equal(parseCli(['clean', '--days=3']).days, 3, 'agent-sandbox: --opt=value works');
43
+ const agentFlags = parseCli(['--model', 'opus', '-p', 'fix it', '--json', '-f', 'x', '--timeout', '5', '--resume', 'abc'], true);
44
+ t.equal(agentFlags.passthrough, ['--model', 'opus', '-p', 'fix it', '--json', '-f', 'x', '--timeout', '5', '--resume', 'abc'], 'claude-sbx: any flag that is not a launch option goes to the agent, in order (even ones agent-sandbox uses elsewhere)');
45
+ t.equal({ json: agentFlags.json, follow: agentFlags.follow, timeout: agentFlags.timeout }, { json: false, follow: false, timeout: 600 }, 'claude-sbx: the agent\'s flags do not set agent-sandbox options');
46
+ const reserved = parseCli(['--sbx-terminal', '--terminal', '--sbx-node=18'], true);
47
+ t.equal({ terminal: reserved.terminal, node: reserved.node, passthrough: reserved.passthrough }, { terminal: true, node: '18', passthrough: [] }, '--sbx-<option> always means agent-sandbox');
48
+ t.equal(parseCli(['launch', '--continue', '--sbx-headless']).passthrough, ['--continue'], 'agent-sandbox launch passes agent flags too');
49
+ await t.throws(() => parseCli(['--sbx-nope'], true), 'Unknown option: --sbx-nope', 'an unknown --sbx-<option> is an error');
50
+ t.section('menu helpers');
51
+ t.equal(splitArgs('--model "big one" -c \'x y\''), ['--model', 'big one', '-c', 'x y'], 'menu: arguments split like a shell');
52
+ t.equal(commandLine(['exec', 'echo', 'a b']), 'sandbox exec echo "a b"', 'menu: command lines are quoted');
53
+ const optionOf = (flag) => COMMANDS.flatMap((cmd) => cmd.options).find((o) => o.flag === flag);
54
+ t.check(optionProblem(optionOf('--node'), 'abc') !== undefined, 'menu: a bad Node version is rejected while typing');
55
+ t.check(optionProblem(optionOf('--node'), '22') === undefined, 'menu: a good Node version is accepted');
56
+ t.check(optionProblem(optionOf('--workspace'), path.join(t.packageRoot, 'no-such-folder-here')) !== undefined, 'menu: a missing folder is rejected');
57
+ t.check(optionProblem(optionOf('--workspace'), t.packageRoot) === undefined, 'menu: an existing folder is accepted');
58
+ t.check(optionProblem(optionOf('--days'), '0') !== undefined, 'menu: a number below the minimum is rejected');
59
+ const rowOf = (area, name) => ({ area, name, path: 'x', bytes: 1 });
60
+ t.equal([cleanTargetOf(rowOf('tools', 'go')), cleanTargetOf(rowOf('tools', 'agents/claude')), cleanTargetOf(rowOf('databases', 'databases/pg')), cleanTargetOf(rowOf('host', 'launches'))], ['tools/go', 'tools/agents/claude', 'databases/pg', 'launches'], 'data menu: removable rows become checkbox targets');
61
+ t.equal([cleanTargetOf(rowOf('host', 'accounts')), cleanTargetOf(rowOf('tools', '(files in tools)'))], [null, null], 'data menu: accounts and loose files are never offered');
62
+ t.equal(COMMANDS.find((cmd) => cmd.name === 'data').options.find((o) => o.flag === '--yes')?.noMenu, true, 'data menu: --yes is never offered');
63
+ t.section('--config placement, history, validation, text helpers');
64
+ t.equal(hoistConfig(['--config', 'a.json', 'status', '--json']), ['status', '--config', 'a.json', '--json'], '--config before the command is moved behind it');
65
+ t.equal(hoistConfig(['--config=a.json']), ['menu', '--config', 'a.json'], '--config alone opens the menu with it');
66
+ t.equal(withoutConfig(['status', '--config', 'a.json', '--json']), ['status', '--json'], 'the recorded history drops --config');
67
+ recordHistory(['launch', '--harness', 'claude']);
68
+ recordHistory(['status']);
69
+ recordHistory(['launch', '--harness', 'claude']);
70
+ recordHistory(['menu']);
71
+ t.equal(readHistory().map((h) => h.argv.join(' ')), ['launch --harness claude', 'status'], 'history: newest first, no duplicates, the menu itself is not recorded');
72
+ t.check(validNodeSpec('22') && validNodeSpec('v22.1.0') && validNodeSpec('lts/*') && !validNodeSpec('abc'), 'validation: Node version syntax');
73
+ t.check(validPythonSpec('3.12') && !validPythonSpec('2.7'), 'validation: Python version syntax');
74
+ t.equal(closest('cluade', ['claude', 'codex']), 'claude', 'validation: did-you-mean for agent names');
75
+ t.equal(closest('zzzzzz', ['claude']), undefined, 'validation: nothing is suggested for an unrelated name');
76
+ const env = (vars, isTty) => colorEnabled(vars, isTty);
77
+ t.check(env({}, true), 'colours: on for a terminal');
78
+ t.check(!env({}, false), 'colours: off for a pipe');
79
+ t.check(!env({ NO_COLOR: '1' }, true) && !env({ TERM: 'dumb' }, true), 'colours: off for NO_COLOR and TERM=dumb');
80
+ t.check(env({ FORCE_COLOR: '1' }, false) && !env({ FORCE_COLOR: '0' }, true), 'colours: FORCE_COLOR overrides');
81
+ t.equal(shorten('abcdefghij', 6), 'abcde…', 'text helpers: shorten');
82
+ t.equal(shortenMiddle('C:\\Users\\me\\project', 11), 'C:\\Us…oject', 'text helpers: shorten in the middle');
83
+ t.equal([ago(0, 10 * 60 * 1000), ago(0, 5000), ago(0, 3 * 86400 * 1000)], ['10 min ago', 'just now', '3 d ago'], 'text helpers: relative time');
84
+ t.section('settings screen');
85
+ const original = '{\n // my notes stay\n "sandbox": { "headless": true }\n}\n';
86
+ const set = editConfigText(original, ['attach', 'auto'], true);
87
+ const nested = 'text' in set ? editConfigText(set.text, ['databases', 'postgres', 'port'], 6543) : set;
88
+ const reset = 'text' in nested ? editConfigText(nested.text, ['sandbox', 'headless'], undefined) : nested;
89
+ t.check('text' in reset, 'editing a config file gives new text');
90
+ const resetText = 'text' in reset ? reset.text : '';
91
+ t.includes(resetText, '// my notes stay', 'settings screen: changes keep the file\'s comments');
92
+ t.includes(resetText, '"auto": true', 'settings screen: a new setting is added');
93
+ t.includes(resetText, '"port": 6543', 'settings screen: nested keys are created');
94
+ t.excludes(resetText, 'headless', 'settings screen: reset removes a key');
95
+ const invalid = editConfigText(original, ['sandbox', 'memoryMb'], 12);
96
+ t.check('problems' in invalid && invalid.problems.join().includes('memoryMb'), 'settings screen: invalid values are not saved', JSON.stringify(invalid));
97
+ const missing = editConfigText('{ "storage": { "root": "C:\\\\x" } }', ['agentCommands', 'enabled'], undefined);
98
+ t.check('text' in missing && missing.text.includes('"root"'), 'removing a setting the file does not have changes nothing (no crash)');
99
+ const projectEdit = editConfigText('{}', ['downloads', 'requireChecksum'], true, 'project');
100
+ const projectBad = editConfigText('{}', ['storage', 'root'], 'C:\\x', 'project');
101
+ t.check('text' in projectEdit && projectEdit.text.includes('requireChecksum'), 'settings screen: a project file accepts project settings');
102
+ t.check('problems' in projectBad, 'settings screen: a project file is validated against the project schema (storage cannot be set there)');
103
+ // Every setting must be reachable in the settings screen, in a category, with an editor that fits it.
104
+ const userSchema = new SchemaNode(Config);
105
+ const projectSchema = new SchemaNode(LocalOverrides);
106
+ const userLeaves = userSchema.leaves();
107
+ const projectLeaves = projectSchema.leaves();
108
+ t.check(userLeaves.length > 100 && projectLeaves.length > 50, 'settings screen: every setting is a leaf (' + userLeaves.length + ' settings, ' + projectLeaves.length + ' in a project file)');
109
+ const uncategorised = [...userSchema.children(), ...projectSchema.children()].map(([name]) => name).filter((name) => !CATEGORIES.some((c) => c.keys.includes(name)));
110
+ t.equal(uncategorised, [], 'settings screen: every top-level setting belongs to a category');
111
+ const kinds = new Set(userLeaves.map((l) => l.node.editor));
112
+ t.equal(['boolean', 'choice', 'union', 'number', 'string', 'string-list', 'choice-list', 'object-list', 'string-map', 'object-map'].filter((k) => !kinds.has(k)), [], 'settings screen: every kind of editor is used');
113
+ t.equal(userLeaves.filter((l) => l.node.editor === 'json').map((l) => l.key.join('.')), [], 'settings screen: no setting falls back to typing JSON');
114
+ t.check(userLeaves.some((l) => l.key.join('.') === 'downloads.requireChecksum') && projectLeaves.some((l) => l.key.join('.') === 'downloads.requireChecksum'), 'settings screen: downloads.requireChecksum is reachable in both files');
115
+ const harness = userSchema.child('harnesses').element;
116
+ t.includes(String(harness.check(harness.initial())), 'npmPackage', 'settings screen: a new agent says what it still needs (a package to install)');
117
+ t.equal(harness.check({ ...harness.initial(), npmPackage: 'my-cli', npmBin: 'my-cli' }), true, '... and is valid once it has one');
118
+ const mapping = userSchema.child('mappings').element;
119
+ t.check(mapping.check(mapping.initial()) !== true, 'settings screen: a new mapped folder needs its folders filled in before it is valid');
120
+ t.equal([Text.humanize('memoryMb'), Text.humanize('vGpu'), nameOf('node')], ['Memory MB', 'V GPU', 'Node.js'], 'settings screen: readable names');
121
+ t.section('screens');
122
+ const fit = Text.fit('\x1b[31mred text\x1b[0m', 5);
123
+ t.equal([Text.width(fit), Text.strip(fit)], [5, 'red …'], 'a coloured label is cut to the width without breaking its colour');
124
+ t.equal(Text.width('日本'), 4, 'wide characters take two columns');
125
+ const screen = new ListScreen({ path: ['T'], items: [{ value: 'a', label: 'Alpha', category: 'x' }, { value: 'b', label: 'Beta', category: 'y', description: 'second' }], categories: [{ id: 'x', label: 'X' }, { id: 'y', label: 'Y' }] });
126
+ const key = (name, text = '') => ({ name, text, ctrl: false, meta: false, shift: false });
127
+ screen.handle(key('left'));
128
+ screen.handle(key('down'));
129
+ screen.handle(key('right'));
130
+ t.equal(screen.handle(key('return')), { done: { kind: 'pick', value: 'b' } }, 'list screen: arrows switch category, Enter picks');
131
+ const search = new ListScreen({ path: ['T'], items: [{ value: 'a', label: 'Alpha' }, { value: 'b', label: 'Beta' }] });
132
+ for (const k of [key('', '/'), key('b', 'b'), key('e', 'e')])
133
+ search.handle(k);
134
+ t.equal(search.handle(key('return')), { done: { kind: 'pick', value: 'b' } }, 'list screen: / searches');
135
+ const lines = Frame.render(screen.render({ width: 60, height: Frame.bodyHeight(20) }), 60, 20);
136
+ t.equal(lines.length, 20, 'a frame fills the terminal height exactly');
137
+ const multi = new ListScreen({ path: ['T'], multi: true, items: [{ value: 'a', label: 'A' }, { value: 'b', label: 'B', checked: true }] });
138
+ multi.handle(key('space'));
139
+ t.equal(multi.handle(key('return')), { done: { kind: 'multi', values: ['a', 'b'] } }, 'list screen: space ticks several');
140
+ const typed = new TextScreen({ path: ['T'], label: 'Port', validate: (v) => (Number(v) > 0 ? true : 'a number') });
141
+ for (const ch of 'x')
142
+ typed.handle(key(ch, ch));
143
+ t.equal(typed.handle(key('return')), undefined, 'text field: an invalid value is not accepted');
144
+ typed.handle(key('backspace'));
145
+ typed.handle(key('8', '8'));
146
+ t.equal(typed.handle(key('return')), { done: '8' }, 'text field: a valid value is');
147
+ },
30
148
  };
@@ -0,0 +1,2 @@
1
+ import type { Suite } from './suite.js';
2
+ export declare const cmdlineSuite: Suite;
@@ -0,0 +1,45 @@
1
+ // How the sandbox builds command lines for npm-installed agents (.cmd files run through cmd.exe): & | < > ^ % and quotes
2
+ // must reach the agent unchanged.
3
+ import { spawnSync } from 'node:child_process';
4
+ import path from 'node:path';
5
+ import { pathToFileURL } from 'node:url';
6
+ import { isWin } from '../core/util.js';
7
+ import { runNode } from './fixtures.js';
8
+ // Odd quote counts flip cmd's quote state for every later argument: those come after them on purpose.
9
+ const TRICKY_ARGUMENTS = ['https://x?a=1&b=2', '50% off', 'a|b<c>d', 'x^y', 'say "hi" & go', 'plain', '%PATH%', 'a"b&c', 'q"x|y^z"w', 'e"5%', 'k&v', '100%'];
10
+ export const cmdlineSuite = {
11
+ name: 'cmdline',
12
+ title: 'cmd.exe quoting for npm-installed agents',
13
+ run: async (t) => {
14
+ t.section('command lines');
15
+ // guest/cmdline.ts is loaded in a child process with a throwaway launch/data folder (it reads session.json on load).
16
+ const quoteDir = t.at('quote');
17
+ t.write(path.join(quoteDir, 'launch', 'session.json'), '{}');
18
+ const agentCmd = t.write(path.join(quoteDir, 'agent.cmd'), '@"' + process.execPath + '" -e "console.log(JSON.stringify(process.argv.slice(1)))" %*\r\n');
19
+ const script = [
20
+ 'const m = await import(process.argv[1]); const [cmd, tricky] = JSON.parse(process.argv[2]);',
21
+ 'console.log(JSON.stringify({ normal: m.commandLine("C:\\\\W\\\\cmd.exe", ["/d", "/c", "C:\\\\a\\\\agent.cmd", "--model", "big one", "plain", ""]), direct: m.commandLine("C:\\\\a\\\\agent.exe", ["a&b", "50%"]),',
22
+ ' url: m.cmdArgs(["https://x?a=1&b=2"]), pct: m.cmdArgs(["50%"]), live: m.commandLine("cmd.exe", ["/d", "/c", cmd, ...tricky]).slice(8) }));',
23
+ ].join('\n');
24
+ const probe = await runNode(['--input-type=module', '-e', script, pathToFileURL(path.join(t.packageRoot, 'dist', 'guest', 'cmdline.js')).href, JSON.stringify([agentCmd, TRICKY_ARGUMENTS])], {
25
+ env: { ...process.env, AGENT_LAUNCH_DIR: path.join(quoteDir, 'launch'), AGENT_DATA_DIR: path.join(quoteDir, 'data') },
26
+ });
27
+ let quoting = {};
28
+ try {
29
+ quoting = JSON.parse(probe.out);
30
+ }
31
+ catch { /* reported below */ }
32
+ const whyEmpty = quoting.normal ? '' : 'the helper printed: ' + probe.out.trim().slice(0, 300);
33
+ t.check(quoting.normal === 'C:\\W\\cmd.exe /d /c C:\\a\\agent.cmd --model "big one" plain ""', 'ordinary arguments are quoted as before', whyEmpty + '\nactual: ' + quoting.normal);
34
+ t.check(quoting.direct === 'C:\\a\\agent.exe a&b 50%', 'a program started directly gets & and % unchanged', 'actual: ' + quoting.direct);
35
+ t.equal(quoting.url, '"https://x?a=1&b=2"', 'cmd /c arguments: & | < > ^ are quoted');
36
+ t.equal(quoting.pct, '"50"%""', 'cmd /c arguments: % is split out so no %name% expands');
37
+ // (A temp folder with spaces would also need cmd's own quote rules, so this end-to-end run skips those.)
38
+ if (isWin && quoting.live && !/\s/.test(agentCmd)) {
39
+ const echoed = spawnSync(process.env.ComSpec ?? 'cmd.exe', [quoting.live], { encoding: 'utf8', windowsVerbatimArguments: true, windowsHide: true });
40
+ t.equal(echoed.stdout.trim(), JSON.stringify(TRICKY_ARGUMENTS), 'cmd /d /c <agent>.cmd passes &, |, <, >, ^, % and quotes through to the agent unchanged');
41
+ }
42
+ else
43
+ t.skip('end-to-end cmd.exe quoting run (not Windows, or the temp path has spaces)');
44
+ },
45
+ };
@@ -1,3 +1,2 @@
1
- import type { Fixtures } from './fixtures.js';
2
- import type { Harness } from './harness.js';
3
- export declare const configTests: (t: Harness, f: Fixtures) => Promise<void>;
1
+ import type { Suite } from './suite.js';
2
+ export declare const configSuite: Suite;