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.
- package/dist/index.d.ts +22 -16
- package/dist/index.js +65 -20
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/plugins/acryl-inspect/LICENSE +21 -0
- package/plugins/acryl-inspect/plugin.json +12 -0
- package/plugins/acryl-inspect/src/index.ts +3 -0
- package/plugins/acryl-inspect/src/report.ts +126 -0
- package/plugins/acryl-inspect/src/service.ts +68 -0
- package/plugins/acryl-inspect/tests/report.spec.ts +63 -0
- package/plugins/acryl-inspect/tests/service.spec.ts +121 -0
- package/plugins/acryl-inspect/upstream.json +18 -0
- package/plugins/acryl-node-server/LICENSE +47 -0
- package/plugins/acryl-node-server/plugin.json +13 -0
- package/plugins/acryl-node-server/src/adapter/conninfo.ts +23 -0
- package/plugins/acryl-node-server/src/adapter/early-hints.ts +55 -0
- package/plugins/acryl-node-server/src/adapter/error.ts +11 -0
- package/plugins/acryl-node-server/src/adapter/headers.ts +249 -0
- package/plugins/acryl-node-server/src/adapter/index.ts +6 -0
- package/plugins/acryl-node-server/src/adapter/listener.ts +540 -0
- package/plugins/acryl-node-server/src/adapter/request.ts +797 -0
- package/plugins/acryl-node-server/src/adapter/response.ts +186 -0
- package/plugins/acryl-node-server/src/adapter/serve-static.ts +238 -0
- package/plugins/acryl-node-server/src/adapter/server.ts +36 -0
- package/plugins/acryl-node-server/src/adapter/types.ts +82 -0
- package/plugins/acryl-node-server/src/adapter/url.ts +42 -0
- package/plugins/acryl-node-server/src/adapter/utils/response/constants.ts +1 -0
- package/plugins/acryl-node-server/src/adapter/utils/response.ts +4 -0
- package/plugins/acryl-node-server/src/adapter/utils/stream.ts +88 -0
- package/plugins/acryl-node-server/src/adapter/utils.ts +96 -0
- package/plugins/acryl-node-server/src/adapter/websocket-types.ts +45 -0
- package/plugins/acryl-node-server/src/adapter/websocket.ts +375 -0
- package/plugins/acryl-node-server/src/index.ts +107 -0
- package/plugins/acryl-node-server/tests/adapter/app.ts +76 -0
- package/plugins/acryl-node-server/tests/adapter/assets/.static/plain.txt +1 -0
- package/plugins/acryl-node-server/tests/adapter/assets/favicon.ico +0 -0
- package/plugins/acryl-node-server/tests/adapter/assets/secret.txt +1 -0
- package/plugins/acryl-node-server/tests/adapter/assets/static/acryl.html +1 -0
- package/plugins/acryl-node-server/tests/adapter/assets/static/admin/secret.txt +1 -0
- package/plugins/acryl-node-server/tests/adapter/assets/static/data.json +5 -0
- package/plugins/acryl-node-server/tests/adapter/assets/static/extensionless +1 -0
- package/plugins/acryl-node-server/tests/adapter/assets/static/foo..bar.txt +0 -0
- package/plugins/acryl-node-server/tests/adapter/assets/static/index.html +1 -0
- package/plugins/acryl-node-server/tests/adapter/assets/static/plain.txt +1 -0
- package/plugins/acryl-node-server/tests/adapter/assets/static-with-context-path/plain.txt +1 -0
- package/plugins/acryl-node-server/tests/adapter/assets/static-with-precompressed/hello.bin +1 -0
- package/plugins/acryl-node-server/tests/adapter/assets/static-with-precompressed/hello.bin.br +1 -0
- package/plugins/acryl-node-server/tests/adapter/assets/static-with-precompressed/hello.txt +1 -0
- package/plugins/acryl-node-server/tests/adapter/assets/static-with-precompressed/hello.txt.br +1 -0
- package/plugins/acryl-node-server/tests/adapter/assets/static-with-precompressed/hello.txt.zst +1 -0
- package/plugins/acryl-node-server/tests/adapter/conninfo.test.ts +34 -0
- package/plugins/acryl-node-server/tests/adapter/content-length.test.ts +248 -0
- package/plugins/acryl-node-server/tests/adapter/early-hints.test.ts +457 -0
- package/plugins/acryl-node-server/tests/adapter/fixtures/keys/agent1-cert.pem +23 -0
- package/plugins/acryl-node-server/tests/adapter/fixtures/keys/agent1-key.pem +27 -0
- package/plugins/acryl-node-server/tests/adapter/headers.test.ts +252 -0
- package/plugins/acryl-node-server/tests/adapter/helpers/request.ts +190 -0
- package/plugins/acryl-node-server/tests/adapter/listener.test.ts +727 -0
- package/plugins/acryl-node-server/tests/adapter/request-client-disconnect.test.ts +189 -0
- package/plugins/acryl-node-server/tests/adapter/request.test.ts +1230 -0
- package/plugins/acryl-node-server/tests/adapter/response.test.ts +285 -0
- package/plugins/acryl-node-server/tests/adapter/serve-static.test.ts +849 -0
- package/plugins/acryl-node-server/tests/adapter/server-socket.test.ts +790 -0
- package/plugins/acryl-node-server/tests/adapter/server.test.ts +1302 -0
- package/plugins/acryl-node-server/tests/adapter/setup.ts +16 -0
- package/plugins/acryl-node-server/tests/adapter/url.test.ts +76 -0
- package/plugins/acryl-node-server/tests/adapter/utils/response.test.ts +23 -0
- package/plugins/acryl-node-server/tests/adapter/utils/stream.test.ts +75 -0
- package/plugins/acryl-node-server/tests/adapter/utils.test.ts +235 -0
- package/plugins/acryl-node-server/tests/adapter/websocket.test.ts +260 -0
- package/plugins/acryl-node-server/tests/server.spec.ts +141 -0
- package/plugins/acryl-node-server/upstream.json +18 -0
- package/plugins/acryl-router/LICENSE +47 -0
- package/plugins/acryl-router/plugin.json +13 -0
- package/plugins/acryl-router/src/client.ts +2 -0
- package/plugins/acryl-router/src/connection.ts +42 -0
- package/plugins/acryl-router/src/core/app-base.ts +544 -0
- package/plugins/acryl-router/src/core/app.test.ts +3833 -0
- package/plugins/acryl-router/src/core/app.ts +34 -0
- package/plugins/acryl-router/src/core/client/client.test.ts +2313 -0
- package/plugins/acryl-router/src/core/client/client.ts +249 -0
- package/plugins/acryl-router/src/core/client/fetch-result-please.ts +95 -0
- package/plugins/acryl-router/src/core/client/index.ts +17 -0
- package/plugins/acryl-router/src/core/client/types.test.ts +149 -0
- package/plugins/acryl-router/src/core/client/types.ts +393 -0
- package/plugins/acryl-router/src/core/client/utils.test.ts +405 -0
- package/plugins/acryl-router/src/core/client/utils.ts +117 -0
- package/plugins/acryl-router/src/core/compose.test.ts +751 -0
- package/plugins/acryl-router/src/core/compose.ts +73 -0
- package/plugins/acryl-router/src/core/context.test.ts +629 -0
- package/plugins/acryl-router/src/core/context.ts +781 -0
- package/plugins/acryl-router/src/core/helper/accepts/accepts.test.ts +344 -0
- package/plugins/acryl-router/src/core/helper/accepts/accepts.ts +96 -0
- package/plugins/acryl-router/src/core/helper/accepts/index.ts +6 -0
- package/plugins/acryl-router/src/core/helper/adapter/index.test.ts +55 -0
- package/plugins/acryl-router/src/core/helper/adapter/index.ts +90 -0
- package/plugins/acryl-router/src/core/helper/conninfo/index.ts +6 -0
- package/plugins/acryl-router/src/core/helper/conninfo/types.ts +45 -0
- package/plugins/acryl-router/src/core/helper/cookie/index.test.ts +510 -0
- package/plugins/acryl-router/src/core/helper/cookie/index.ts +146 -0
- package/plugins/acryl-router/src/core/helper/factory/index.test.ts +434 -0
- package/plugins/acryl-router/src/core/helper/factory/index.ts +375 -0
- package/plugins/acryl-router/src/core/helper/html/index.test.ts +78 -0
- package/plugins/acryl-router/src/core/helper/html/index.ts +57 -0
- package/plugins/acryl-router/src/core/helper/route/index.test.ts +259 -0
- package/plugins/acryl-router/src/core/helper/route/index.ts +144 -0
- package/plugins/acryl-router/src/core/helper/streaming/index.ts +9 -0
- package/plugins/acryl-router/src/core/helper/streaming/sse.ts +101 -0
- package/plugins/acryl-router/src/core/helper/streaming/stream.test.ts +128 -0
- package/plugins/acryl-router/src/core/helper/streaming/stream.ts +45 -0
- package/plugins/acryl-router/src/core/helper/streaming/text.test.ts +34 -0
- package/plugins/acryl-router/src/core/helper/streaming/text.ts +15 -0
- package/plugins/acryl-router/src/core/helper/streaming/utils.ts +11 -0
- package/plugins/acryl-router/src/core/helper/testing/index.test.ts +42 -0
- package/plugins/acryl-router/src/core/helper/testing/index.ts +28 -0
- package/plugins/acryl-router/src/core/helper/websocket/index.test.ts +124 -0
- package/plugins/acryl-router/src/core/helper/websocket/index.ts +140 -0
- package/plugins/acryl-router/src/core/http-exception.test.ts +45 -0
- package/plugins/acryl-router/src/core/http-exception.ts +77 -0
- package/plugins/acryl-router/src/core/index.ts +53 -0
- package/plugins/acryl-router/src/core/middleware/basic-auth/index.test.ts +434 -0
- package/plugins/acryl-router/src/core/middleware/basic-auth/index.ts +152 -0
- package/plugins/acryl-router/src/core/middleware/bearer-auth/index.test.ts +1018 -0
- package/plugins/acryl-router/src/core/middleware/bearer-auth/index.ts +220 -0
- package/plugins/acryl-router/src/core/middleware/body-limit/index.test.ts +273 -0
- package/plugins/acryl-router/src/core/middleware/body-limit/index.ts +110 -0
- package/plugins/acryl-router/src/core/middleware/combine/index.test.ts +355 -0
- package/plugins/acryl-router/src/core/middleware/combine/index.ts +171 -0
- package/plugins/acryl-router/src/core/middleware/compress/index.test.ts +535 -0
- package/plugins/acryl-router/src/core/middleware/compress/index.ts +142 -0
- package/plugins/acryl-router/src/core/middleware/context-storage/index.test.ts +43 -0
- package/plugins/acryl-router/src/core/middleware/context-storage/index.ts +58 -0
- package/plugins/acryl-router/src/core/middleware/cors/index.test.ts +532 -0
- package/plugins/acryl-router/src/core/middleware/cors/index.ts +163 -0
- package/plugins/acryl-router/src/core/middleware/csrf/index.test.ts +534 -0
- package/plugins/acryl-router/src/core/middleware/csrf/index.ts +150 -0
- package/plugins/acryl-router/src/core/middleware/etag/digest.test.ts +21 -0
- package/plugins/acryl-router/src/core/middleware/etag/digest.ts +95 -0
- package/plugins/acryl-router/src/core/middleware/etag/index.test.ts +503 -0
- package/plugins/acryl-router/src/core/middleware/etag/index.ts +135 -0
- package/plugins/acryl-router/src/core/middleware/ip-restriction/index.test.ts +217 -0
- package/plugins/acryl-router/src/core/middleware/ip-restriction/index.ts +278 -0
- package/plugins/acryl-router/src/core/middleware/jwk/index.test.ts +1135 -0
- package/plugins/acryl-router/src/core/middleware/jwk/index.ts +1 -0
- package/plugins/acryl-router/src/core/middleware/jwk/jwk.ts +195 -0
- package/plugins/acryl-router/src/core/middleware/jwk/keys.test.json +48 -0
- package/plugins/acryl-router/src/core/middleware/jwt/index.test.ts +802 -0
- package/plugins/acryl-router/src/core/middleware/jwt/index.ts +9 -0
- package/plugins/acryl-router/src/core/middleware/jwt/jwt.ts +189 -0
- package/plugins/acryl-router/src/core/middleware/language/index.test.ts +552 -0
- package/plugins/acryl-router/src/core/middleware/language/index.ts +12 -0
- package/plugins/acryl-router/src/core/middleware/language/language.ts +314 -0
- package/plugins/acryl-router/src/core/middleware/logger/index.test.ts +204 -0
- package/plugins/acryl-router/src/core/middleware/logger/index.ts +95 -0
- package/plugins/acryl-router/src/core/middleware/method-not-allowed/index.test.ts +292 -0
- package/plugins/acryl-router/src/core/middleware/method-not-allowed/index.ts +141 -0
- package/plugins/acryl-router/src/core/middleware/method-override/index.test.ts +221 -0
- package/plugins/acryl-router/src/core/middleware/method-override/index.ts +147 -0
- package/plugins/acryl-router/src/core/middleware/powered-by/index.test.ts +37 -0
- package/plugins/acryl-router/src/core/middleware/powered-by/index.ts +35 -0
- package/plugins/acryl-router/src/core/middleware/pretty-json/index.test.ts +218 -0
- package/plugins/acryl-router/src/core/middleware/pretty-json/index.ts +67 -0
- package/plugins/acryl-router/src/core/middleware/request-id/index.test.ts +168 -0
- package/plugins/acryl-router/src/core/middleware/request-id/index.ts +7 -0
- package/plugins/acryl-router/src/core/middleware/request-id/request-id.ts +59 -0
- package/plugins/acryl-router/src/core/middleware/secure-headers/index.test.ts +671 -0
- package/plugins/acryl-router/src/core/middleware/secure-headers/index.ts +8 -0
- package/plugins/acryl-router/src/core/middleware/secure-headers/permissions-policy.ts +101 -0
- package/plugins/acryl-router/src/core/middleware/secure-headers/secure-headers.ts +340 -0
- package/plugins/acryl-router/src/core/middleware/serve-static/index.test.ts +450 -0
- package/plugins/acryl-router/src/core/middleware/serve-static/index.ts +135 -0
- package/plugins/acryl-router/src/core/middleware/serve-static/path.test.ts +50 -0
- package/plugins/acryl-router/src/core/middleware/serve-static/path.ts +25 -0
- package/plugins/acryl-router/src/core/middleware/timeout/index.test.ts +70 -0
- package/plugins/acryl-router/src/core/middleware/timeout/index.ts +58 -0
- package/plugins/acryl-router/src/core/middleware/timing/index.test.ts +310 -0
- package/plugins/acryl-router/src/core/middleware/timing/index.ts +8 -0
- package/plugins/acryl-router/src/core/middleware/timing/timing.ts +256 -0
- package/plugins/acryl-router/src/core/middleware/trailing-slash/index.test.ts +381 -0
- package/plugins/acryl-router/src/core/middleware/trailing-slash/index.ts +156 -0
- package/plugins/acryl-router/src/core/request/constants.ts +1 -0
- package/plugins/acryl-router/src/core/request.test.ts +751 -0
- package/plugins/acryl-router/src/core/request.ts +502 -0
- package/plugins/acryl-router/src/core/router/common.case.test.ts +1132 -0
- package/plugins/acryl-router/src/core/router/linear-router/index.ts +6 -0
- package/plugins/acryl-router/src/core/router/linear-router/router.test.ts +80 -0
- package/plugins/acryl-router/src/core/router/linear-router/router.ts +152 -0
- package/plugins/acryl-router/src/core/router/pattern-router/index.ts +6 -0
- package/plugins/acryl-router/src/core/router/pattern-router/router.test.ts +46 -0
- package/plugins/acryl-router/src/core/router/pattern-router/router.ts +54 -0
- package/plugins/acryl-router/src/core/router/reg-exp-router/index.ts +7 -0
- package/plugins/acryl-router/src/core/router/reg-exp-router/matcher.ts +33 -0
- package/plugins/acryl-router/src/core/router/reg-exp-router/node.ts +170 -0
- package/plugins/acryl-router/src/core/router/reg-exp-router/prepared-router.test.ts +154 -0
- package/plugins/acryl-router/src/core/router/reg-exp-router/prepared-router.ts +165 -0
- package/plugins/acryl-router/src/core/router/reg-exp-router/router.test.ts +265 -0
- package/plugins/acryl-router/src/core/router/reg-exp-router/router.ts +174 -0
- package/plugins/acryl-router/src/core/router/reg-exp-router/trie.ts +84 -0
- package/plugins/acryl-router/src/core/router/smart-router/index.ts +6 -0
- package/plugins/acryl-router/src/core/router/smart-router/router.test.ts +13 -0
- package/plugins/acryl-router/src/core/router/smart-router/router.ts +70 -0
- package/plugins/acryl-router/src/core/router/trie-router/index.ts +6 -0
- package/plugins/acryl-router/src/core/router/trie-router/node.test.ts +896 -0
- package/plugins/acryl-router/src/core/router/trie-router/node.ts +221 -0
- package/plugins/acryl-router/src/core/router/trie-router/router.test.ts +8 -0
- package/plugins/acryl-router/src/core/router/trie-router/router.ts +18 -0
- package/plugins/acryl-router/src/core/router/utils.ts +1 -0
- package/plugins/acryl-router/src/core/router.ts +103 -0
- package/plugins/acryl-router/src/core/types.test.ts +4134 -0
- package/plugins/acryl-router/src/core/types.ts +2782 -0
- package/plugins/acryl-router/src/core/utils/accept.test.ts +299 -0
- package/plugins/acryl-router/src/core/utils/accept.ts +268 -0
- package/plugins/acryl-router/src/core/utils/basic-auth.test.ts +56 -0
- package/plugins/acryl-router/src/core/utils/basic-auth.ts +26 -0
- package/plugins/acryl-router/src/core/utils/body.test.ts +496 -0
- package/plugins/acryl-router/src/core/utils/body.ts +263 -0
- package/plugins/acryl-router/src/core/utils/buffer.test.ts +147 -0
- package/plugins/acryl-router/src/core/utils/buffer.ts +117 -0
- package/plugins/acryl-router/src/core/utils/color.test.ts +23 -0
- package/plugins/acryl-router/src/core/utils/color.ts +56 -0
- package/plugins/acryl-router/src/core/utils/compress.ts +10 -0
- package/plugins/acryl-router/src/core/utils/concurrent.test.ts +69 -0
- package/plugins/acryl-router/src/core/utils/concurrent.ts +55 -0
- package/plugins/acryl-router/src/core/utils/constants.ts +4 -0
- package/plugins/acryl-router/src/core/utils/cookie.test.ts +460 -0
- package/plugins/acryl-router/src/core/utils/cookie.ts +281 -0
- package/plugins/acryl-router/src/core/utils/crypto.test.ts +43 -0
- package/plugins/acryl-router/src/core/utils/crypto.ts +58 -0
- package/plugins/acryl-router/src/core/utils/encode.test.ts +61 -0
- package/plugins/acryl-router/src/core/utils/encode.ts +34 -0
- package/plugins/acryl-router/src/core/utils/filepath.test.ts +78 -0
- package/plugins/acryl-router/src/core/utils/filepath.ts +60 -0
- package/plugins/acryl-router/src/core/utils/handler.ts +15 -0
- package/plugins/acryl-router/src/core/utils/headers.ts +355 -0
- package/plugins/acryl-router/src/core/utils/html.test.ts +16 -0
- package/plugins/acryl-router/src/core/utils/html.ts +182 -0
- package/plugins/acryl-router/src/core/utils/http-status.ts +72 -0
- package/plugins/acryl-router/src/core/utils/ipaddr.test.ts +149 -0
- package/plugins/acryl-router/src/core/utils/ipaddr.ts +374 -0
- package/plugins/acryl-router/src/core/utils/jwt/index.ts +7 -0
- package/plugins/acryl-router/src/core/utils/jwt/jwa.test.ts +86 -0
- package/plugins/acryl-router/src/core/utils/jwt/jwa.ts +38 -0
- package/plugins/acryl-router/src/core/utils/jwt/jws.ts +235 -0
- package/plugins/acryl-router/src/core/utils/jwt/jwt.test.ts +1788 -0
- package/plugins/acryl-router/src/core/utils/jwt/jwt.ts +292 -0
- package/plugins/acryl-router/src/core/utils/jwt/types.ts +157 -0
- package/plugins/acryl-router/src/core/utils/jwt/utf8.ts +7 -0
- package/plugins/acryl-router/src/core/utils/mime.test.ts +64 -0
- package/plugins/acryl-router/src/core/utils/mime.ts +94 -0
- package/plugins/acryl-router/src/core/utils/stream.test.ts +212 -0
- package/plugins/acryl-router/src/core/utils/stream.ts +107 -0
- package/plugins/acryl-router/src/core/utils/types.test.ts +295 -0
- package/plugins/acryl-router/src/core/utils/types.ts +123 -0
- package/plugins/acryl-router/src/core/utils/url.test.ts +384 -0
- package/plugins/acryl-router/src/core/utils/url.ts +337 -0
- package/plugins/acryl-router/src/core/validator/index.ts +8 -0
- package/plugins/acryl-router/src/core/validator/utils.test.ts +82 -0
- package/plugins/acryl-router/src/core/validator/utils.ts +69 -0
- package/plugins/acryl-router/src/core/validator/validator.test.ts +1488 -0
- package/plugins/acryl-router/src/core/validator/validator.ts +185 -0
- package/plugins/acryl-router/src/helpers/index.ts +13 -0
- package/plugins/acryl-router/src/helpers/schema-validator.ts +35 -0
- package/plugins/acryl-router/src/index.ts +6 -0
- package/plugins/acryl-router/src/middleware/index.ts +25 -0
- package/plugins/acryl-router/src/middleware/loopback-only.ts +88 -0
- package/plugins/acryl-router/src/service.ts +356 -0
- package/plugins/acryl-router/tests/docs-examples.spec.ts +152 -0
- package/plugins/acryl-router/tests/live-install.spec.ts +64 -0
- package/plugins/acryl-router/tests/loader.spec.ts +72 -0
- package/plugins/acryl-router/tests/service.spec.ts +409 -0
- package/plugins/acryl-router/tests/shadow-state.spec.ts +79 -0
- package/plugins/acryl-router/upstream.json +20 -0
- package/plugins/acryl-router-http/LICENSE +21 -0
- package/plugins/acryl-router-http/plugin.json +12 -0
- package/plugins/acryl-router-http/src/bridge.ts +54 -0
- package/plugins/acryl-router-http/src/index.ts +63 -0
- package/plugins/acryl-router-http/src/serve.ts +24 -0
- package/plugins/acryl-router-http/tests/adapter.spec.ts +174 -0
- package/plugins/acryl-router-http/tests/stream.spec.ts +133 -0
- package/plugins/acryl-router-http/tests/surfaces.spec.ts +101 -0
- package/plugins/acryl-router-http/upstream.json +22 -0
- package/plugins/acryl-state/LICENSE +21 -0
- package/plugins/acryl-state/NOTICE +30 -0
- package/plugins/acryl-state/plugin.json +12 -0
- package/plugins/acryl-state/src/cell.ts +67 -0
- package/plugins/acryl-state/src/index.ts +3 -0
- package/plugins/acryl-state/src/react.ts +11 -0
- package/plugins/acryl-state/src/service.ts +119 -0
- package/plugins/acryl-state/tests/cell.spec.ts +71 -0
- package/plugins/acryl-state/tests/react.spec.ts +39 -0
- package/plugins/acryl-state/tests/service.spec.ts +132 -0
- package/plugins/acryl-state/upstream.json +22 -0
- package/plugins/builder/script.ts +6 -10
- package/plugins/extensions/index.ts +20 -3
- package/plugins/extensions/resolve.ts +28 -3
- package/plugins/openai/prompt.ts +12 -6
- package/plugins/tasks/index.ts +14 -0
- package/plugins/tsconfig.json +14 -5
- package/templates/agent/blend.yaml +5 -7
- package/templates/agent/src/app.ts +8 -8
- package/templates/agent/src/routes.ts +39 -38
- package/templates/agent/template.json +3 -3
- package/templates/agent/test/builder.test.ts +22 -14
- package/templates/agent/test/model.test.ts +3 -3
- package/templates/agent/tsconfig.json +8 -5
- package/templates/{app → blank}/blend.yaml +3 -3
- package/templates/{app → blank}/src/app.ts +5 -5
- package/templates/blank/src/routes.ts +16 -0
- package/templates/blank/template.json +1 -0
- package/templates/{app → blank}/test/app.test.ts +1 -1
- package/templates/{app → blank}/tsconfig.json +2 -4
- package/templates/todo/README.md +27 -0
- package/templates/todo/blend.yaml +25 -0
- package/templates/todo/gitignore +2 -0
- package/templates/todo/package.json +23 -0
- package/templates/todo/src/app.ts +71 -0
- package/templates/todo/src/main.ts +19 -0
- package/templates/todo/src/page.ts +28 -0
- package/templates/todo/src/todo.ts +123 -0
- package/templates/todo/template.json +1 -0
- package/templates/todo/test/todo.test.ts +145 -0
- package/templates/todo/tsconfig.json +17 -0
- package/templates/todo/upstream.json +12 -0
- package/plugins/events/index.ts +0 -42
- package/plugins/events/plugin.json +0 -9
- package/plugins/http/index.ts +0 -64
- package/plugins/http/plugin.json +0 -10
- package/plugins/server/index.ts +0 -138
- package/plugins/server/plugin.json +0 -10
- package/templates/app/src/routes.ts +0 -14
- package/templates/app/template.json +0 -1
- /package/templates/{app → blank}/README.md +0 -0
- /package/templates/{app → blank}/gitignore +0 -0
- /package/templates/{app → blank}/package.json +0 -0
- /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 = ['
|
|
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
|
-
|
|
33
|
-
ctx.
|
|
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
|
|
38
|
-
if (reply.
|
|
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.
|
|
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
|
-
|
|
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) {
|
package/plugins/openai/prompt.ts
CHANGED
|
@@ -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 = ['
|
|
15
|
+
export const inject = ['router'] // services it needs; it waits (pending) until they exist
|
|
15
16
|
export function apply(ctx: Context) {
|
|
16
|
-
|
|
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
|
|
21
|
-
if (
|
|
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
|
|
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.`
|
package/plugins/tasks/index.ts
CHANGED
|
@@ -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. */
|
package/plugins/tsconfig.json
CHANGED
|
@@ -1,12 +1,10 @@
|
|
|
1
1
|
{
|
|
2
2
|
"compilerOptions": {
|
|
3
|
-
"target": "
|
|
4
|
-
"lib": ["
|
|
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:
|
|
38
|
-
name:
|
|
39
|
-
- id:
|
|
40
|
-
name:
|
|
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}}"
|