acrylblends 1.0.4 → 1.0.6

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 (351) hide show
  1. package/docs/framework/agents/index.md +7 -5
  2. package/lib/accept-CNNHu4Cf.js +193 -0
  3. package/lib/{accepts-XBuZPVRI.js → accepts-Btq83xPd.js} +1 -1
  4. package/lib/accepts.js +1 -1
  5. package/lib/acryl-base.js +1 -331
  6. package/lib/adapter-CaZ6FZUU.js +37 -0
  7. package/lib/adapter.js +1 -36
  8. package/lib/{app-BM0rTCp7.js → app-CQcGWjrk.js} +4 -4
  9. package/lib/app-base-CpRMZL4v.js +332 -0
  10. package/lib/{audit-D5z9uwOP.d.ts → audit-GcLtvNLV.d.ts} +4 -2
  11. package/lib/{base-NSN_SNv-.js → base-D_6kfOr-.js} +1 -1
  12. package/lib/basic-auth-C0HTRpXh.js +20 -0
  13. package/lib/basic-auth-DFLflXNe.js +95 -0
  14. package/lib/basic-auth.js +1 -94
  15. package/lib/bearer-auth-Cwxt-vgS.js +94 -0
  16. package/lib/bearer-auth.js +1 -93
  17. package/lib/body-D7f4e7w_.js +94 -0
  18. package/lib/body-limit-WmIjWRLt.js +66 -0
  19. package/lib/body-limit.js +1 -65
  20. package/lib/buffer-CkEOJCcY.js +51 -0
  21. package/lib/bun.js +3 -3
  22. package/lib/cache-B0bHH7s5.js +169 -0
  23. package/lib/cache.js +1 -168
  24. package/lib/{client-DDLXwhBo.js → client-BrMFnMEd.js} +1 -1
  25. package/lib/{client-modules-CLvWOjbd.js → client-modules-drj4mjhj.js} +4 -4
  26. package/lib/client.js +1 -1
  27. package/lib/color-COa3DplO.js +37 -0
  28. package/lib/combine-TWuGdcbC.js +137 -0
  29. package/lib/combine.js +1 -136
  30. package/lib/{components-cEW3ovK0.js → components-B2ka9Ax1.js} +4 -4
  31. package/lib/{components-JXVm45kK.js → components-PVXwaOaC.js} +2 -2
  32. package/lib/compress-BzWqsHF1.js +79 -0
  33. package/lib/compress-C-9vK4B2.d.ts +11 -0
  34. package/lib/compress-DCyrrQ5D.js +11 -0
  35. package/lib/compress.d.ts +1 -1
  36. package/lib/compress.js +2 -78
  37. package/lib/concurrent-DrF3d2c-.js +30 -0
  38. package/lib/conninfo-BCROmdnf.d.ts +11 -0
  39. package/lib/conninfo-CWJwieh4.js +19 -0
  40. package/lib/constants-CBSEudrb.js +7 -0
  41. package/lib/{context-DDO0cfuP.js → context-CkOh7Ygv.js} +2 -2
  42. package/lib/context-storage-iuPoI_CR.js +52 -0
  43. package/lib/context-storage.js +1 -51
  44. package/lib/cookie-B1LlkzOe.js +125 -0
  45. package/lib/cookie-BHIduy2T.js +82 -0
  46. package/lib/cookie.js +1 -81
  47. package/lib/cordis/db-sqlite.d.ts +1 -1
  48. package/lib/cordis/door.d.ts +99 -5
  49. package/lib/cordis/door.js +562 -20
  50. package/lib/cordis/inspect.js +4 -4
  51. package/lib/cordis/kv-db.d.ts +1 -1
  52. package/lib/cordis/kv-db.js +1 -1
  53. package/lib/cordis/kv-json.d.ts +1 -1
  54. package/lib/cordis/kv-json.js +1 -1
  55. package/lib/cordis/node-server.d.ts +1 -1
  56. package/lib/cordis/node-server.js +3 -116
  57. package/lib/cordis/router-http.js +2 -2
  58. package/lib/cordis/router.js +4 -4
  59. package/lib/cordis/state.js +7 -7
  60. package/lib/{core-CaY8XDZ6.js → core-BOu1jYua.js} +1 -1
  61. package/lib/cors-CbAQ-8qU.js +106 -0
  62. package/lib/cors.js +1 -105
  63. package/lib/crypto-iVP-N-tS.js +34 -0
  64. package/lib/csrf-DqqOZtgy.js +101 -0
  65. package/lib/csrf.js +1 -100
  66. package/lib/css-C5CPVMvM.js +134 -0
  67. package/lib/css.js +2 -133
  68. package/lib/db/sqlite.d.ts +1 -25
  69. package/lib/dev-CLgu-JRe.js +50 -0
  70. package/lib/dev.js +1 -49
  71. package/lib/dom-YjND9Jt3.js +74 -0
  72. package/lib/door/host.d.ts +2 -2
  73. package/lib/door/host.js +1 -1
  74. package/lib/door/page.d.ts +3 -1
  75. package/lib/door/page.js +2 -2
  76. package/lib/door/protocol.d.ts +1 -1
  77. package/lib/door/protocol.js +1 -212
  78. package/lib/encode-BhbNLrjp.js +33 -0
  79. package/lib/factory-CLdSf-xS.js +30 -0
  80. package/lib/factory.js +1 -29
  81. package/lib/handler-C0wZ0KwO.js +12 -0
  82. package/lib/helpers-BEjcdbgw.js +5 -0
  83. package/lib/{hooks-w72iLeFR.js → hooks-CfC07ri0.js} +1 -1
  84. package/lib/html-CgNa89xw.js +31 -0
  85. package/lib/html-DGExBFNg.js +103 -0
  86. package/lib/html.d.ts +2 -5
  87. package/lib/html.js +2 -30
  88. package/lib/http-exception-MjWkT89V.js +54 -0
  89. package/lib/http-exception.js +1 -53
  90. package/lib/index-dAzQsJG9.d.ts +6 -0
  91. package/lib/index.js +3 -3
  92. package/lib/ip-restriction-jKtKCRM_.js +166 -0
  93. package/lib/ip-restriction.js +1 -165
  94. package/lib/ipaddr-DCoIo528.js +238 -0
  95. package/lib/jsx/dom/client.js +1 -1
  96. package/lib/jsx/dom/jsx-dev-runtime.js +1 -1
  97. package/lib/jsx/dom/jsx-runtime.js +2 -1
  98. package/lib/jsx/dom/server.js +3 -3
  99. package/lib/jsx/dom.js +5 -73
  100. package/lib/jsx/jsx-dev-runtime.d.ts +2 -5
  101. package/lib/jsx/jsx-dev-runtime.js +2 -13
  102. package/lib/jsx/jsx-runtime.d.ts +2 -2
  103. package/lib/jsx/jsx-runtime.js +4 -4
  104. package/lib/jsx/streaming.js +1 -1
  105. package/lib/jsx-CUQgttxU.js +47 -0
  106. package/lib/jsx-dev-runtime-B4WTyq8r.js +14 -0
  107. package/lib/{jsx-dev-runtime-CWLRA2F3.js → jsx-dev-runtime-BLfA-EhI.js} +2 -2
  108. package/lib/jsx-dev-runtime-CGt4kNYr.d.ts +6 -0
  109. package/lib/jsx-renderer-CfifM4hT.js +99 -0
  110. package/lib/jsx-renderer.js +1 -98
  111. package/lib/jsx-runtime-COCRWBJ9.js +2 -0
  112. package/lib/jsx.js +5 -46
  113. package/lib/{jwk-LkzeoaU8.js → jwk-CDPpYQpD.js} +3 -3
  114. package/lib/jwk.js +1 -1
  115. package/lib/{jwt-BGJBw5AF.js → jwt-BXoIA2fQ.js} +2 -2
  116. package/lib/{jwt-CskdhEW9.js → jwt-lyS4Q3mT.js} +3 -3
  117. package/lib/jwt.js +2 -2
  118. package/lib/kv/db.d.ts +1 -10
  119. package/lib/kv/db.js +1 -56
  120. package/lib/kv/json.d.ts +1 -9
  121. package/lib/kv/json.js +1 -223
  122. package/lib/{language-D9a4zqvn.js → language-DeGRtLJt.js} +2 -2
  123. package/lib/language.js +1 -1
  124. package/lib/{linear-router-C3FPib6h.js → linear-router-CmZn_JLV.js} +2 -2
  125. package/lib/logger-Ckxu8diz.js +54 -0
  126. package/lib/logger.js +1 -53
  127. package/lib/loopback-only-BbasoF7R.js +90 -0
  128. package/lib/loopback-only.js +1 -89
  129. package/lib/method-not-allowed-TO2_hnfl.js +82 -0
  130. package/lib/method-not-allowed.js +1 -81
  131. package/lib/method-override-DnZKfJbV.js +103 -0
  132. package/lib/method-override.js +1 -102
  133. package/lib/middleware-BfG_fp8P.js +12 -0
  134. package/lib/mime-BnScXkQn.js +77 -0
  135. package/lib/mount-QkK_3NtJ.js +67 -0
  136. package/lib/mount.js +1 -66
  137. package/lib/node-server/conninfo.d.ts +1 -10
  138. package/lib/node-server/conninfo.js +1 -18
  139. package/lib/node-server/register.js +25 -2
  140. package/lib/node-server/serve-static.d.ts +1 -17
  141. package/lib/node-server/serve-static.js +1 -1
  142. package/lib/node-server.d.ts +2 -2
  143. package/lib/node-server.js +4 -4
  144. package/lib/open-BqK4D2Wn.d.ts +11 -0
  145. package/lib/open-But9E2rL.js +57 -0
  146. package/lib/open-CULVJgOc.d.ts +12 -0
  147. package/lib/open-Dp4sxspc.js +224 -0
  148. package/lib/open-T7kgbO2O.d.ts +27 -0
  149. package/lib/{pattern-router-HZxjDGEQ.js → pattern-router-CJBecJBd.js} +1 -1
  150. package/lib/powered-by-CXQNjNIX.js +24 -0
  151. package/lib/powered-by.js +1 -23
  152. package/lib/pretty-json-CeC1yvYt.js +39 -0
  153. package/lib/pretty-json.js +1 -38
  154. package/lib/{protocol-C37gPePD.d.ts → protocol-CR31M7K_.d.ts} +2 -0
  155. package/lib/protocol-Dt0AZBOz.js +213 -0
  156. package/lib/proxy-C2w_mH7k.js +121 -0
  157. package/lib/proxy.js +1 -120
  158. package/lib/quick.js +4 -4
  159. package/lib/{reg-exp-router-DC1R3qoY.js → reg-exp-router-C_WV84eV.js} +2 -2
  160. package/lib/request-DvMgGtlW.js +323 -0
  161. package/lib/request.js +1 -322
  162. package/lib/route-D8m-a2I1.js +126 -0
  163. package/lib/route.js +1 -125
  164. package/lib/router/linear-router.js +1 -1
  165. package/lib/router/pattern-router.js +1 -1
  166. package/lib/router/reg-exp-router.js +1 -1
  167. package/lib/router/smart-router.js +1 -1
  168. package/lib/router/trie-router.js +1 -1
  169. package/lib/router-Be_o6nFu.js +35 -0
  170. package/lib/router.js +1 -34
  171. package/lib/schema-validator-EK3cUUCt.js +22 -0
  172. package/lib/schema-validator.js +1 -21
  173. package/lib/{secure-headers-mP3j6Hm6.js → secure-headers-C5ZOcXqk.js} +1 -1
  174. package/lib/secure-headers.js +1 -1
  175. package/lib/serve-static-B7sqnftW.d.ts +18 -0
  176. package/lib/{serve-static-3uo_9rjF.js → serve-static-C1Nkj_xB.js} +2 -2
  177. package/lib/{serve-static-Cp5adzGo.js → serve-static-EiJf1zkO.js} +4 -4
  178. package/lib/serve-static.js +1 -1
  179. package/lib/{smart-router-C5CZ5XSM.js → smart-router-BjxkDQCE.js} +1 -1
  180. package/lib/{snapshot-text-BLz8nlSc.js → snapshot-text-CtQm3n8n.js} +23 -14
  181. package/lib/{src-dy3Fk9tA.js → src-BTBm4OPD.js} +6 -6
  182. package/lib/src-LX0E4ec4.js +117 -0
  183. package/lib/{ssg-BwjcA-Ge.js → ssg-BEl7MHqg.js} +5 -5
  184. package/lib/ssg.js +1 -1
  185. package/lib/stream-CzM_SHkq.js +88 -0
  186. package/lib/{streaming-DHST3Scg.js → streaming-BlDaph8U.js} +3 -3
  187. package/lib/streaming.js +1 -1
  188. package/lib/{test-websocket-D8WWqE5G.js → test-websocket-BLeEvybj.js} +2 -2
  189. package/lib/testing-BQx5T_UX.js +17 -0
  190. package/lib/testing.js +1 -16
  191. package/lib/timeout-FeqZ9A-0.js +42 -0
  192. package/lib/timeout.js +1 -41
  193. package/lib/tiny.js +2 -2
  194. package/lib/trailing-slash-DwTx-1sk.js +85 -0
  195. package/lib/trailing-slash.js +1 -84
  196. package/lib/{trie-router-CfFbapzL.js → trie-router-JydLKUa2.js} +2 -2
  197. package/lib/url-BPh4d6j-.js +197 -0
  198. package/lib/utils/accept.js +1 -192
  199. package/lib/utils/basic-auth.js +1 -19
  200. package/lib/utils/body.js +1 -93
  201. package/lib/utils/buffer.js +1 -50
  202. package/lib/utils/color.js +1 -36
  203. package/lib/utils/compress.d.ts +1 -10
  204. package/lib/utils/compress.js +1 -10
  205. package/lib/utils/concurrent.js +1 -29
  206. package/lib/utils/constants.js +1 -6
  207. package/lib/utils/cookie.js +1 -124
  208. package/lib/utils/crypto.js +1 -33
  209. package/lib/utils/encode.js +1 -32
  210. package/lib/utils/handler.js +1 -11
  211. package/lib/utils/html.js +1 -102
  212. package/lib/utils/ipaddr.js +1 -237
  213. package/lib/utils/jwt.js +1 -1
  214. package/lib/utils/mime.js +1 -76
  215. package/lib/utils/stream.js +1 -87
  216. package/lib/utils/url.js +1 -196
  217. package/lib/{validator-D84mq-dO.js → validator-Btiyt5VJ.js} +3 -3
  218. package/lib/validator.js +1 -1
  219. package/lib/websocket-DsyBIKVA.js +56 -0
  220. package/lib/ws.js +1 -55
  221. package/package.json +1 -1
  222. package/plugins/acryl-door/src/cli/acryl.ts +36 -5
  223. package/plugins/acryl-door/src/host/channel.ts +14 -6
  224. package/plugins/acryl-door/src/host/chat-tools.ts +2 -0
  225. package/plugins/acryl-door/src/host/door.ts +97 -12
  226. package/plugins/acryl-door/src/host/page-routes.ts +3 -1
  227. package/plugins/acryl-door/src/host/shell.ts +147 -0
  228. package/plugins/acryl-door/src/host/shim.ts +39 -3
  229. package/plugins/acryl-door/src/host/verbs.ts +39 -2
  230. package/plugins/acryl-door/src/index.ts +22 -3
  231. package/plugins/acryl-door/src/page/channel.ts +3 -1
  232. package/plugins/acryl-door/src/page/index.ts +2 -1
  233. package/plugins/acryl-door/src/page/protocol.ts +2 -0
  234. package/plugins/acryl-door/src/shell/protocol.ts +130 -0
  235. package/plugins/acryl-node-server/src/register.ts +29 -2
  236. package/plugins/agent/model.ts +6 -1
  237. package/plugins/agent/tools.ts +12 -2
  238. package/plugins/agent-panel/client/describe.ts +44 -0
  239. package/plugins/agent-panel/client/main.ts +4 -1
  240. package/plugins/agent-panel/headless.ts +29 -8
  241. package/plugins/agent-panel/index.ts +22 -10
  242. package/plugins/agent-panel/launch.ts +67 -0
  243. package/plugins/desktop/attach.ts +72 -0
  244. package/plugins/desktop/door-shell.ts +379 -0
  245. package/plugins/desktop/main.ts +82 -15
  246. package/plugins/extensions/index.ts +3 -4
  247. package/plugins/extensions/stage.ts +33 -5
  248. package/plugins/openai/index.ts +1 -1
  249. package/templates/agent/AGENTS.md +4 -0
  250. package/templates/agent/blend.yaml +4 -1
  251. package/templates/agent/src/app.ts +2 -0
  252. package/templates/agent/src/routes.ts +1 -1
  253. package/templates/agent/template.json +1 -0
  254. package/templates/agent/test/builder.test.ts +26 -21
  255. package/lib/helpers-CvTauQT5.js +0 -5
  256. package/lib/middleware-CPQ8himN.js +0 -12
  257. package/plugins/acryl-router/src/connection.d.ts +0 -25
  258. package/plugins/acryl-router/src/core/app-base.d.ts +0 -215
  259. package/plugins/acryl-router/src/core/app.d.ts +0 -19
  260. package/plugins/acryl-router/src/core/client/client.d.ts +0 -4
  261. package/plugins/acryl-router/src/core/client/fetch-result-please.d.ts +0 -35
  262. package/plugins/acryl-router/src/core/client/index.d.ts +0 -7
  263. package/plugins/acryl-router/src/core/client/types.d.ts +0 -229
  264. package/plugins/acryl-router/src/core/client/utils.d.ts +0 -18
  265. package/plugins/acryl-router/src/core/compose.d.ts +0 -14
  266. package/plugins/acryl-router/src/core/context.d.ts +0 -443
  267. package/plugins/acryl-router/src/core/helper/accepts/accepts.d.ts +0 -30
  268. package/plugins/acryl-router/src/core/helper/accepts/index.d.ts +0 -5
  269. package/plugins/acryl-router/src/core/helper/conninfo/index.d.ts +0 -5
  270. package/plugins/acryl-router/src/core/helper/conninfo/types.d.ts +0 -36
  271. package/plugins/acryl-router/src/core/helper/cookie/index.d.ts +0 -25
  272. package/plugins/acryl-router/src/core/helper/css/common.d.ts +0 -59
  273. package/plugins/acryl-router/src/core/helper/css/index.d.ts +0 -80
  274. package/plugins/acryl-router/src/core/helper/dev/index.d.ts +0 -20
  275. package/plugins/acryl-router/src/core/helper/factory/index.d.ts +0 -82
  276. package/plugins/acryl-router/src/core/helper/html/index.d.ts +0 -8
  277. package/plugins/acryl-router/src/core/helper/proxy/index.d.ts +0 -73
  278. package/plugins/acryl-router/src/core/helper/route/index.d.ts +0 -75
  279. package/plugins/acryl-router/src/core/helper/ssg/index.d.ts +0 -7
  280. package/plugins/acryl-router/src/core/helper/ssg/middleware.d.ts +0 -44
  281. package/plugins/acryl-router/src/core/helper/ssg/plugins.d.ts +0 -27
  282. package/plugins/acryl-router/src/core/helper/ssg/ssg.d.ts +0 -91
  283. package/plugins/acryl-router/src/core/helper/ssg/utils.d.ts +0 -16
  284. package/plugins/acryl-router/src/core/helper/streaming/index.d.ts +0 -8
  285. package/plugins/acryl-router/src/core/helper/streaming/sse.d.ts +0 -13
  286. package/plugins/acryl-router/src/core/helper/streaming/stream.d.ts +0 -3
  287. package/plugins/acryl-router/src/core/helper/streaming/text.d.ts +0 -3
  288. package/plugins/acryl-router/src/core/helper/streaming/utils.d.ts +0 -1
  289. package/plugins/acryl-router/src/core/helper/testing/index.d.ts +0 -13
  290. package/plugins/acryl-router/src/core/helper/websocket/index.d.ts +0 -70
  291. package/plugins/acryl-router/src/core/http-exception.d.ts +0 -58
  292. package/plugins/acryl-router/src/core/index.d.ts +0 -37
  293. package/plugins/acryl-router/src/core/jsx/base.d.ts +0 -61
  294. package/plugins/acryl-router/src/core/jsx/children.d.ts +0 -9
  295. package/plugins/acryl-router/src/core/jsx/components.d.ts +0 -15
  296. package/plugins/acryl-router/src/core/jsx/constants.d.ts +0 -6
  297. package/plugins/acryl-router/src/core/jsx/context.d.ts +0 -51
  298. package/plugins/acryl-router/src/core/jsx/dom/components.d.ts +0 -10
  299. package/plugins/acryl-router/src/core/jsx/dom/context.d.ts +0 -3
  300. package/plugins/acryl-router/src/core/jsx/dom/css.d.ts +0 -83
  301. package/plugins/acryl-router/src/core/jsx/dom/hooks/index.d.ts +0 -39
  302. package/plugins/acryl-router/src/core/jsx/dom/intrinsic-element/components.d.ts +0 -16
  303. package/plugins/acryl-router/src/core/jsx/dom/jsx-dev-runtime.d.ts +0 -8
  304. package/plugins/acryl-router/src/core/jsx/dom/jsx-runtime.d.ts +0 -7
  305. package/plugins/acryl-router/src/core/jsx/dom/render.d.ts +0 -73
  306. package/plugins/acryl-router/src/core/jsx/dom/utils.d.ts +0 -1
  307. package/plugins/acryl-router/src/core/jsx/hooks/index.d.ts +0 -47
  308. package/plugins/acryl-router/src/core/jsx/index.d.ts +0 -76
  309. package/plugins/acryl-router/src/core/jsx/intrinsic-element/common.d.ts +0 -6
  310. package/plugins/acryl-router/src/core/jsx/intrinsic-element/components.d.ts +0 -13
  311. package/plugins/acryl-router/src/core/jsx/intrinsic-elements.d.ts +0 -735
  312. package/plugins/acryl-router/src/core/jsx/streaming.d.ts +0 -36
  313. package/plugins/acryl-router/src/core/jsx/types.d.ts +0 -36
  314. package/plugins/acryl-router/src/core/jsx/utils.d.ts +0 -4
  315. package/plugins/acryl-router/src/core/request/constants.d.ts +0 -1
  316. package/plugins/acryl-router/src/core/request.d.ts +0 -306
  317. package/plugins/acryl-router/src/core/router/reg-exp-router/index.d.ts +0 -6
  318. package/plugins/acryl-router/src/core/router/reg-exp-router/matcher.d.ts +0 -7
  319. package/plugins/acryl-router/src/core/router/reg-exp-router/node.d.ts +0 -13
  320. package/plugins/acryl-router/src/core/router/reg-exp-router/prepared-router.d.ts +0 -17
  321. package/plugins/acryl-router/src/core/router/reg-exp-router/router.d.ts +0 -11
  322. package/plugins/acryl-router/src/core/router/reg-exp-router/trie.d.ts +0 -8
  323. package/plugins/acryl-router/src/core/router/smart-router/index.d.ts +0 -5
  324. package/plugins/acryl-router/src/core/router/smart-router/router.d.ts +0 -11
  325. package/plugins/acryl-router/src/core/router/trie-router/index.d.ts +0 -5
  326. package/plugins/acryl-router/src/core/router/trie-router/node.d.ts +0 -6
  327. package/plugins/acryl-router/src/core/router/trie-router/router.d.ts +0 -7
  328. package/plugins/acryl-router/src/core/router/utils.d.ts +0 -1
  329. package/plugins/acryl-router/src/core/router.d.ts +0 -97
  330. package/plugins/acryl-router/src/core/types.d.ts +0 -573
  331. package/plugins/acryl-router/src/core/utils/accept.d.ts +0 -6
  332. package/plugins/acryl-router/src/core/utils/body.d.ts +0 -79
  333. package/plugins/acryl-router/src/core/utils/buffer.d.ts +0 -17
  334. package/plugins/acryl-router/src/core/utils/color.d.ts +0 -21
  335. package/plugins/acryl-router/src/core/utils/concurrent.d.ts +0 -11
  336. package/plugins/acryl-router/src/core/utils/constants.d.ts +0 -4
  337. package/plugins/acryl-router/src/core/utils/cookie.d.ts +0 -41
  338. package/plugins/acryl-router/src/core/utils/crypto.d.ts +0 -15
  339. package/plugins/acryl-router/src/core/utils/handler.d.ts +0 -6
  340. package/plugins/acryl-router/src/core/utils/headers.d.ts +0 -8
  341. package/plugins/acryl-router/src/core/utils/html.d.ts +0 -44
  342. package/plugins/acryl-router/src/core/utils/http-status.d.ts +0 -32
  343. package/plugins/acryl-router/src/core/utils/mime.d.ts +0 -70
  344. package/plugins/acryl-router/src/core/utils/stream.d.ts +0 -31
  345. package/plugins/acryl-router/src/core/utils/types.d.ts +0 -128
  346. package/plugins/acryl-router/src/core/utils/url.d.ts +0 -44
  347. package/plugins/acryl-router/src/core/validator/index.d.ts +0 -7
  348. package/plugins/acryl-router/src/core/validator/utils.d.ts +0 -29
  349. package/plugins/acryl-router/src/core/validator/validator.d.ts +0 -16
  350. package/plugins/acryl-router/src/helpers/index.d.ts +0 -17
  351. package/plugins/acryl-router/src/helpers/schema-validator.d.ts +0 -54
@@ -0,0 +1,67 @@
1
+ /**
2
+ * Starting a coding agent (`claude`, `codex`, `pi`) without a shell. Off Windows the program is spawned by name. On Windows an npm-installed CLI is a `.cmd` shim, which CreateProcess
3
+ * cannot start, and a shell would pass the person's message through cmd.exe quoting (`& | < > ^ "` would run or break). So the shim is read: npm writes the script it runs
4
+ * (`"%dp0%\node_modules\<package>\dist\cli.js"`), and that script is started with Node directly, as one process. A native `.exe` (the Windows installer of claude) is started as it is.
5
+ */
6
+ import { existsSync, readFileSync } from 'node:fs'
7
+ import { win32 } from 'node:path'
8
+
9
+ export interface Launch {
10
+ readonly file: string
11
+ readonly args: string[]
12
+ }
13
+
14
+ export interface LaunchDeps {
15
+ readonly platform?: NodeJS.Platform
16
+ readonly env?: NodeJS.ProcessEnv
17
+ readonly exists?: (path: string) => boolean
18
+ readonly read?: (path: string) => string
19
+ /** The Node that starts a shim's script (this process's). */
20
+ readonly execPath?: string
21
+ }
22
+
23
+ /** The script an npm `.cmd` shim runs: the quoted path after `%dp0%\`, ending in .js, .cjs or .mjs. */
24
+ const SHIM_SCRIPT = /"%dp0%[\\/]([^"\r\n]+?\.(?:mjs|cjs|js))"/iu
25
+
26
+ /**
27
+ * @returns how to start `program` with `args`, or a sentence saying why it cannot be started here. A program that is not found is also a sentence.
28
+ */
29
+ export function resolveLaunch(
30
+ program: string,
31
+ args: readonly string[],
32
+ deps: LaunchDeps = {},
33
+ ): Launch | { readonly error: string } {
34
+ const platform = deps.platform ?? process.platform
35
+ if (platform !== 'win32') return { file: program, args: [...args] }
36
+ const env = deps.env ?? process.env
37
+ const exists = deps.exists ?? existsSync
38
+ const read = deps.read ?? ((path: string) => readFileSync(path, 'utf8'))
39
+ const execPath = deps.execPath ?? process.execPath
40
+ const folders = (env.PATH ?? env.Path ?? '').split(';').filter(Boolean)
41
+ for (const folder of folders) {
42
+ for (const extension of ['.exe', '.com', '.cmd', '.bat']) {
43
+ const found = win32.join(folder, `${program}${extension}`)
44
+ if (!exists(found)) continue
45
+ if (extension === '.exe' || extension === '.com') return { file: found, args: [...args] }
46
+ let text: string
47
+ try {
48
+ text = read(found)
49
+ } catch {
50
+ return { error: `${program} is installed as ${found} but that file cannot be read` }
51
+ }
52
+ const script = SHIM_SCRIPT.exec(text)?.[1]
53
+ if (script === undefined) {
54
+ return {
55
+ error: `${program} is installed as ${found}, a batch file the panel cannot start without a shell (a shell would pass the message through cmd.exe quoting). Install the native program, or an npm package that creates an npm shim`,
56
+ }
57
+ }
58
+ const directory = win32.dirname(found)
59
+ const node = win32.join(directory, 'node.exe')
60
+ return {
61
+ file: exists(node) ? node : execPath,
62
+ args: [win32.join(directory, script), ...args],
63
+ }
64
+ }
65
+ }
66
+ return { error: `${program} was not found on the PATH` }
67
+ }
@@ -0,0 +1,72 @@
1
+ /**
2
+ * `npm run desktop` while `npm run dev` runs: the door allows one process per app folder, so a second server cannot start. Instead of failing, the window attaches to the dev server
3
+ * that is running, and its door (the same pages, the same agent, the same data); closing the window then leaves that server running.
4
+ *
5
+ * The running server is found where the door says it is: `.acryl/door.json` (its loopback address and pid). Only a live process on a loopback address that answers is attached to.
6
+ */
7
+ import { readFileSync } from 'node:fs'
8
+ import { join } from 'node:path'
9
+
10
+ export interface RunningApp {
11
+ readonly url: string
12
+ readonly pid: number
13
+ }
14
+
15
+ export interface AttachDeps {
16
+ readonly read?: (file: string) => string
17
+ /** True when a process with this pid is alive. */
18
+ readonly alive?: (pid: number) => boolean
19
+ /** Whether something answers HTTP at the address. */
20
+ readonly answers?: (url: string) => Promise<boolean>
21
+ }
22
+
23
+ const LOOPBACK = new Set(['127.0.0.1', 'localhost', '[::1]'])
24
+
25
+ const processAlive = (pid: number): boolean => {
26
+ try {
27
+ process.kill(pid, 0)
28
+ return true
29
+ } catch (error) {
30
+ return (error as NodeJS.ErrnoException).code === 'EPERM'
31
+ }
32
+ }
33
+
34
+ const httpAnswers = async (url: string): Promise<boolean> => {
35
+ try {
36
+ await fetch(url, { signal: AbortSignal.timeout(1500), redirect: 'manual' })
37
+ return true
38
+ } catch {
39
+ return false
40
+ }
41
+ }
42
+
43
+ /** @returns the dev server running from `root`, or `undefined` when there is none (no door file, a dead process, a stranger's address, nothing answering). */
44
+ export async function runningApp(
45
+ root: string,
46
+ deps: AttachDeps = {},
47
+ ): Promise<RunningApp | undefined> {
48
+ const read = deps.read ?? ((file: string) => readFileSync(file, 'utf8'))
49
+ let found: { url?: unknown; pid?: unknown }
50
+ try {
51
+ found = JSON.parse(read(join(root, '.acryl', 'door.json'))) as { url?: unknown; pid?: unknown }
52
+ } catch {
53
+ return undefined
54
+ }
55
+ if (
56
+ typeof found.url !== 'string' ||
57
+ typeof found.pid !== 'number' ||
58
+ !Number.isInteger(found.pid) ||
59
+ found.pid <= 0
60
+ )
61
+ return undefined
62
+ let parsed: URL
63
+ try {
64
+ parsed = new URL(found.url)
65
+ } catch {
66
+ return undefined
67
+ }
68
+ if (parsed.protocol !== 'http:' || !LOOPBACK.has(parsed.hostname)) return undefined
69
+ if (!(deps.alive ?? processAlive)(found.pid)) return undefined
70
+ if (!(await (deps.answers ?? httpAnswers)(parsed.origin))) return undefined
71
+ return { url: parsed.origin, pid: found.pid }
72
+ }
@@ -0,0 +1,379 @@
1
+ /**
2
+ * The desktop shell's side of the door: the app's windows and menu, for an agent that drives the app (`acryl window list|open|focus|close|resize`, `acryl menu list|click`).
3
+ *
4
+ * The door lives in the app's server, which is this process when `npm run desktop` started it and another one when the window attached to `npm run dev`; so the shell
5
+ * reaches the door the way a page does, over HTTP: it reads `.acryl/door.json` (the address and this run's secret), opens an event stream, answers each call by POST, and
6
+ * opens the stream again when the server restarts. Nothing here imports electron: `main.ts` passes in what Electron offers ({@link DesktopPort}), so the handler is tested
7
+ * without it. The door is development only; with no `.acryl/door.json` this keeps looking and does nothing else.
8
+ */
9
+ import { readFileSync } from 'node:fs'
10
+ import { join } from 'node:path'
11
+
12
+ /** A window as Electron has it, the part this file uses. */
13
+ export interface WindowPort {
14
+ readonly id: number
15
+ getTitle(): string
16
+ getURL(): string
17
+ isFocused(): boolean
18
+ isMinimized(): boolean
19
+ getSize(): number[]
20
+ restore(): void
21
+ focus(): void
22
+ close(): void
23
+ setSize(width: number, height: number): void
24
+ }
25
+
26
+ /** A menu item as Electron has it. */
27
+ export interface MenuItemPort {
28
+ readonly label: string
29
+ readonly enabled: boolean
30
+ readonly visible: boolean
31
+ readonly type: string
32
+ /** Electron's role of the item (`quit`, `close`, ...). */
33
+ readonly role?: string | undefined
34
+ readonly submenu?: { readonly items: readonly MenuItemPort[] } | null | undefined
35
+ click(): void
36
+ }
37
+
38
+ export interface DesktopPort {
39
+ /** Every window, in Electron's order. */
40
+ windows(): readonly WindowPort[]
41
+ /** A new window on a path of the app (the page is not awaited). @returns the window. */
42
+ open(path: string | undefined): WindowPort
43
+ /** The application menu's items, or none when the app has no menu. */
44
+ menu(): readonly MenuItemPort[]
45
+ }
46
+
47
+ /** The door's side of a call, as `plugins/acryl-door/src/shell/protocol.ts` defines it. */
48
+ export type ShellRequest =
49
+ | { readonly op: 'window-list' }
50
+ | { readonly op: 'window-open'; readonly path?: string }
51
+ | { readonly op: 'window-focus'; readonly id: number }
52
+ | { readonly op: 'window-close'; readonly id: number; readonly force?: boolean }
53
+ | {
54
+ readonly op: 'window-resize'
55
+ readonly id: number
56
+ readonly width: number
57
+ readonly height: number
58
+ }
59
+ | { readonly op: 'menu-list' }
60
+ | { readonly op: 'menu-click'; readonly path: string; readonly force?: boolean }
61
+
62
+ export type ShellResult =
63
+ | {
64
+ readonly windows: readonly {
65
+ id: number
66
+ title: string
67
+ path: string
68
+ focused: boolean
69
+ width: number
70
+ height: number
71
+ }[]
72
+ }
73
+ | { readonly items: readonly { path: string; label: string; enabled: boolean; ends?: boolean }[] }
74
+ | { readonly ok: true; readonly detail: string; readonly id?: number }
75
+
76
+ /** A refusal with a code the door passes on to the agent. */
77
+ export class ShellRefusal extends Error {
78
+ readonly code: string
79
+
80
+ constructor(code: string, message: string) {
81
+ super(message)
82
+ this.code = code
83
+ this.name = 'ShellRefusal'
84
+ }
85
+ }
86
+
87
+ /** The path and query a window shows, never its origin. */
88
+ function pathOf(url: string): string {
89
+ try {
90
+ const parsed = new URL(url)
91
+ return `${parsed.pathname}${parsed.search}`
92
+ } catch {
93
+ return ''
94
+ }
95
+ }
96
+
97
+ /** The menu as paths (`File>New Note`), submenus in order, separators and hidden items left out. */
98
+ export function menuPaths(
99
+ items: readonly MenuItemPort[],
100
+ prefix = '',
101
+ ): { path: string; label: string; enabled: boolean; item: MenuItemPort }[] {
102
+ const found: { path: string; label: string; enabled: boolean; item: MenuItemPort }[] = []
103
+ for (const item of items) {
104
+ if (item.type === 'separator' || !item.visible || item.label === '') continue
105
+ const path = prefix === '' ? item.label : `${prefix}>${item.label}`
106
+ const children = item.submenu?.items ?? []
107
+ if (children.length > 0) found.push(...menuPaths(children, path))
108
+ else found.push({ path, label: item.label, enabled: item.enabled, item })
109
+ }
110
+ return found
111
+ }
112
+
113
+ /** Whether pressing this item ends the desktop app: Quit always, Close Window when it is the last window. */
114
+ function endsApp(item: MenuItemPort, windows: number): boolean {
115
+ return item.role === 'quit' || (item.role === 'close' && windows <= 1)
116
+ }
117
+
118
+ /** Answer one call of the door. @throws ShellRefusal for what the app cannot do (no such window, the last window, a disabled item). */
119
+ export function handleShellCall(request: ShellRequest, desktop: DesktopPort): ShellResult {
120
+ const windowById = (id: number): WindowPort => {
121
+ const found = desktop.windows().find((window) => window.id === id)
122
+ if (found === undefined) {
123
+ const ids = desktop.windows().map((window) => String(window.id))
124
+ throw new ShellRefusal(
125
+ 'no-such-window',
126
+ `there is no window ${String(id)}${ids.length === 0 ? ' (no window is open)' : `; the windows are ${ids.join(', ')}`}`,
127
+ )
128
+ }
129
+ return found
130
+ }
131
+ switch (request.op) {
132
+ case 'window-list':
133
+ return {
134
+ windows: desktop.windows().map((window) => {
135
+ const [width = 0, height = 0] = window.getSize()
136
+ return {
137
+ id: window.id,
138
+ title: window.getTitle(),
139
+ path: pathOf(window.getURL()),
140
+ focused: window.isFocused(),
141
+ width,
142
+ height,
143
+ }
144
+ }),
145
+ }
146
+ case 'window-open': {
147
+ const window = desktop.open(request.path)
148
+ return {
149
+ ok: true,
150
+ detail: `opened window ${String(window.id)} on ${request.path ?? '/'}`,
151
+ id: window.id,
152
+ }
153
+ }
154
+ case 'window-focus': {
155
+ const window = windowById(request.id)
156
+ if (window.isMinimized()) window.restore()
157
+ window.focus()
158
+ return { ok: true, detail: `focused window ${String(window.id)}`, id: window.id }
159
+ }
160
+ case 'window-close': {
161
+ const window = windowById(request.id)
162
+ // The door refuses this first; the shell does not rely on it, because the last window ends the desktop app.
163
+ if (desktop.windows().length === 1 && request.force !== true) {
164
+ throw new ShellRefusal(
165
+ 'last-window',
166
+ `window ${String(window.id)} is the app's last window: closing it ends the desktop app. Open another window first, or pass force to end it on purpose`,
167
+ )
168
+ }
169
+ window.close()
170
+ return { ok: true, detail: `closed window ${String(window.id)}`, id: window.id }
171
+ }
172
+ case 'window-resize': {
173
+ const window = windowById(request.id)
174
+ window.setSize(request.width, request.height)
175
+ return {
176
+ ok: true,
177
+ detail: `window ${String(window.id)} is now ${String(request.width)}x${String(request.height)}`,
178
+ id: window.id,
179
+ }
180
+ }
181
+ case 'menu-list':
182
+ return {
183
+ items: menuPaths(desktop.menu()).map(({ path, label, enabled, item }) => ({
184
+ path,
185
+ label,
186
+ enabled,
187
+ ...(endsApp(item, desktop.windows().length) ? { ends: true } : {}),
188
+ })),
189
+ }
190
+ case 'menu-click': {
191
+ const wanted = request.path
192
+ .split('>')
193
+ .map((label) => label.trim())
194
+ .join('>')
195
+ const found = menuPaths(desktop.menu()).find((entry) => entry.path === wanted)
196
+ if (found === undefined)
197
+ throw new ShellRefusal(
198
+ 'no-such-menu-item',
199
+ `the menu has no item ${JSON.stringify(wanted)}; menu list prints the paths`,
200
+ )
201
+ if (!found.enabled) throw new ShellRefusal('disabled', `${found.path} is disabled`)
202
+ // Quit, or Close Window on the last window, ends the desktop app: an agent must not end the session it works in by accident.
203
+ if (endsApp(found.item, desktop.windows().length) && request.force !== true) {
204
+ throw new ShellRefusal(
205
+ 'quits-app',
206
+ `${found.path} ends the desktop app${found.item.role === 'close' ? ' (it is the last window)' : ''}. Pass force to do it on purpose`,
207
+ )
208
+ }
209
+ found.item.click()
210
+ return { ok: true, detail: `pressed ${found.path}` }
211
+ }
212
+ }
213
+ }
214
+
215
+ export interface DoorShellDeps {
216
+ readonly desktop: DesktopPort
217
+ readonly fetch?: typeof fetch
218
+ readonly read?: (file: string) => string
219
+ /** Milliseconds to wait before looking for the door again; grows to `maxDelayMs`. */
220
+ readonly retryMs?: number
221
+ readonly maxDelayMs?: number
222
+ readonly log?: (message: string) => void
223
+ }
224
+
225
+ interface DoorLocation {
226
+ readonly url: string
227
+ readonly secret: string
228
+ }
229
+
230
+ const LOOPBACK = new Set(['127.0.0.1', 'localhost', '[::1]'])
231
+
232
+ /** Where the door is now: its address and this run's secret, from `.acryl/door.json` (read again on every connection: a restarted server has a new secret). */
233
+ export function findDoor(
234
+ root: string,
235
+ read: (file: string) => string = (file) => readFileSync(file, 'utf8'),
236
+ ): DoorLocation | undefined {
237
+ try {
238
+ const file = JSON.parse(read(join(root, '.acryl', 'door.json'))) as {
239
+ url?: unknown
240
+ secret?: unknown
241
+ }
242
+ if (typeof file.url !== 'string' || typeof file.secret !== 'string' || file.secret === '')
243
+ return undefined
244
+ const parsed = new URL(file.url)
245
+ if (parsed.protocol !== 'http:' || !LOOPBACK.has(parsed.hostname)) return undefined
246
+ return { url: parsed.origin, secret: file.secret }
247
+ } catch {
248
+ return undefined
249
+ }
250
+ }
251
+
252
+ /** Server-sent events from a response body: `{ event, data }` per block. */
253
+ async function* events(
254
+ body: ReadableStream<Uint8Array>,
255
+ ): AsyncGenerator<{ event: string; data: string }> {
256
+ const reader = body.getReader()
257
+ const decoder = new TextDecoder()
258
+ let buffer = ''
259
+ try {
260
+ for (;;) {
261
+ const { done, value } = await reader.read()
262
+ if (done) return
263
+ buffer += decoder.decode(value, { stream: true })
264
+ for (let end = buffer.indexOf('\n\n'); end !== -1; end = buffer.indexOf('\n\n')) {
265
+ const block = buffer.slice(0, end)
266
+ buffer = buffer.slice(end + 2)
267
+ let event = 'message'
268
+ const data: string[] = []
269
+ for (const line of block.split('\n')) {
270
+ if (line.startsWith('event:')) event = line.slice(6).trim()
271
+ else if (line.startsWith('data:'))
272
+ data.push(line.slice(line.startsWith('data: ') ? 6 : 5))
273
+ }
274
+ yield { event, data: data.join('\n') }
275
+ }
276
+ }
277
+ } finally {
278
+ await reader.cancel().catch(() => undefined)
279
+ }
280
+ }
281
+
282
+ /**
283
+ * Keep the shell connected to the door of the app in `root` until the returned function is called. The door is found again after each loss, so a dev server that restarts (a
284
+ * change of `src/`) is followed. Each call is answered with its result, or with the refusal's code and message.
285
+ */
286
+ export function startDoorShell(root: string, deps: DoorShellDeps): () => void {
287
+ const request = deps.fetch ?? fetch
288
+ const retry = deps.retryMs ?? 500
289
+ const maxDelay = deps.maxDelayMs ?? 5000
290
+ const controller = new AbortController()
291
+ let wait: NodeJS.Timeout | undefined
292
+ const sleep = (ms: number): Promise<void> =>
293
+ new Promise((resolve) => {
294
+ wait = setTimeout(resolve, ms)
295
+ controller.signal.addEventListener(
296
+ 'abort',
297
+ () => {
298
+ clearTimeout(wait)
299
+ resolve()
300
+ },
301
+ { once: true },
302
+ )
303
+ })
304
+
305
+ const answer = async (door: DoorLocation, body: unknown): Promise<void> => {
306
+ await request(`${door.url}/api/acryl-door/shell/answer`, {
307
+ method: 'POST',
308
+ headers: { authorization: `Bearer ${door.secret}`, 'content-type': 'application/json' },
309
+ body: JSON.stringify(body),
310
+ signal: controller.signal,
311
+ }).catch(() => undefined)
312
+ }
313
+
314
+ const serve = async (door: DoorLocation): Promise<void> => {
315
+ const response = await request(`${door.url}/api/acryl-door/shell/events`, {
316
+ headers: { authorization: `Bearer ${door.secret}`, accept: 'text/event-stream' },
317
+ signal: controller.signal,
318
+ })
319
+ if (!response.ok || response.body === null)
320
+ throw new Error(`the door answered ${String(response.status)}`)
321
+ for await (const { event, data } of events(response.body)) {
322
+ if (event !== 'call') continue
323
+ let call: { id?: unknown; request?: ShellRequest }
324
+ try {
325
+ call = JSON.parse(data) as { id?: unknown; request?: ShellRequest }
326
+ } catch {
327
+ continue
328
+ }
329
+ if (typeof call.id !== 'number' || call.request === undefined) continue
330
+ try {
331
+ await answer(door, {
332
+ t: 'result',
333
+ id: call.id,
334
+ value: handleShellCall(call.request, deps.desktop),
335
+ })
336
+ } catch (cause) {
337
+ const refusal =
338
+ cause instanceof ShellRefusal
339
+ ? cause
340
+ : new ShellRefusal(
341
+ 'failed',
342
+ cause instanceof Error ? cause.message : 'the shell could not do that',
343
+ )
344
+ await answer(door, {
345
+ t: 'error',
346
+ id: call.id,
347
+ code: refusal.code,
348
+ message: refusal.message,
349
+ })
350
+ }
351
+ }
352
+ }
353
+
354
+ void (async () => {
355
+ let delay = retry
356
+ let announced = false
357
+ while (!controller.signal.aborted) {
358
+ const door = findDoor(root, deps.read)
359
+ if (door !== undefined) {
360
+ try {
361
+ if (!announced)
362
+ deps.log?.(
363
+ 'desktop: windows and menu are available to the door (acryl window list, acryl menu list)',
364
+ )
365
+ announced = true
366
+ await serve(door)
367
+ delay = retry
368
+ } catch {
369
+ // The server restarted or is not up yet: look again.
370
+ }
371
+ }
372
+ await sleep(delay)
373
+ delay = Math.min(delay * 2, maxDelay)
374
+ }
375
+ })()
376
+ return () => {
377
+ controller.abort()
378
+ }
379
+ }
@@ -9,10 +9,17 @@
9
9
  import { mkdirSync, readFileSync } from 'node:fs'
10
10
  import { join } from 'node:path'
11
11
  import { fileURLToPath } from 'node:url'
12
- import { app, BrowserWindow, shell } from 'electron'
12
+ import { app, BrowserWindow, Menu, type MenuItem, shell } from 'electron'
13
13
  import type { App } from '../../src/app.ts'
14
14
  import '../acryl-node-server/src/register.ts'
15
15
  import { selectInstance } from '../instance/index.ts'
16
+ import { runningApp } from './attach.ts'
17
+ import {
18
+ type DesktopPort,
19
+ type MenuItemPort,
20
+ startDoorShell,
21
+ type WindowPort,
22
+ } from './door-shell.ts'
16
23
  import { createDesktopShutdown, installShutdownRequests } from './shutdown.ts'
17
24
  import { resolveDesktopUserDataOverride } from './user-data.ts'
18
25
  import { windowOptions } from './window.ts'
@@ -40,9 +47,11 @@ if (!app.requestSingleInstanceLock()) {
40
47
  } else {
41
48
  let running: App | undefined
42
49
  let window: BrowserWindow | undefined
50
+ let stopDoorShell: (() => void) | undefined
43
51
 
44
52
  const shutdown = createDesktopShutdown(
45
53
  async () => {
54
+ stopDoorShell?.()
46
55
  await running?.stop()
47
56
  },
48
57
  (code) => {
@@ -65,21 +74,79 @@ if (!app.requestSingleInstanceLock()) {
65
74
 
66
75
  void app.whenReady().then(async () => {
67
76
  // The hook above is registered before the app's modules are loaded, so the app may have .tsx pages: they are loaded here, not as a static import.
68
- const { createApp } = await import('../../src/app.ts')
69
- running = await createApp(chosen)
70
- const origin = new URL(running.url).origin
71
- window = new BrowserWindow(windowOptions({ title: chosen.name }))
72
- window.once('ready-to-show', () => window?.show())
73
- // Navigation stays on the app's own server; any other address opens in the system browser.
74
- window.webContents.setWindowOpenHandler(({ url }) => {
75
- void shell.openExternal(url)
76
- return { action: 'deny' }
77
+ // A dev server is already running from this folder (`npm run dev`): the door allows one process per folder, so the window opens on that server instead of starting a second one,
78
+ // and closing the window leaves it running.
79
+ const attached = await runningApp(projectRoot)
80
+ let url: string
81
+ if (attached !== undefined) {
82
+ console.log(
83
+ `desktop: attached to the app already running from this folder at ${attached.url} (its server keeps running when the window closes)`,
84
+ )
85
+ url = attached.url
86
+ } else {
87
+ const { createApp } = await import('../../src/app.ts')
88
+ running = await createApp(chosen)
89
+ url = running.url
90
+ }
91
+ const origin = new URL(url).origin
92
+ /** One window on a path of the app. Navigation stays on the app's own server; any other address opens in the system browser. The user agent names the window to the door (`AcrylWindow/<id>`), so an agent can drive it with `--window <id>`. */
93
+ const createWindow = (path: string): BrowserWindow => {
94
+ const created = new BrowserWindow(windowOptions({ title: chosen.name }))
95
+ created.webContents.setUserAgent(
96
+ `${created.webContents.getUserAgent()} AcrylWindow/${String(created.id)}`,
97
+ )
98
+ created.once('ready-to-show', () => created.show())
99
+ created.webContents.setWindowOpenHandler(({ url }) => {
100
+ void shell.openExternal(url)
101
+ return { action: 'deny' }
102
+ })
103
+ created.webContents.on('will-navigate', (event, url) => {
104
+ if (new URL(url).origin === origin) return
105
+ event.preventDefault()
106
+ void shell.openExternal(url)
107
+ })
108
+ created.loadURL(new URL(path, url).href).catch((cause: unknown) => {
109
+ console.warn(
110
+ `desktop: the window could not load ${path}: ${cause instanceof Error ? cause.message : String(cause)}`,
111
+ )
112
+ })
113
+ return created
114
+ }
115
+ window = createWindow(startPath())
116
+ // The door (development) reaches the windows and the menu through this port: `acryl window list`, `acryl menu click`.
117
+ const live = (): BrowserWindow[] =>
118
+ BrowserWindow.getAllWindows().filter((candidate) => !candidate.isDestroyed())
119
+ const port = (candidate: BrowserWindow): WindowPort => ({
120
+ id: candidate.id,
121
+ getTitle: () => candidate.getTitle(),
122
+ getURL: () => candidate.webContents.getURL(),
123
+ isFocused: () => candidate.isFocused(),
124
+ isMinimized: () => candidate.isMinimized(),
125
+ getSize: () => candidate.getSize(),
126
+ restore: () => candidate.restore(),
127
+ focus: () => candidate.focus(),
128
+ close: () => candidate.close(),
129
+ setSize: (width, height) => candidate.setSize(width, height),
77
130
  })
78
- window.webContents.on('will-navigate', (event, url) => {
79
- if (new URL(url).origin === origin) return
80
- event.preventDefault()
81
- void shell.openExternal(url)
131
+ // Pressing an item as Electron's own menu does: with the window that has focus (the roles reload, zoom and close act on it).
132
+ const menuItem = (item: MenuItem): MenuItemPort => ({
133
+ label: item.label,
134
+ enabled: item.enabled,
135
+ visible: item.visible,
136
+ type: item.type,
137
+ role: item.role,
138
+ // Electron gives a plain item `submenu: null` on Linux and `undefined` elsewhere.
139
+ submenu: item.submenu ? { items: item.submenu.items.map(menuItem) } : null,
140
+ click: () => {
141
+ const target = BrowserWindow.getFocusedWindow() ?? live()[0]
142
+ item.click(undefined, target, target?.webContents)
143
+ },
82
144
  })
83
- await window.loadURL(new URL(startPath(), running.url).href)
145
+ const desktop: DesktopPort = {
146
+ windows: () => live().map(port),
147
+ open: (path) => port(createWindow(path ?? startPath())),
148
+ menu: () => (Menu.getApplicationMenu()?.items ?? []).map(menuItem),
149
+ }
150
+ stopDoorShell = startDoorShell(projectRoot, { desktop, log: console.log })
84
151
  })
85
152
  }
@@ -21,7 +21,6 @@ import {
21
21
  readdirSync,
22
22
  readFileSync,
23
23
  renameSync,
24
- rmSync,
25
24
  statSync,
26
25
  writeFileSync,
27
26
  } from 'node:fs'
@@ -33,7 +32,7 @@ import type {} from '../instance/index.ts'
33
32
  import type { Sandbox } from '../sandbox/index.ts'
34
33
  import { appendLedger, type LedgerEntry, readLedger } from './ledger.ts'
35
34
  import { resolveFromApp, routerAliases } from './resolve.ts'
36
- import { hashFolder, prune, stage } from './stage.ts'
35
+ import { discard, hashFolder, prune, stage } from './stage.ts'
37
36
 
38
37
  /** A fiber's lifecycle state, by name (Cordis's own enum is a `const enum`, which does not exist at run time). */
39
38
  export type ExtensionState = 'pending' | 'loading' | 'active' | 'failed' | 'disposed' | 'unloading'
@@ -611,7 +610,7 @@ export class Extensions extends Service {
611
610
  try {
612
611
  module = await this.load(staged.dir, name)
613
612
  } catch (error) {
614
- rmSync(staged.dir, { recursive: true, force: true })
613
+ discard(staged.dir)
615
614
  this.record({ kind: 'refused', name, version: staged.version, error: message(error) })
616
615
  return {
617
616
  ok: false,
@@ -638,7 +637,7 @@ export class Extensions extends Service {
638
637
  : undefined
639
638
  if (failure) {
640
639
  await mounted.fiber.dispose().catch(() => undefined)
641
- rmSync(staged.dir, { recursive: true, force: true })
640
+ discard(staged.dir)
642
641
  const error = failure.error
643
642
  this.record({ kind: 'refused', name, version: staged.version, error })
644
643
  let restored = false