acrylblends 0.2.0 → 0.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 (335) hide show
  1. package/dist/index.d.ts +22 -16
  2. package/dist/index.js +65 -20
  3. package/dist/index.js.map +1 -1
  4. package/package.json +1 -1
  5. package/plugins/acryl-inspect/LICENSE +21 -0
  6. package/plugins/acryl-inspect/plugin.json +12 -0
  7. package/plugins/acryl-inspect/src/index.ts +3 -0
  8. package/plugins/acryl-inspect/src/report.ts +126 -0
  9. package/plugins/acryl-inspect/src/service.ts +68 -0
  10. package/plugins/acryl-inspect/tests/report.spec.ts +63 -0
  11. package/plugins/acryl-inspect/tests/service.spec.ts +121 -0
  12. package/plugins/acryl-inspect/upstream.json +18 -0
  13. package/plugins/acryl-node-server/LICENSE +47 -0
  14. package/plugins/acryl-node-server/plugin.json +13 -0
  15. package/plugins/acryl-node-server/src/adapter/conninfo.ts +23 -0
  16. package/plugins/acryl-node-server/src/adapter/early-hints.ts +55 -0
  17. package/plugins/acryl-node-server/src/adapter/error.ts +11 -0
  18. package/plugins/acryl-node-server/src/adapter/headers.ts +249 -0
  19. package/plugins/acryl-node-server/src/adapter/index.ts +6 -0
  20. package/plugins/acryl-node-server/src/adapter/listener.ts +540 -0
  21. package/plugins/acryl-node-server/src/adapter/request.ts +797 -0
  22. package/plugins/acryl-node-server/src/adapter/response.ts +186 -0
  23. package/plugins/acryl-node-server/src/adapter/serve-static.ts +238 -0
  24. package/plugins/acryl-node-server/src/adapter/server.ts +36 -0
  25. package/plugins/acryl-node-server/src/adapter/types.ts +82 -0
  26. package/plugins/acryl-node-server/src/adapter/url.ts +42 -0
  27. package/plugins/acryl-node-server/src/adapter/utils/response/constants.ts +1 -0
  28. package/plugins/acryl-node-server/src/adapter/utils/response.ts +4 -0
  29. package/plugins/acryl-node-server/src/adapter/utils/stream.ts +88 -0
  30. package/plugins/acryl-node-server/src/adapter/utils.ts +96 -0
  31. package/plugins/acryl-node-server/src/adapter/websocket-types.ts +45 -0
  32. package/plugins/acryl-node-server/src/adapter/websocket.ts +375 -0
  33. package/plugins/acryl-node-server/src/index.ts +107 -0
  34. package/plugins/acryl-node-server/tests/adapter/app.ts +76 -0
  35. package/plugins/acryl-node-server/tests/adapter/assets/.static/plain.txt +1 -0
  36. package/plugins/acryl-node-server/tests/adapter/assets/favicon.ico +0 -0
  37. package/plugins/acryl-node-server/tests/adapter/assets/secret.txt +1 -0
  38. package/plugins/acryl-node-server/tests/adapter/assets/static/acryl.html +1 -0
  39. package/plugins/acryl-node-server/tests/adapter/assets/static/admin/secret.txt +1 -0
  40. package/plugins/acryl-node-server/tests/adapter/assets/static/data.json +5 -0
  41. package/plugins/acryl-node-server/tests/adapter/assets/static/extensionless +1 -0
  42. package/plugins/acryl-node-server/tests/adapter/assets/static/foo..bar.txt +0 -0
  43. package/plugins/acryl-node-server/tests/adapter/assets/static/index.html +1 -0
  44. package/plugins/acryl-node-server/tests/adapter/assets/static/plain.txt +1 -0
  45. package/plugins/acryl-node-server/tests/adapter/assets/static-with-context-path/plain.txt +1 -0
  46. package/plugins/acryl-node-server/tests/adapter/assets/static-with-precompressed/hello.bin +1 -0
  47. package/plugins/acryl-node-server/tests/adapter/assets/static-with-precompressed/hello.bin.br +1 -0
  48. package/plugins/acryl-node-server/tests/adapter/assets/static-with-precompressed/hello.txt +1 -0
  49. package/plugins/acryl-node-server/tests/adapter/assets/static-with-precompressed/hello.txt.br +1 -0
  50. package/plugins/acryl-node-server/tests/adapter/assets/static-with-precompressed/hello.txt.zst +1 -0
  51. package/plugins/acryl-node-server/tests/adapter/conninfo.test.ts +34 -0
  52. package/plugins/acryl-node-server/tests/adapter/content-length.test.ts +248 -0
  53. package/plugins/acryl-node-server/tests/adapter/early-hints.test.ts +457 -0
  54. package/plugins/acryl-node-server/tests/adapter/fixtures/keys/agent1-cert.pem +23 -0
  55. package/plugins/acryl-node-server/tests/adapter/fixtures/keys/agent1-key.pem +27 -0
  56. package/plugins/acryl-node-server/tests/adapter/headers.test.ts +252 -0
  57. package/plugins/acryl-node-server/tests/adapter/helpers/request.ts +190 -0
  58. package/plugins/acryl-node-server/tests/adapter/listener.test.ts +727 -0
  59. package/plugins/acryl-node-server/tests/adapter/request-client-disconnect.test.ts +189 -0
  60. package/plugins/acryl-node-server/tests/adapter/request.test.ts +1230 -0
  61. package/plugins/acryl-node-server/tests/adapter/response.test.ts +285 -0
  62. package/plugins/acryl-node-server/tests/adapter/serve-static.test.ts +849 -0
  63. package/plugins/acryl-node-server/tests/adapter/server-socket.test.ts +790 -0
  64. package/plugins/acryl-node-server/tests/adapter/server.test.ts +1302 -0
  65. package/plugins/acryl-node-server/tests/adapter/setup.ts +16 -0
  66. package/plugins/acryl-node-server/tests/adapter/url.test.ts +76 -0
  67. package/plugins/acryl-node-server/tests/adapter/utils/response.test.ts +23 -0
  68. package/plugins/acryl-node-server/tests/adapter/utils/stream.test.ts +75 -0
  69. package/plugins/acryl-node-server/tests/adapter/utils.test.ts +235 -0
  70. package/plugins/acryl-node-server/tests/adapter/websocket.test.ts +260 -0
  71. package/plugins/acryl-node-server/tests/server.spec.ts +141 -0
  72. package/plugins/acryl-node-server/upstream.json +18 -0
  73. package/plugins/acryl-router/LICENSE +47 -0
  74. package/plugins/acryl-router/plugin.json +13 -0
  75. package/plugins/acryl-router/src/client.ts +2 -0
  76. package/plugins/acryl-router/src/connection.ts +42 -0
  77. package/plugins/acryl-router/src/core/app-base.ts +544 -0
  78. package/plugins/acryl-router/src/core/app.test.ts +3833 -0
  79. package/plugins/acryl-router/src/core/app.ts +34 -0
  80. package/plugins/acryl-router/src/core/client/client.test.ts +2313 -0
  81. package/plugins/acryl-router/src/core/client/client.ts +249 -0
  82. package/plugins/acryl-router/src/core/client/fetch-result-please.ts +95 -0
  83. package/plugins/acryl-router/src/core/client/index.ts +17 -0
  84. package/plugins/acryl-router/src/core/client/types.test.ts +149 -0
  85. package/plugins/acryl-router/src/core/client/types.ts +393 -0
  86. package/plugins/acryl-router/src/core/client/utils.test.ts +405 -0
  87. package/plugins/acryl-router/src/core/client/utils.ts +117 -0
  88. package/plugins/acryl-router/src/core/compose.test.ts +751 -0
  89. package/plugins/acryl-router/src/core/compose.ts +73 -0
  90. package/plugins/acryl-router/src/core/context.test.ts +629 -0
  91. package/plugins/acryl-router/src/core/context.ts +781 -0
  92. package/plugins/acryl-router/src/core/helper/accepts/accepts.test.ts +344 -0
  93. package/plugins/acryl-router/src/core/helper/accepts/accepts.ts +96 -0
  94. package/plugins/acryl-router/src/core/helper/accepts/index.ts +6 -0
  95. package/plugins/acryl-router/src/core/helper/adapter/index.test.ts +55 -0
  96. package/plugins/acryl-router/src/core/helper/adapter/index.ts +90 -0
  97. package/plugins/acryl-router/src/core/helper/conninfo/index.ts +6 -0
  98. package/plugins/acryl-router/src/core/helper/conninfo/types.ts +45 -0
  99. package/plugins/acryl-router/src/core/helper/cookie/index.test.ts +510 -0
  100. package/plugins/acryl-router/src/core/helper/cookie/index.ts +146 -0
  101. package/plugins/acryl-router/src/core/helper/factory/index.test.ts +434 -0
  102. package/plugins/acryl-router/src/core/helper/factory/index.ts +375 -0
  103. package/plugins/acryl-router/src/core/helper/html/index.test.ts +78 -0
  104. package/plugins/acryl-router/src/core/helper/html/index.ts +57 -0
  105. package/plugins/acryl-router/src/core/helper/route/index.test.ts +259 -0
  106. package/plugins/acryl-router/src/core/helper/route/index.ts +144 -0
  107. package/plugins/acryl-router/src/core/helper/streaming/index.ts +9 -0
  108. package/plugins/acryl-router/src/core/helper/streaming/sse.ts +101 -0
  109. package/plugins/acryl-router/src/core/helper/streaming/stream.test.ts +128 -0
  110. package/plugins/acryl-router/src/core/helper/streaming/stream.ts +45 -0
  111. package/plugins/acryl-router/src/core/helper/streaming/text.test.ts +34 -0
  112. package/plugins/acryl-router/src/core/helper/streaming/text.ts +15 -0
  113. package/plugins/acryl-router/src/core/helper/streaming/utils.ts +11 -0
  114. package/plugins/acryl-router/src/core/helper/testing/index.test.ts +42 -0
  115. package/plugins/acryl-router/src/core/helper/testing/index.ts +28 -0
  116. package/plugins/acryl-router/src/core/helper/websocket/index.test.ts +124 -0
  117. package/plugins/acryl-router/src/core/helper/websocket/index.ts +140 -0
  118. package/plugins/acryl-router/src/core/http-exception.test.ts +45 -0
  119. package/plugins/acryl-router/src/core/http-exception.ts +77 -0
  120. package/plugins/acryl-router/src/core/index.ts +53 -0
  121. package/plugins/acryl-router/src/core/middleware/basic-auth/index.test.ts +434 -0
  122. package/plugins/acryl-router/src/core/middleware/basic-auth/index.ts +152 -0
  123. package/plugins/acryl-router/src/core/middleware/bearer-auth/index.test.ts +1018 -0
  124. package/plugins/acryl-router/src/core/middleware/bearer-auth/index.ts +220 -0
  125. package/plugins/acryl-router/src/core/middleware/body-limit/index.test.ts +273 -0
  126. package/plugins/acryl-router/src/core/middleware/body-limit/index.ts +110 -0
  127. package/plugins/acryl-router/src/core/middleware/combine/index.test.ts +355 -0
  128. package/plugins/acryl-router/src/core/middleware/combine/index.ts +171 -0
  129. package/plugins/acryl-router/src/core/middleware/compress/index.test.ts +535 -0
  130. package/plugins/acryl-router/src/core/middleware/compress/index.ts +142 -0
  131. package/plugins/acryl-router/src/core/middleware/context-storage/index.test.ts +43 -0
  132. package/plugins/acryl-router/src/core/middleware/context-storage/index.ts +58 -0
  133. package/plugins/acryl-router/src/core/middleware/cors/index.test.ts +532 -0
  134. package/plugins/acryl-router/src/core/middleware/cors/index.ts +163 -0
  135. package/plugins/acryl-router/src/core/middleware/csrf/index.test.ts +534 -0
  136. package/plugins/acryl-router/src/core/middleware/csrf/index.ts +150 -0
  137. package/plugins/acryl-router/src/core/middleware/etag/digest.test.ts +21 -0
  138. package/plugins/acryl-router/src/core/middleware/etag/digest.ts +95 -0
  139. package/plugins/acryl-router/src/core/middleware/etag/index.test.ts +503 -0
  140. package/plugins/acryl-router/src/core/middleware/etag/index.ts +135 -0
  141. package/plugins/acryl-router/src/core/middleware/ip-restriction/index.test.ts +217 -0
  142. package/plugins/acryl-router/src/core/middleware/ip-restriction/index.ts +278 -0
  143. package/plugins/acryl-router/src/core/middleware/jwk/index.test.ts +1135 -0
  144. package/plugins/acryl-router/src/core/middleware/jwk/index.ts +1 -0
  145. package/plugins/acryl-router/src/core/middleware/jwk/jwk.ts +195 -0
  146. package/plugins/acryl-router/src/core/middleware/jwk/keys.test.json +48 -0
  147. package/plugins/acryl-router/src/core/middleware/jwt/index.test.ts +802 -0
  148. package/plugins/acryl-router/src/core/middleware/jwt/index.ts +9 -0
  149. package/plugins/acryl-router/src/core/middleware/jwt/jwt.ts +189 -0
  150. package/plugins/acryl-router/src/core/middleware/language/index.test.ts +552 -0
  151. package/plugins/acryl-router/src/core/middleware/language/index.ts +12 -0
  152. package/plugins/acryl-router/src/core/middleware/language/language.ts +314 -0
  153. package/plugins/acryl-router/src/core/middleware/logger/index.test.ts +204 -0
  154. package/plugins/acryl-router/src/core/middleware/logger/index.ts +95 -0
  155. package/plugins/acryl-router/src/core/middleware/method-not-allowed/index.test.ts +292 -0
  156. package/plugins/acryl-router/src/core/middleware/method-not-allowed/index.ts +141 -0
  157. package/plugins/acryl-router/src/core/middleware/method-override/index.test.ts +221 -0
  158. package/plugins/acryl-router/src/core/middleware/method-override/index.ts +147 -0
  159. package/plugins/acryl-router/src/core/middleware/powered-by/index.test.ts +37 -0
  160. package/plugins/acryl-router/src/core/middleware/powered-by/index.ts +35 -0
  161. package/plugins/acryl-router/src/core/middleware/pretty-json/index.test.ts +218 -0
  162. package/plugins/acryl-router/src/core/middleware/pretty-json/index.ts +67 -0
  163. package/plugins/acryl-router/src/core/middleware/request-id/index.test.ts +168 -0
  164. package/plugins/acryl-router/src/core/middleware/request-id/index.ts +7 -0
  165. package/plugins/acryl-router/src/core/middleware/request-id/request-id.ts +59 -0
  166. package/plugins/acryl-router/src/core/middleware/secure-headers/index.test.ts +671 -0
  167. package/plugins/acryl-router/src/core/middleware/secure-headers/index.ts +8 -0
  168. package/plugins/acryl-router/src/core/middleware/secure-headers/permissions-policy.ts +101 -0
  169. package/plugins/acryl-router/src/core/middleware/secure-headers/secure-headers.ts +340 -0
  170. package/plugins/acryl-router/src/core/middleware/serve-static/index.test.ts +450 -0
  171. package/plugins/acryl-router/src/core/middleware/serve-static/index.ts +135 -0
  172. package/plugins/acryl-router/src/core/middleware/serve-static/path.test.ts +50 -0
  173. package/plugins/acryl-router/src/core/middleware/serve-static/path.ts +25 -0
  174. package/plugins/acryl-router/src/core/middleware/timeout/index.test.ts +70 -0
  175. package/plugins/acryl-router/src/core/middleware/timeout/index.ts +58 -0
  176. package/plugins/acryl-router/src/core/middleware/timing/index.test.ts +310 -0
  177. package/plugins/acryl-router/src/core/middleware/timing/index.ts +8 -0
  178. package/plugins/acryl-router/src/core/middleware/timing/timing.ts +256 -0
  179. package/plugins/acryl-router/src/core/middleware/trailing-slash/index.test.ts +381 -0
  180. package/plugins/acryl-router/src/core/middleware/trailing-slash/index.ts +156 -0
  181. package/plugins/acryl-router/src/core/request/constants.ts +1 -0
  182. package/plugins/acryl-router/src/core/request.test.ts +751 -0
  183. package/plugins/acryl-router/src/core/request.ts +502 -0
  184. package/plugins/acryl-router/src/core/router/common.case.test.ts +1132 -0
  185. package/plugins/acryl-router/src/core/router/linear-router/index.ts +6 -0
  186. package/plugins/acryl-router/src/core/router/linear-router/router.test.ts +80 -0
  187. package/plugins/acryl-router/src/core/router/linear-router/router.ts +152 -0
  188. package/plugins/acryl-router/src/core/router/pattern-router/index.ts +6 -0
  189. package/plugins/acryl-router/src/core/router/pattern-router/router.test.ts +46 -0
  190. package/plugins/acryl-router/src/core/router/pattern-router/router.ts +54 -0
  191. package/plugins/acryl-router/src/core/router/reg-exp-router/index.ts +7 -0
  192. package/plugins/acryl-router/src/core/router/reg-exp-router/matcher.ts +33 -0
  193. package/plugins/acryl-router/src/core/router/reg-exp-router/node.ts +170 -0
  194. package/plugins/acryl-router/src/core/router/reg-exp-router/prepared-router.test.ts +154 -0
  195. package/plugins/acryl-router/src/core/router/reg-exp-router/prepared-router.ts +165 -0
  196. package/plugins/acryl-router/src/core/router/reg-exp-router/router.test.ts +265 -0
  197. package/plugins/acryl-router/src/core/router/reg-exp-router/router.ts +174 -0
  198. package/plugins/acryl-router/src/core/router/reg-exp-router/trie.ts +84 -0
  199. package/plugins/acryl-router/src/core/router/smart-router/index.ts +6 -0
  200. package/plugins/acryl-router/src/core/router/smart-router/router.test.ts +13 -0
  201. package/plugins/acryl-router/src/core/router/smart-router/router.ts +70 -0
  202. package/plugins/acryl-router/src/core/router/trie-router/index.ts +6 -0
  203. package/plugins/acryl-router/src/core/router/trie-router/node.test.ts +896 -0
  204. package/plugins/acryl-router/src/core/router/trie-router/node.ts +221 -0
  205. package/plugins/acryl-router/src/core/router/trie-router/router.test.ts +8 -0
  206. package/plugins/acryl-router/src/core/router/trie-router/router.ts +18 -0
  207. package/plugins/acryl-router/src/core/router/utils.ts +1 -0
  208. package/plugins/acryl-router/src/core/router.ts +103 -0
  209. package/plugins/acryl-router/src/core/types.test.ts +4134 -0
  210. package/plugins/acryl-router/src/core/types.ts +2782 -0
  211. package/plugins/acryl-router/src/core/utils/accept.test.ts +299 -0
  212. package/plugins/acryl-router/src/core/utils/accept.ts +268 -0
  213. package/plugins/acryl-router/src/core/utils/basic-auth.test.ts +56 -0
  214. package/plugins/acryl-router/src/core/utils/basic-auth.ts +26 -0
  215. package/plugins/acryl-router/src/core/utils/body.test.ts +496 -0
  216. package/plugins/acryl-router/src/core/utils/body.ts +263 -0
  217. package/plugins/acryl-router/src/core/utils/buffer.test.ts +147 -0
  218. package/plugins/acryl-router/src/core/utils/buffer.ts +117 -0
  219. package/plugins/acryl-router/src/core/utils/color.test.ts +23 -0
  220. package/plugins/acryl-router/src/core/utils/color.ts +56 -0
  221. package/plugins/acryl-router/src/core/utils/compress.ts +10 -0
  222. package/plugins/acryl-router/src/core/utils/concurrent.test.ts +69 -0
  223. package/plugins/acryl-router/src/core/utils/concurrent.ts +55 -0
  224. package/plugins/acryl-router/src/core/utils/constants.ts +4 -0
  225. package/plugins/acryl-router/src/core/utils/cookie.test.ts +460 -0
  226. package/plugins/acryl-router/src/core/utils/cookie.ts +281 -0
  227. package/plugins/acryl-router/src/core/utils/crypto.test.ts +43 -0
  228. package/plugins/acryl-router/src/core/utils/crypto.ts +58 -0
  229. package/plugins/acryl-router/src/core/utils/encode.test.ts +61 -0
  230. package/plugins/acryl-router/src/core/utils/encode.ts +34 -0
  231. package/plugins/acryl-router/src/core/utils/filepath.test.ts +78 -0
  232. package/plugins/acryl-router/src/core/utils/filepath.ts +60 -0
  233. package/plugins/acryl-router/src/core/utils/handler.ts +15 -0
  234. package/plugins/acryl-router/src/core/utils/headers.ts +355 -0
  235. package/plugins/acryl-router/src/core/utils/html.test.ts +16 -0
  236. package/plugins/acryl-router/src/core/utils/html.ts +182 -0
  237. package/plugins/acryl-router/src/core/utils/http-status.ts +72 -0
  238. package/plugins/acryl-router/src/core/utils/ipaddr.test.ts +149 -0
  239. package/plugins/acryl-router/src/core/utils/ipaddr.ts +374 -0
  240. package/plugins/acryl-router/src/core/utils/jwt/index.ts +7 -0
  241. package/plugins/acryl-router/src/core/utils/jwt/jwa.test.ts +86 -0
  242. package/plugins/acryl-router/src/core/utils/jwt/jwa.ts +38 -0
  243. package/plugins/acryl-router/src/core/utils/jwt/jws.ts +235 -0
  244. package/plugins/acryl-router/src/core/utils/jwt/jwt.test.ts +1788 -0
  245. package/plugins/acryl-router/src/core/utils/jwt/jwt.ts +292 -0
  246. package/plugins/acryl-router/src/core/utils/jwt/types.ts +157 -0
  247. package/plugins/acryl-router/src/core/utils/jwt/utf8.ts +7 -0
  248. package/plugins/acryl-router/src/core/utils/mime.test.ts +64 -0
  249. package/plugins/acryl-router/src/core/utils/mime.ts +94 -0
  250. package/plugins/acryl-router/src/core/utils/stream.test.ts +212 -0
  251. package/plugins/acryl-router/src/core/utils/stream.ts +107 -0
  252. package/plugins/acryl-router/src/core/utils/types.test.ts +295 -0
  253. package/plugins/acryl-router/src/core/utils/types.ts +123 -0
  254. package/plugins/acryl-router/src/core/utils/url.test.ts +384 -0
  255. package/plugins/acryl-router/src/core/utils/url.ts +337 -0
  256. package/plugins/acryl-router/src/core/validator/index.ts +8 -0
  257. package/plugins/acryl-router/src/core/validator/utils.test.ts +82 -0
  258. package/plugins/acryl-router/src/core/validator/utils.ts +69 -0
  259. package/plugins/acryl-router/src/core/validator/validator.test.ts +1488 -0
  260. package/plugins/acryl-router/src/core/validator/validator.ts +185 -0
  261. package/plugins/acryl-router/src/helpers/index.ts +13 -0
  262. package/plugins/acryl-router/src/helpers/schema-validator.ts +35 -0
  263. package/plugins/acryl-router/src/index.ts +6 -0
  264. package/plugins/acryl-router/src/middleware/index.ts +25 -0
  265. package/plugins/acryl-router/src/middleware/loopback-only.ts +88 -0
  266. package/plugins/acryl-router/src/service.ts +356 -0
  267. package/plugins/acryl-router/tests/docs-examples.spec.ts +152 -0
  268. package/plugins/acryl-router/tests/live-install.spec.ts +64 -0
  269. package/plugins/acryl-router/tests/loader.spec.ts +72 -0
  270. package/plugins/acryl-router/tests/service.spec.ts +409 -0
  271. package/plugins/acryl-router/tests/shadow-state.spec.ts +79 -0
  272. package/plugins/acryl-router/upstream.json +20 -0
  273. package/plugins/acryl-router-http/LICENSE +21 -0
  274. package/plugins/acryl-router-http/plugin.json +12 -0
  275. package/plugins/acryl-router-http/src/bridge.ts +54 -0
  276. package/plugins/acryl-router-http/src/index.ts +63 -0
  277. package/plugins/acryl-router-http/src/serve.ts +24 -0
  278. package/plugins/acryl-router-http/tests/adapter.spec.ts +174 -0
  279. package/plugins/acryl-router-http/tests/stream.spec.ts +133 -0
  280. package/plugins/acryl-router-http/tests/surfaces.spec.ts +101 -0
  281. package/plugins/acryl-router-http/upstream.json +22 -0
  282. package/plugins/acryl-state/LICENSE +21 -0
  283. package/plugins/acryl-state/NOTICE +30 -0
  284. package/plugins/acryl-state/plugin.json +12 -0
  285. package/plugins/acryl-state/src/cell.ts +67 -0
  286. package/plugins/acryl-state/src/index.ts +3 -0
  287. package/plugins/acryl-state/src/react.ts +11 -0
  288. package/plugins/acryl-state/src/service.ts +119 -0
  289. package/plugins/acryl-state/tests/cell.spec.ts +71 -0
  290. package/plugins/acryl-state/tests/react.spec.ts +39 -0
  291. package/plugins/acryl-state/tests/service.spec.ts +132 -0
  292. package/plugins/acryl-state/upstream.json +22 -0
  293. package/plugins/builder/script.ts +6 -10
  294. package/plugins/extensions/index.ts +20 -3
  295. package/plugins/extensions/resolve.ts +28 -3
  296. package/plugins/openai/prompt.ts +12 -6
  297. package/plugins/tasks/index.ts +14 -0
  298. package/plugins/tsconfig.json +14 -5
  299. package/templates/agent/blend.yaml +5 -7
  300. package/templates/agent/src/app.ts +8 -8
  301. package/templates/agent/src/routes.ts +39 -38
  302. package/templates/agent/template.json +3 -3
  303. package/templates/agent/test/builder.test.ts +22 -14
  304. package/templates/agent/test/model.test.ts +3 -3
  305. package/templates/agent/tsconfig.json +8 -5
  306. package/templates/{app → blank}/blend.yaml +3 -3
  307. package/templates/{app → blank}/src/app.ts +5 -5
  308. package/templates/blank/src/routes.ts +16 -0
  309. package/templates/blank/template.json +1 -0
  310. package/templates/{app → blank}/test/app.test.ts +1 -1
  311. package/templates/{app → blank}/tsconfig.json +2 -4
  312. package/templates/todo/README.md +27 -0
  313. package/templates/todo/blend.yaml +25 -0
  314. package/templates/todo/gitignore +2 -0
  315. package/templates/todo/package.json +23 -0
  316. package/templates/todo/src/app.ts +71 -0
  317. package/templates/todo/src/main.ts +19 -0
  318. package/templates/todo/src/page.ts +28 -0
  319. package/templates/todo/src/todo.ts +123 -0
  320. package/templates/todo/template.json +1 -0
  321. package/templates/todo/test/todo.test.ts +145 -0
  322. package/templates/todo/tsconfig.json +17 -0
  323. package/templates/todo/upstream.json +12 -0
  324. package/plugins/events/index.ts +0 -42
  325. package/plugins/events/plugin.json +0 -9
  326. package/plugins/http/index.ts +0 -64
  327. package/plugins/http/plugin.json +0 -10
  328. package/plugins/server/index.ts +0 -138
  329. package/plugins/server/plugin.json +0 -10
  330. package/templates/app/src/routes.ts +0 -14
  331. package/templates/app/template.json +0 -1
  332. /package/templates/{app → blank}/README.md +0 -0
  333. /package/templates/{app → blank}/gitignore +0 -0
  334. /package/templates/{app → blank}/package.json +0 -0
  335. /package/templates/{app → blank}/src/main.ts +0 -0
@@ -0,0 +1,119 @@
1
+ /**
2
+ * The ACRYL state service (`ctx.state`): observable cells owned by plugins. A cell created from a plugin's context is removed
3
+ * when that plugin unloads, so a surface never keeps a value alive for a plugin that is gone. In process the cells are read
4
+ * and subscribed to directly (the terminal UI); with a router present they are also readable as JSON and streamable as
5
+ * server-sent events (Web and Desktop), read-only: only the Host plugin that owns a cell writes it.
6
+ */
7
+
8
+ import { Context, Service } from '@deepseek-ai/cordis'
9
+ import { App, type Context as RequestContext } from '../../acryl-router/src/index.ts'
10
+ import { streamSSE } from '../../acryl-router/src/helpers/index.ts'
11
+ import { loopbackOnly, methodNotAllowed } from '../../acryl-router/src/middleware/index.ts'
12
+ import { createCell, createDerived, type Cell, type ReadonlyCell } from './cell.ts'
13
+
14
+ declare module '@deepseek-ai/cordis' {
15
+ interface Context {
16
+ state: State
17
+ }
18
+ }
19
+
20
+ const KEEP_ALIVE_MS = 15_000
21
+
22
+ /** Stable Cordis plugin name, equal to the package name and the Loader row id. */
23
+ export const name = 'acryl-state'
24
+
25
+ const KEY_PATTERN = /^[a-z][a-z0-9-]*(\.[a-z][a-z0-9-]*)*$/
26
+
27
+ export class State extends Service {
28
+ private readonly cells = new Map<string, ReadonlyCell<unknown>>()
29
+
30
+ constructor(ctx: Context) {
31
+ super(ctx, 'state')
32
+ }
33
+
34
+ private claim(key: string): void {
35
+ if (!KEY_PATTERN.test(key)) throw new TypeError(`acryl-state: key "${key}" must be dotted lowercase words, like "market.sources"`)
36
+ if (this.cells.has(key)) throw new Error(`acryl-state: key "${key}" is already in use`)
37
+ }
38
+
39
+ private report = (error: unknown): void => {
40
+ this.ctx.logger.error(`acryl-state: a subscriber failed: ${error instanceof Error ? error.message : String(error)}`)
41
+ }
42
+
43
+ /** Create a writable cell owned by the calling plugin. The key must be new. */
44
+ cell<T>(key: string, initial: T): Cell<T> {
45
+ this.claim(key)
46
+ const cell = createCell(key, initial, this.report)
47
+ this.cells.set(key, cell as ReadonlyCell<unknown>)
48
+ this.ctx.effect(() => () => { this.cells.delete(key) }, `acryl-state: cell ${key}`)
49
+ return cell
50
+ }
51
+
52
+ /** Create a read-only cell computed from other cells, owned by the calling plugin. */
53
+ derive<D extends readonly ReadonlyCell<unknown>[], T>(
54
+ key: string,
55
+ inputs: D,
56
+ compute: (...values: { [K in keyof D]: D[K] extends ReadonlyCell<infer V> ? V : never }) => T,
57
+ ): ReadonlyCell<T> {
58
+ this.claim(key)
59
+ const derived = createDerived(key, inputs, compute, this.report)
60
+ this.cells.set(key, derived.cell as ReadonlyCell<unknown>)
61
+ this.ctx.effect(() => () => { derived.dispose(); this.cells.delete(key) }, `acryl-state: derived ${key}`)
62
+ return derived.cell
63
+ }
64
+
65
+ /** A cell by key, if one is live. */
66
+ find<T = unknown>(key: string): ReadonlyCell<T> | undefined {
67
+ return this.cells.get(key) as ReadonlyCell<T> | undefined
68
+ }
69
+
70
+ keys(): string[] {
71
+ return [...this.cells.keys()].sort()
72
+ }
73
+
74
+ /**
75
+ * The current value of `key` as a server-sent event stream: the value now, then one event per change, until the client cancels or the router
76
+ * is disposed (both abort the stream). A comment line goes out every 15 seconds so proxies and browsers keep an idle stream open.
77
+ */
78
+ stream(key: string, c: RequestContext): Response | undefined {
79
+ const cell = this.find(key)
80
+ if (cell === undefined) return undefined
81
+ return streamSSE(c, async stream => {
82
+ const ended = new Promise<void>(resolve => { stream.onAbort(resolve) })
83
+ // Writes are asynchronous; chaining them keeps events in the order the cell changed.
84
+ let queue: Promise<unknown> = Promise.resolve()
85
+ const send = (value: unknown): void => {
86
+ queue = queue.then(() => stream.writeSSE({ event: 'value', data: typeof value === 'string' ? value : JSON.stringify(value) })).catch(() => {})
87
+ }
88
+ send(cell.get())
89
+ const unsubscribe = cell.subscribe(send)
90
+ const keepAlive = setInterval(() => { queue = queue.then(() => stream.write(': keepalive\n\n')).catch(() => {}) }, KEEP_ALIVE_MS)
91
+ keepAlive.unref()
92
+ try {
93
+ await ended
94
+ } finally {
95
+ clearInterval(keepAlive)
96
+ unsubscribe()
97
+ }
98
+ })
99
+ }
100
+ }
101
+
102
+ export function apply(ctx: Context): void {
103
+ const state = new State(ctx)
104
+ // The same cells on the router, read-only, for any surface; present only where a router is. A request over a socket must come from this machine to this
105
+ // app; the terminal calls the router in process and is the host's own code.
106
+ ctx.inject(['router'], inner => {
107
+ const api = new App()
108
+ api.use(loopbackOnly({ inProcess: 'allow' }), methodNotAllowed({ app: api }))
109
+ .get('/', c => c.json({ keys: state.keys() }))
110
+ .get('/:key', c => {
111
+ const cell = state.find(c.req.param('key'))
112
+ return cell === undefined ? c.json({ error: 'no such state' }, 404) : c.json({ key: cell.key, value: cell.get() })
113
+ })
114
+ .get('/:key/events', c => state.stream(c.req.param('key'), c) ?? c.json({ error: 'no such state' }, 404))
115
+ inner.router.route('/api/state', api)
116
+ // Over HTTP only where an adapter exists (Web and Desktop); the terminal needs none.
117
+ inner.inject(['routerHttp'], http => { http.routerHttp.mount('/api/state') })
118
+ })
119
+ }
@@ -0,0 +1,71 @@
1
+ import { describe, expect, it, vi } from 'vitest'
2
+ import { createCell, createDerived } from '../src/cell.ts'
3
+
4
+ describe('cells', () => {
5
+ it('holds a value, notifies on change with the previous value, and ignores a set to an equal value', () => {
6
+ const cell = createCell('a.b', 1)
7
+ const seen: Array<[number, number]> = []
8
+ cell.subscribe((value, previous) => seen.push([value, previous]))
9
+ cell.set(2)
10
+ cell.set(2)
11
+ cell.update(n => n + 1)
12
+ expect(cell.get()).toBe(3)
13
+ expect(seen).toEqual([[2, 1], [3, 2]])
14
+ })
15
+
16
+ it('stops notifying after unsubscribe, and unsubscribing twice is harmless', () => {
17
+ const cell = createCell('a', 0)
18
+ const listener = vi.fn()
19
+ const stop = cell.subscribe(listener)
20
+ cell.set(1)
21
+ stop()
22
+ stop()
23
+ cell.set(2)
24
+ expect(listener).toHaveBeenCalledTimes(1)
25
+ })
26
+
27
+ it('isolates a failing subscriber: the others still run and the error is reported', () => {
28
+ const report = vi.fn()
29
+ const cell = createCell('a', 0, report)
30
+ const after = vi.fn()
31
+ cell.subscribe(() => { throw new Error('boom') })
32
+ cell.subscribe(after)
33
+ cell.set(1)
34
+ expect(after).toHaveBeenCalledWith(1, 0)
35
+ expect(report).toHaveBeenCalledTimes(1)
36
+ })
37
+
38
+ it('lets a subscriber unsubscribe itself while being notified', () => {
39
+ const cell = createCell('a', 0)
40
+ const calls: number[] = []
41
+ const stop = cell.subscribe(value => { calls.push(value); stop() })
42
+ cell.subscribe(value => calls.push(value * 10))
43
+ cell.set(1)
44
+ cell.set(2)
45
+ expect(calls).toEqual([1, 10, 20])
46
+ })
47
+ })
48
+
49
+ describe('derived cells', () => {
50
+ it('recomputes from its inputs, notifies only when its own value changes, and detaches on dispose', () => {
51
+ const a = createCell('a', 2)
52
+ const b = createCell('b', 3)
53
+ const { cell: sum, dispose } = createDerived('sum', [a, b] as const, (x, y) => x + y)
54
+ const seen: number[] = []
55
+ sum.subscribe(value => seen.push(value))
56
+ expect(sum.get()).toBe(5)
57
+ a.set(10)
58
+ b.set(1)
59
+ expect(seen).toEqual([13, 11])
60
+ const parity = createDerived('parity', [a] as const, x => x % 2 === 0).cell
61
+ const flips: boolean[] = []
62
+ parity.subscribe(value => flips.push(value))
63
+ a.set(12)
64
+ a.set(14)
65
+ expect(flips).toEqual([])
66
+ dispose()
67
+ a.set(100)
68
+ // Detached: it keeps the last value it computed (14 + 1), not 100 + 1.
69
+ expect(sum.get()).toBe(15)
70
+ })
71
+ })
@@ -0,0 +1,39 @@
1
+ // @vitest-environment jsdom
2
+ import { act, cleanup, render } from '@testing-library/react'
3
+ import { createElement } from 'react'
4
+ import { afterEach, describe, expect, it } from 'vitest'
5
+ import { createCell } from '../src/cell.ts'
6
+ import { useCell } from '../src/react.ts'
7
+
8
+ ;(globalThis as { IS_REACT_ACT_ENVIRONMENT?: boolean }).IS_REACT_ACT_ENVIRONMENT = true
9
+ afterEach(cleanup)
10
+
11
+ function Counter({ cell }: { cell: ReturnType<typeof createCell<number>> }) {
12
+ return createElement('span', { 'data-testid': 'v' }, String(useCell(cell)))
13
+ }
14
+
15
+ describe('useCell', () => {
16
+ it('renders the current value and re-renders when the cell changes', () => {
17
+ const cell = createCell('r.count', 1)
18
+ const view = render(createElement(Counter, { cell }))
19
+ expect(view.getByTestId('v').textContent).toBe('1')
20
+ act(() => { cell.set(2) })
21
+ expect(view.getByTestId('v').textContent).toBe('2')
22
+ })
23
+
24
+ it('unsubscribes on unmount, and a second component on the same cell stays in step', () => {
25
+ const cell = createCell('r.count', 0)
26
+ let live = 0
27
+ const watched = { ...cell, subscribe: (listener: Parameters<typeof cell.subscribe>[0]) => { live += 1; const stop = cell.subscribe(listener); return () => { live -= 1; stop() } } }
28
+ const a = render(createElement(Counter, { cell: watched }))
29
+ const b = render(createElement(Counter, { cell: watched }))
30
+ expect(live).toBe(2)
31
+ act(() => { cell.set(7) })
32
+ expect(a.container.querySelector('[data-testid=v]')?.textContent).toBe('7')
33
+ expect(b.container.querySelector('[data-testid=v]')?.textContent).toBe('7')
34
+ a.unmount()
35
+ expect(live).toBe(1)
36
+ b.unmount()
37
+ expect(live).toBe(0)
38
+ })
39
+ })
@@ -0,0 +1,132 @@
1
+ /**
2
+ * The state service through a real Cordis `Context`: cells are owned by the plugin that made them, readable and
3
+ * subscribable in process, and streamed through the router as server-sent events, with cancellation and disposal.
4
+ */
5
+
6
+ import { Context } from '@deepseek-ai/cordis'
7
+ import { apply as routerApply, name as routerName } from '../../acryl-router/src/index.ts'
8
+ import { afterEach, describe, expect, it } from 'vitest'
9
+ import { apply, name } from '../src/index.ts'
10
+
11
+ const contexts: Context[] = []
12
+ const settle = (ms = 0): Promise<void> => new Promise(resolve => setTimeout(resolve, ms))
13
+ afterEach(async () => {
14
+ while (contexts.length > 0) await contexts.pop()?.fiber.dispose()
15
+ })
16
+
17
+ async function system(withRouter = true): Promise<Context> {
18
+ const ctx = new Context()
19
+ contexts.push(ctx)
20
+ if (withRouter) ctx.plugin({ name: routerName, apply: routerApply }, { trailingSlash: 'strict' })
21
+ ctx.plugin({ name, apply })
22
+ await settle()
23
+ return ctx
24
+ }
25
+
26
+ /** A tiny SSE reader over a Response body. */
27
+ async function events(response: Response, count: number): Promise<string[]> {
28
+ const reader = response.body!.getReader()
29
+ const decoder = new TextDecoder()
30
+ let buffer = ''
31
+ const out: string[] = []
32
+ while (out.length < count) {
33
+ const { done, value } = await reader.read()
34
+ if (done) break
35
+ buffer += decoder.decode(value, { stream: true })
36
+ for (let at = buffer.indexOf('\n\n'); at !== -1; at = buffer.indexOf('\n\n')) {
37
+ out.push(buffer.slice(0, at))
38
+ buffer = buffer.slice(at + 2)
39
+ }
40
+ }
41
+ reader.releaseLock()
42
+ return out
43
+ }
44
+
45
+ describe('acryl-state', () => {
46
+ it('creates cells owned by the calling plugin and removes them when it unloads', async () => {
47
+ const ctx = await system()
48
+ const fiber = ctx.plugin({ name: 'owner', inject: ['state'], apply(c: Context) { c.state.cell('owner.count', 1) } })
49
+ await settle()
50
+ expect(ctx.state.keys()).toEqual(['owner.count'])
51
+ expect(ctx.state.find<number>('owner.count')?.get()).toBe(1)
52
+ await fiber.dispose()
53
+ await settle()
54
+ expect(ctx.state.keys()).toEqual([])
55
+ expect(ctx.state.find('owner.count')).toBeUndefined()
56
+ })
57
+
58
+ it('rejects a bad or duplicate key', async () => {
59
+ const ctx = await system()
60
+ ctx.state.cell('ok.key', 0)
61
+ expect(() => ctx.state.cell('ok.key', 1)).toThrow(/already in use/)
62
+ expect(() => ctx.state.cell('Bad Key', 1)).toThrow(/dotted lowercase/)
63
+ expect(() => ctx.state.derive('ok.key', [], () => 1)).toThrow(/already in use/)
64
+ })
65
+
66
+ it('derives a cell from others and detaches it when its owner unloads', async () => {
67
+ const ctx = await system()
68
+ const a = ctx.state.cell<number>('n.a', 1)
69
+ const fiber = ctx.plugin({ name: 'deriver', inject: ['state'], apply(c: Context) { c.state.derive('n.double', [a] as const, x => x * 2) } })
70
+ await settle()
71
+ expect(ctx.state.find<number>('n.double')?.get()).toBe(2)
72
+ a.set(5)
73
+ expect(ctx.state.find<number>('n.double')?.get()).toBe(10)
74
+ await fiber.dispose()
75
+ await settle()
76
+ expect(ctx.state.find('n.double')).toBeUndefined()
77
+ })
78
+
79
+ it('serves keys and values as JSON on the router, read-only', async () => {
80
+ const ctx = await system()
81
+ ctx.state.cell('app.title', 'hello')
82
+ const keys = await ctx.router.fetch(new Request('http://acryl.test/api/state'))
83
+ expect(await keys.json()).toEqual({ keys: ['app.title'] })
84
+ const one = await ctx.router.fetch(new Request('http://acryl.test/api/state/app.title'))
85
+ expect(await one.json()).toEqual({ key: 'app.title', value: 'hello' })
86
+ expect((await ctx.router.fetch(new Request('http://acryl.test/api/state/none'))).status).toBe(404)
87
+ expect((await ctx.router.fetch(new Request('http://acryl.test/api/state/app.title', { method: 'PUT', body: '"x"' }))).status).toBe(405)
88
+ expect(ctx.state.find('app.title')?.get()).toBe('hello')
89
+ })
90
+
91
+ it('streams the value now and every change as server-sent events, and ends when the caller aborts', async () => {
92
+ const ctx = await system()
93
+ const cell = ctx.state.cell<number>('app.count', 0)
94
+ const controller = new AbortController()
95
+ const response = await ctx.router.fetch(new Request('http://acryl.test/api/state/app.count/events', { signal: controller.signal }))
96
+ expect(response.headers.get('content-type')).toContain('text/event-stream')
97
+ const reading = events(response, 3)
98
+ await settle(5)
99
+ cell.set(1)
100
+ cell.set(2)
101
+ expect(await reading).toEqual(['event: value\ndata: 0', 'event: value\ndata: 1', 'event: value\ndata: 2'])
102
+ controller.abort()
103
+ await settle(5)
104
+ // After the abort the stream no longer holds a subscription on the cell.
105
+ let notified = 0
106
+ const probe = ctx.state.find<number>('app.count')!.subscribe(() => { notified += 1 })
107
+ cell.set(3)
108
+ probe()
109
+ expect(notified).toBe(1)
110
+ })
111
+
112
+ it('closes every open stream when the router is disposed', async () => {
113
+ const ctx = await system()
114
+ ctx.state.cell<number>('app.count', 0)
115
+ const routerFiber = [...[...ctx.registry.values()].find(runtime => runtime.name === routerName)!.fibers][0]!
116
+ const response = await ctx.router.fetch(new Request('http://acryl.test/api/state/app.count/events'))
117
+ const reader = response.body!.getReader()
118
+ await reader.read()
119
+ const ended = reader.read()
120
+ await routerFiber.dispose()
121
+ await expect(ended).resolves.toMatchObject({ done: true })
122
+ })
123
+
124
+ it('works with no router: cells and subscriptions only, no routes', async () => {
125
+ const ctx = await system(false)
126
+ const cell = ctx.state.cell<string>('plain.value', 'a')
127
+ const seen: string[] = []
128
+ cell.subscribe(value => seen.push(value))
129
+ cell.set('b')
130
+ expect(seen).toEqual(['b'])
131
+ })
132
+ })
@@ -0,0 +1,22 @@
1
+ {
2
+ "source": {
3
+ "repo": "acryldev/acryl",
4
+ "path": "plugins/acryl-state",
5
+ "commit": "df79b188398b6d4f8cceda35a72b88300a10a9ce"
6
+ },
7
+ "vendored": [
8
+ "src",
9
+ "tests",
10
+ "LICENSE",
11
+ "NOTICE"
12
+ ],
13
+ "rewrittenSpecifiers": 4,
14
+ "digest": "sha256:8feb067a67ea2628ff31a9fd2201361be4be5a8c217be3b7169277c0ce472b69",
15
+ "testsNotRun": {
16
+ "tests/react.spec.ts": "needs react and @testing-library/react, which generated apps do not install"
17
+ },
18
+ "notice": [
19
+ "LICENSE",
20
+ "NOTICE"
21
+ ]
22
+ }
@@ -18,24 +18,20 @@ const PATH = /^\/[a-z0-9/-]*$/
18
18
 
19
19
  export function routeSource(name: string, path: string, text: string): string {
20
20
  return `import type { Context } from '@deepseek-ai/cordis'
21
+ import { App } from 'acryl-router'
21
22
 
22
23
  /** Written by the builder: answers GET ${path}, and checks that it does before the install counts. */
23
24
  export const name = '${name}'
24
- export const inject = ['http']
25
-
26
- interface Http {
27
- route(method: string, path: string, handler: () => { body: unknown }): () => void
28
- handle(request: { method: string; path: string; body: unknown }): Promise<{ status: number; body: unknown }>
29
- }
25
+ export const inject = ['router']
30
26
 
31
27
  export function apply(ctx: Context) {
32
- const http = ctx.get('http') as Http
33
- ctx.effect(() => http.route('GET', ${JSON.stringify(path)}, () => ({ body: { text: ${JSON.stringify(text)} } })))
28
+ // The app is mounted through this plugin's context, so the route is removed when the plugin is unloaded.
29
+ ctx.router.route('/', new App().get(${JSON.stringify(path)}, (c) => c.json({ text: ${JSON.stringify(text)} })))
34
30
  }
35
31
 
36
32
  export async function check(ctx: Context) {
37
- const reply = await (ctx.get('http') as Http).handle({ method: 'GET', path: ${JSON.stringify(path)}, body: undefined })
38
- if (reply.status !== 200) throw new Error(\`GET ${path} answered \${reply.status}\`)
33
+ const reply = await ctx.get('router')!.request(${JSON.stringify(path)})
34
+ if (!reply.ok) throw new Error(\`GET ${path} answered \${reply.status}\`)
39
35
  }
40
36
  `
41
37
  }
@@ -32,7 +32,7 @@ import { type Context, type Fiber, Service } from '@deepseek-ai/cordis'
32
32
  import type {} from '../instance/index.ts'
33
33
  import type { Sandbox } from '../sandbox/index.ts'
34
34
  import { appendLedger, type LedgerEntry, readLedger } from './ledger.ts'
35
- import { resolveFromApp } from './resolve.ts'
35
+ import { resolveFromApp, routerAliases } from './resolve.ts'
36
36
  import { hashFolder, prune, stage } from './stage.ts'
37
37
 
38
38
  /** A fiber's lifecycle state, by name (Cordis's own enum is a `const enum`, which does not exist at run time). */
@@ -181,6 +181,11 @@ function injected(module: PluginModule): string[] {
181
181
  }
182
182
 
183
183
  /** The service. One instance serves one app home. */
184
+ /** The slice of the state service this plugin uses, by name, so it imports nothing from acryl-state. */
185
+ interface StateService {
186
+ cell<T>(key: string, initial: T): { set(value: T): void }
187
+ }
188
+
184
189
  export class Extensions extends Service {
185
190
  readonly sources: readonly ExtensionSource[]
186
191
  readonly stageRoot: string
@@ -195,9 +200,15 @@ export class Extensions extends Service {
195
200
  private readonly requests = new Map<string, { dir: string; version: string }>()
196
201
  private tail: Promise<unknown> = Promise.resolve()
197
202
  private disposed = false
203
+ /**
204
+ * The context the service was constructed with. Inside a service method `this.ctx` is Cordis's shadow context, which resolves services as the
205
+ * service's own fiber, so a plugin mounted from it would read `ctx.router` from the installer's fiber and fail. Extensions mount from this one.
206
+ */
207
+ private readonly owner: Context
198
208
 
199
209
  constructor(ctx: Context, config: ExtensionsConfig = {}) {
200
210
  super(ctx, 'extensions')
211
+ this.owner = ctx
201
212
  const { home, root } = ctx.appInstance
202
213
  this.sources = (config.sources ?? [{ dir: join(home, 'extensions'), trust: 'install' }]).map(
203
214
  (source) => ({
@@ -210,7 +221,7 @@ export class Extensions extends Service {
210
221
  this.ledgerFile = join(home, 'extensions-ledger.jsonl')
211
222
  // Effects run their disposers newest first: settle pending operations, then dispose every mounted extension, then drop the hook.
212
223
  ctx.effect(
213
- () => resolveFromApp(this.stageRoot, root),
224
+ () => resolveFromApp(this.stageRoot, root, routerAliases()),
214
225
  'extensions: resolve staged imports from the app',
215
226
  )
216
227
  ctx.effect(
@@ -228,6 +239,12 @@ export class Extensions extends Service {
228
239
  },
229
240
  'extensions: settle pending operations',
230
241
  )
242
+ // With the state service mounted, what is installed is also a cell: any surface reads it as JSON or streams its changes (`/api/state/extensions`).
243
+ ctx.inject(['state'], (inner) => {
244
+ const state = (inner as unknown as { state: StateService }).state
245
+ const cell = state.cell('extensions', this.list())
246
+ inner.on('extensions/changed', () => cell.set(this.list()))
247
+ })
231
248
  this.ready = this.serial(() => this.startup())
232
249
  }
233
250
 
@@ -512,7 +529,7 @@ export class Extensions extends Service {
512
529
  /** Mount a module as a child fiber and wait until it settles. */
513
530
  private async mount(module: PluginModule): Promise<{ fiber: Fiber; error?: unknown }> {
514
531
  // An extension is a plugin module whose config, if any, comes from its own defaults.
515
- const fiber = this.ctx.plugin(module as unknown as Parameters<Context['plugin']>[0])
532
+ const fiber = this.owner.plugin(module as unknown as Parameters<Context['plugin']>[0])
516
533
  try {
517
534
  await fiber.await()
518
535
  return { fiber }
@@ -3,20 +3,45 @@
3
3
  * `@deepseek-ai/cordis` would not find the app's `node_modules`. This resolve hook leaves resolution alone, and only when it fails for a
4
4
  * module inside the staging folder, resolves the same name from the app's project folder. Uses Node's public `module.registerHooks`
5
5
  * (ACRYL needed the same fallback on Deno); the returned function removes the hook.
6
+ *
7
+ * The framework plugins are not packages in an app (they are source under `src/plugins/`), so `aliases` names the files a framework specifier such as
8
+ * `acryl-router` stands for. A staged plugin then imports the same module the app itself runs, so `App` is the very class the router mounts.
6
9
  */
7
- import { mkdirSync, realpathSync } from 'node:fs'
10
+ import { existsSync, mkdirSync, realpathSync } from 'node:fs'
8
11
  import { registerHooks } from 'node:module'
9
12
  import { join } from 'node:path'
10
- import { pathToFileURL } from 'node:url'
13
+ import { fileURLToPath, pathToFileURL } from 'node:url'
11
14
 
12
15
  const isBare = (specifier: string) => !/^[./]/.test(specifier) && !specifier.includes(':')
13
16
 
14
- export function resolveFromApp(stageRoot: string, appRoot: string): () => void {
17
+ /** The router's specifiers, as files of the vendored plugin that sits beside this one; empty when the app has no router. */
18
+ export function routerAliases(): Record<string, string> {
19
+ const src = new URL('../acryl-router/src/', import.meta.url)
20
+ const files = {
21
+ 'acryl-router': 'index.ts',
22
+ 'acryl-router/middleware': 'middleware/index.ts',
23
+ 'acryl-router/helpers': 'helpers/index.ts',
24
+ }
25
+ return Object.fromEntries(
26
+ Object.entries(files)
27
+ .map(([specifier, file]) => [specifier, fileURLToPath(new URL(file, src))] as const)
28
+ .filter(([, path]) => existsSync(path)),
29
+ )
30
+ }
31
+
32
+ export function resolveFromApp(
33
+ stageRoot: string,
34
+ appRoot: string,
35
+ aliases: Record<string, string> = {},
36
+ ): () => void {
15
37
  mkdirSync(stageRoot, { recursive: true })
16
38
  const staged = pathToFileURL(`${realpathSync(stageRoot)}/`).href
17
39
  const parentURL = pathToFileURL(join(realpathSync(appRoot), 'package.json')).href
18
40
  const hook = registerHooks({
19
41
  resolve(specifier, context, nextResolve) {
42
+ const alias = aliases[specifier]
43
+ if (alias !== undefined && context.parentURL?.startsWith(staged))
44
+ return nextResolve(pathToFileURL(alias).href, context)
20
45
  try {
21
46
  return nextResolve(specifier, context)
22
47
  } catch (error) {
@@ -10,22 +10,28 @@ To add a capability:
10
10
  A plugin's index.ts is TypeScript that Node runs directly (erasable syntax only: no enums, no parameter properties):
11
11
 
12
12
  import type { Context } from '@deepseek-ai/cordis'
13
+ import { App } from 'acryl-router'
13
14
  export const name = 'hello' // must equal the folder name
14
- export const inject = ['http'] // services it needs; it waits (pending) until they exist
15
+ export const inject = ['router'] // services it needs; it waits (pending) until they exist
15
16
  export function apply(ctx: Context) {
16
- const http = ctx.get('http') as { route(method: string, path: string, handler: (request: { body: unknown }) => { status?: number; body: unknown }): () => void }
17
- ctx.effect(() => http.route('GET', '/hello', () => ({ body: { text: 'Hello' } }))) // in ctx.effect: removed when the plugin is
17
+ ctx.router.route('/', new App().get('/hello', (c) => c.json({ text: 'Hello' }))) // removed when the plugin is unloaded
18
18
  }
19
19
  export async function check(ctx: Context) { // optional: must pass, or the install is rolled back
20
- const http = ctx.get('http') as { handle(r: { method: string; path: string; body: unknown }): Promise<{ status: number }> }
21
- if ((await http.handle({ method: 'GET', path: '/hello', body: undefined })).status !== 200) throw new Error('GET /hello failed')
20
+ const reply = await ctx.get('router')!.request('/hello')
21
+ if (!reply.ok) throw new Error('GET /hello failed')
22
22
  }
23
23
 
24
+ The router: build an App (import { App } from 'acryl-router'), register routes on it with app.get/post/put/patch/delete(path, handler) and mount it with
25
+ ctx.router.route(prefix, app). A path parameter is :id and is read with c.req.param('id'); c.json(body, status) answers JSON, c.text(...) answers text,
26
+ await c.req.json() reads a JSON body, throw new HTTPException(404, { message }) (also from 'acryl-router') answers an error. Routes added to an app
27
+ after route() are not served: build the whole app first. ctx.get('router')!.request('/path', { method: 'POST', body: ... }) calls a route in process (use it in
28
+ check). The mount goes away when the plugin is unloaded.
29
+
24
30
  For a capability that is a pure function of data (parse, convert, compute), write a sandboxed TOOL instead of a plugin. It cannot touch the
25
31
  host: it runs in a separate process that can read only its own folder. Files: index.ts exporting run(input) and tool.json:
26
32
  { "description": "what it does and its input", "examples": [ { "input": {...}, "output": {...} } ] }
27
33
  The examples are run in the sandbox at install and must match exactly, or the install is refused. Use write_plugin and install_plugin for tools too.
28
34
 
29
- Rules: import only relative files, node: built-ins and @deepseek-ai/cordis. Provide services with ctx.provide(name, value) and
35
+ Rules: import only relative files, node: built-ins, @deepseek-ai/cordis and acryl-router. Provide services with ctx.provide(name, value) and
30
36
  use others with ctx.get(name). Put timers, listeners and routes inside ctx.effect(() => cleanup). Use list_plugins and
31
37
  read_plugin to see what exists. When you are done, answer in one or two plain sentences saying what you built.`
@@ -69,6 +69,14 @@ declare module '@deepseek-ai/cordis' {
69
69
  type Change = Partial<TaskRecord> & { readonly id: string }
70
70
  const changeOf = (change: TaskChange) => change as unknown as Change
71
71
 
72
+ /** The slice of the state service the tasks plugin uses, by name, so it imports nothing from acryl-state. */
73
+ interface StateService {
74
+ cell<T>(key: string, initial: T): { set(value: T): void }
75
+ }
76
+
77
+ /** How many of the newest tasks the state cell holds. */
78
+ const STATE_LIMIT = 100
79
+
72
80
  const FINAL: ReadonlySet<TaskStatus> = new Set(['completed', 'failed', 'cancelled', 'interrupted'])
73
81
  const message = (error: unknown) => (error instanceof Error ? error.message : String(error))
74
82
 
@@ -111,6 +119,12 @@ export class Tasks extends Service {
111
119
  },
112
120
  'tasks: cancel running work',
113
121
  )
122
+ // With the state service mounted, the latest tasks are also a cell: any surface reads them as JSON or streams their changes (`/api/state/tasks`).
123
+ ctx.inject(['state'], (inner) => {
124
+ const state = (inner as unknown as { state: StateService }).state
125
+ const cell = state.cell('tasks', this.list().slice(0, STATE_LIMIT))
126
+ inner.on('tasks/changed', () => cell.set(this.list().slice(0, STATE_LIMIT)))
127
+ })
114
128
  }
115
129
 
116
130
  /** Start a task. The record is written before this returns. */
@@ -1,12 +1,10 @@
1
1
  {
2
2
  "compilerOptions": {
3
- "target": "ES2023",
4
- "lib": ["ES2023"],
3
+ "target": "ES2024",
4
+ "lib": ["ES2024", "DOM", "DOM.Iterable"],
5
5
  "module": "NodeNext",
6
6
  "moduleResolution": "NodeNext",
7
7
  "strict": true,
8
- "noUncheckedIndexedAccess": true,
9
- "exactOptionalPropertyTypes": true,
10
8
  "noImplicitOverride": true,
11
9
  "verbatimModuleSyntax": true,
12
10
  "erasableSyntaxOnly": true,
@@ -15,5 +13,16 @@
15
13
  "skipLibCheck": true,
16
14
  "types": ["node"]
17
15
  },
18
- "include": ["*/**/*.ts"]
16
+ "include": ["*/**/*.ts"],
17
+ "exclude": [
18
+ "acryl-*/src/**/*.test.ts",
19
+ "acryl-*/src/**/*.case.ts",
20
+ "acryl-node-server/tests/adapter",
21
+ "acryl-router/tests/loader.spec.ts",
22
+ "acryl-state/src/react.ts",
23
+ "acryl-state/tests/react.spec.ts",
24
+ "acryl-router-http/tests/adapter.spec.ts",
25
+ "acryl-router-http/tests/stream.spec.ts",
26
+ "acryl-router-http/tests/surfaces.spec.ts"
27
+ ]
19
28
  }
@@ -34,12 +34,10 @@ spec:
34
34
  name: agent
35
35
  config:
36
36
  steps: "{{parameters.steps}}"
37
- - id: http
38
- name: http
39
- - id: events
40
- name: events
41
- config:
42
- topics: [tasks/changed, extensions/changed]
37
+ - id: router
38
+ name: acryl-router
39
+ - id: state
40
+ name: acryl-state
43
41
  - id: sandbox
44
42
  name: sandbox
45
43
  - id: extensions
@@ -53,6 +51,6 @@ spec:
53
51
  - id: routes
54
52
  name: routes
55
53
  - id: server
56
- name: server
54
+ name: acryl-node-server
57
55
  config:
58
56
  hostname: "{{parameters.hostname}}"