From 28049af39a31e77c73822fb28a92cd730afe6448 Mon Sep 17 00:00:00 2001 From: Altay Date: Mon, 10 Aug 2026 10:34:56 +0300 Subject: [PATCH 1/8] fix: upgrade Effect v4 runtime boundaries --- AGENTS.md | 10 + README.md | 2 +- docs/READINESS.md | 9 +- package.json | 6 +- pnpm-lock.yaml | 303 +++++++++++++------------------ pnpm-workspace.yaml | 4 + scripts/prepare-effect.sh | 11 +- src/cli-artifact-commands.ts | 4 +- src/cli-command.ts | 4 +- src/cli-input-json-read.ts | 37 ++-- src/cli-input-source.ts | 2 +- src/cli-node-condition.ts | 2 +- src/cli-runner.ts | 4 +- src/cli.ts | 4 +- src/debug.ts | 12 +- src/device.ts | 5 +- src/discovery.ts | 2 +- src/ecp.ts | 35 ++-- src/errors.ts | 15 +- src/installer.ts | 40 ++-- src/package-zip.ts | 44 +++-- src/polling.ts | 39 ++-- src/rokit-effect.ts | 2 +- src/roku-context-requirements.ts | 4 +- src/runtime.ts | 6 +- src/screenshot.ts | 61 ++++--- src/timing.ts | 3 + test/errors.test.ts | 6 +- test/roku.test.ts | 19 +- 29 files changed, 353 insertions(+), 342 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index 6c28643..346d3e1 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -52,6 +52,16 @@ Keep it platform-focused, typed, and useful for both humans and agents. - Keep app journeys, content IDs, account data, and product assertions out of the generic harness. +## Learning more about the Effect + +This repository uses the Effect Typescript library. + +Before writing any Effect code, first read `node_modules/effect/AGENTS.md` +**completely**, and follow the links in the file when required. + +If you need to learn more about particular Effect apis and concepts that the +guide doesn't cover, search through the source code in `node_modules/effect/src`. + ## Sharp Edges - Missing config/env and child-command failures should not print stack traces. diff --git a/README.md b/README.md index 5345596..d43a716 100644 --- a/README.md +++ b/README.md @@ -17,7 +17,7 @@ ## Install -Requires Node `>=24.14.0`; install it in a consumer repo with: +Requires Node `>=24.18.0`; install it in a consumer repo with: ```bash pnpm add -D @putdotio/rokit diff --git a/docs/READINESS.md b/docs/READINESS.md index ee1ae5e..60ef6f2 100644 --- a/docs/READINESS.md +++ b/docs/READINESS.md @@ -1,7 +1,5 @@ # Agent Readiness -Status: current as of 2026-05-28 - `rokit` is a CLI/package repo. There is no long-running app to boot; readiness is based on deterministic package verification plus optional live Roku proof. ## Grade @@ -66,9 +64,10 @@ pnpm hooks:install pnpm verify ``` -`pnpm install` runs the local Effect source setup outside CI only. When `CI` is -set to any non-empty value, `scripts/prepare-effect.sh` exits without cloning -`.repos/effect` so install, pack, publish, and CI/CD workspaces stay predictable. +`pnpm install` checks out the exact `effect` dependency tag into +`.repos/effect` outside CI only. When `CI` is set to any non-empty value, +`scripts/prepare-effect.sh` exits without cloning the source so install, pack, +publish, and CI/CD workspaces stay predictable. Optional live smoke: diff --git a/package.json b/package.json index 9bce7a6..8056484 100644 --- a/package.json +++ b/package.json @@ -58,13 +58,13 @@ "verify": "vp check . && tsc --noEmit && vp pack src/index.ts src/rokit.ts --dts && node dist/rokit.mjs --version && node dist/rokit.mjs --help >/dev/null && vp test run --passWithNoTests && npm pack --dry-run" }, "dependencies": { - "@effect/platform-node": "4.0.0-beta.102", - "effect": "4.0.0-beta.102", + "@effect/platform-node": "4.0.0-beta.107", + "effect": "4.0.0-beta.107", "jszip": "^3.10.1", "roku-deploy": "3.18.2" }, "devDependencies": { - "@effect/vitest": "4.0.0-beta.102", + "@effect/vitest": "4.0.0-beta.107", "@types/node": "^26.1.2", "typescript": "^7.0.2", "vite": "catalog:", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 3d7460e..0af8604 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -13,11 +13,11 @@ importers: .: dependencies: '@effect/platform-node': - specifier: 4.0.0-beta.102 - version: 4.0.0-beta.102(effect@4.0.0-beta.102)(ioredis@5.10.1) + specifier: 4.0.0-beta.107 + version: 4.0.0-beta.107(effect@4.0.0-beta.107)(ioredis@5.11.1) effect: - specifier: 4.0.0-beta.102 - version: 4.0.0-beta.102 + specifier: 4.0.0-beta.107 + version: 4.0.0-beta.107 jszip: specifier: ^3.10.1 version: 3.10.1 @@ -26,23 +26,23 @@ importers: version: 3.18.2 devDependencies: '@effect/vitest': - specifier: 4.0.0-beta.102 - version: 4.0.0-beta.102(effect@4.0.0-beta.102)(vitest@4.1.10) + specifier: 4.0.0-beta.107 + version: 4.0.0-beta.107(effect@4.0.0-beta.107)(vitest@4.1.10(@types/node@26.2.0)(@vitest/browser-preview@4.1.10)(@voidzero-dev/vite-plus-core@0.2.6(@types/node@26.2.0)(typescript@7.0.2))) '@types/node': specifier: ^26.1.2 - version: 26.1.2 + version: 26.2.0 typescript: specifier: ^7.0.2 version: 7.0.2 vite: specifier: npm:@voidzero-dev/vite-plus-core@0.2.6 - version: '@voidzero-dev/vite-plus-core@0.2.6(@types/node@26.1.2)(typescript@7.0.2)(yaml@2.9.0)' + version: '@voidzero-dev/vite-plus-core@0.2.6(@types/node@26.2.0)(typescript@7.0.2)' vite-plus: specifier: 0.2.6 - version: 0.2.6(@types/node@26.1.2)(@voidzero-dev/vite-plus-core@0.2.6(@types/node@26.1.2)(typescript@7.0.2)(yaml@2.9.0))(typescript@7.0.2)(yaml@2.9.0) + version: 0.2.6(@types/node@26.2.0)(@voidzero-dev/vite-plus-core@0.2.6(@types/node@26.2.0)(typescript@7.0.2))(typescript@7.0.2) vitest: specifier: 4.1.10 - version: 4.1.10(@types/node@26.1.2)(@vitest/browser-preview@4.1.10)(@voidzero-dev/vite-plus-core@0.2.6(@types/node@26.1.2)(typescript@7.0.2)(yaml@2.9.0)) + version: 4.1.10(@types/node@26.2.0)(@vitest/browser-preview@4.1.10)(@voidzero-dev/vite-plus-core@0.2.6(@types/node@26.2.0)(typescript@7.0.2)) packages: @@ -61,27 +61,27 @@ packages: '@blazediff/core@1.9.1': resolution: {integrity: sha512-ehg3jIkYKulZh+8om/O25vkvSsXXwC+skXmyA87FFx6A/45eqOkZsBltMw/TVteb0mloiGT8oGRTcjRAz66zaA==} - '@effect/platform-node-shared@4.0.0-beta.102': - resolution: {integrity: sha512-gVd793I72MrkX4dXo7eYtRKfNj0RW4eMRfVEKEJI16h2+mBDCzQ+gqMrog2hSTHQnaIbvbYShNQ4TVGuRCYZeQ==} + '@effect/platform-node-shared@4.0.0-beta.107': + resolution: {integrity: sha512-y6BqcRi86BfTJv+tvDrob4ozYVHxxlHYcn/zIQqZjXI9CvKnkgD6ng+38G1o45c4f2ucU+6HRI9POCmFdMoVGA==} engines: {node: '>=18.0.0'} peerDependencies: - effect: ^4.0.0-beta.102 + effect: ^4.0.0-beta.107 - '@effect/platform-node@4.0.0-beta.102': - resolution: {integrity: sha512-wYVAU9jAePT+gouMr/EVz1CaW6yDLjPAgXYeEFLz7wugT5iZhFRag6mqajV/wwN3bzWP2fzZHokLuPmqD/rqeA==} + '@effect/platform-node@4.0.0-beta.107': + resolution: {integrity: sha512-k+6YNbV4Ck0L6YXtlgkvEnuP5tlxWD8EeWOrpn46PDqbGEwt4ONpRltTwm3tn2cyBXD0i+2P11cUH/6sdFagTA==} engines: {node: '>=18.0.0'} peerDependencies: - effect: ^4.0.0-beta.102 - ioredis: ^5.7.0 + effect: ^4.0.0-beta.107 + ioredis: '>=5.7.0 <6.0.0' - '@effect/vitest@4.0.0-beta.102': - resolution: {integrity: sha512-4dipFAYG6imOzrY3zy3BgzCJkbb9xESyzUef0WSx8bsK0/SpITqbJpdwKeOyDWLZ+rimjua8IbTFMAde865pIQ==} + '@effect/vitest@4.0.0-beta.107': + resolution: {integrity: sha512-n4/qsx4DnT4dEI/wNgMivxyUeJoeiU1TCSz0WnoHWk/dny40Oxjip2P9IXGQDgPb9fsYVnerF0QRA6nPUuExQA==} peerDependencies: - effect: ^4.0.0-beta.102 + effect: ^4.0.0-beta.107 vitest: 4.1.10 - '@ioredis/commands@1.5.1': - resolution: {integrity: sha512-JH8ZL/ywcJyR9MmJ5BNqZllXNZQqQbnVZOqpPQqE1vHiFgAw4NHbvE0FOduNU8IX9babitBT46571OnPTT0Zcw==} + '@ioredis/commands@1.10.0': + resolution: {integrity: sha512-UmeW7z4LfctwoQ5wkhVzgq8tXkreED2xZGpX+Bg+zA+WJFZCT6c062AfCK/Dfk81xZnnwdhJCUMkitihRaoC2Q==} '@jridgewell/sourcemap-codec@1.5.5': resolution: {integrity: sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==} @@ -426,8 +426,8 @@ packages: resolution: {integrity: sha512-o4PXJQidqJl82ckFaXUeoAW+XysPLauYI43Abki5hABd853iMhitooc6znOnczgbTYmEP6U6/y1ZyKAIsvMKGg==} engines: {node: '>=18'} - '@testing-library/user-event@14.6.1': - resolution: {integrity: sha512-vq7fv0rnt+QTXgPxr5Hjc210p6YKq2kmdziLgnsZGgLJ9e6VAShx1pACLuRjd/AS/sr7phAR58OIIpf0LlmQNw==} + '@testing-library/user-event@14.6.3': + resolution: {integrity: sha512-6dBq67jT8lE+JTE8Exm02Kt6ze43hz1jdiSpSJwtTZiT1xQQ6b7nZYTTQ9njdArdU8XklOwaDp/AbT/eYSKF4g==} engines: {node: '>=12', npm: '>=6'} peerDependencies: '@testing-library/dom': '>=7.21.4' @@ -444,8 +444,8 @@ packages: '@types/estree@1.0.9': resolution: {integrity: sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==} - '@types/node@26.1.2': - resolution: {integrity: sha512-Vu4a5UFA9rIIFJ7rB/Vaafh9lrCQszopTCx6KjFboXTGQbPNasehVR5TEiithSDGyd1DEiUByggTZsg8jukeIg==} + '@types/node@26.2.0': + resolution: {integrity: sha512-5IviulTZeRNp2vAJ514cc/HUlY5nZ9fCbq9DMyC52BrhFZACo3nI0R7qBxhQmo/d27NFe96ur/b7Wwxklda+kg==} '@types/ws@8.18.1': resolution: {integrity: sha512-ThVF6DCVhA8kUGy+aazFQ4kXQ7E1Ty7A3ypFOe0IcJV8O/M511G99AW24irKrW56Wt44yG9+ij8FaqoBGkuBXg==} @@ -882,8 +882,8 @@ packages: resolution: {integrity: sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==} engines: {node: '>=10'} - cluster-key-slot@1.1.2: - resolution: {integrity: sha512-RMr0FhtfXemyinomL4hrWcYJxmX6deFdCxpJzhDttxgO1+bcCnkk+9drydLVDmAMG7NE6aN/fl4F7ucU/90gAA==} + cluster-key-slot@1.1.1: + resolution: {integrity: sha512-rwHwUfXL40Chm1r08yrhU3qpUvdVlgkKNeyeGPOxnW8/SyVDvgRaed/Uz54AqWNaTCAThlj6QAs3TZcKI0xDEw==} engines: {node: '>=0.10.0'} color-convert@1.9.3: @@ -929,11 +929,11 @@ packages: dom-accessibility-api@0.5.16: resolution: {integrity: sha512-X7BJ2yElsnOJ30pZF4uIIDfBEVgF4XEBxL9Bxhy6dnrm5hkzqmsWHGTiHqRiITNhMyFLyAiWndIJP7Z1NTteDg==} - effect@4.0.0-beta.102: - resolution: {integrity: sha512-z8Y+Q76Hh/kjLFZrXu8tGn6e+tDsg45R+UHhxd190pXxD53OGwf/G/zDxXTkse4HJ5mobNZfitLfUCp4fMvu6w==} + effect@4.0.0-beta.107: + resolution: {integrity: sha512-OoBAv8eF+yanc+C6xhgEUnWeXUSHA6ynnscYqpkAY9GSnzZWystsIjBowVqCkLpHGlnRtdIqYT3wHwpOY6JDnQ==} - es-module-lexer@2.3.0: - resolution: {integrity: sha512-KLdwQm2NvGLDkQDCGvmiQrhkd0JbMzXthwQAUgWjQuQdBLFa3eiBP5arXZyA+f8x+x7OXgud6bq2rxjGtHV2tw==} + es-module-lexer@2.3.1: + resolution: {integrity: sha512-shc1dbU90Yl/xq1QrC7QRtfcwURZuVRfPhZbDoldJ1cn1gzDvBaBWlv0eFolj5+0znnPJz5TXLxsN77X/12KTA==} escape-string-regexp@1.0.5: resolution: {integrity: sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==} @@ -970,9 +970,6 @@ packages: resolution: {integrity: sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==} engines: {node: '>=8'} - find-my-way-ts@0.1.6: - resolution: {integrity: sha512-a85L9ZoXtNAey3Y6Z+eBWW658kO/MwR7zIafkIUPUMf3isZG0NCs2pjW2wtjxAKuJPxMAsHUIP4ZPGv0o5gyTA==} - fs-extra@7.0.1: resolution: {integrity: sha512-YJDaCJZEnBmcbw13fvdAM9AwNOJwOzrE4pqMqBq5nFiEqXUqHwlK4B+3pUw6JNvfSPtX05xFHtYy/1ni01eGCw==} engines: {node: '>=6 <7 || >=8'} @@ -1007,12 +1004,8 @@ packages: inherits@2.0.4: resolution: {integrity: sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==} - ini@7.0.0: - resolution: {integrity: sha512-ifK0CgjALofS5bkrcTy4RaQ9Vx2Knf/eLeIO+NaswQEpH1UblrtTSCIvN71qQDMq0PeQ/SSPojvEJp9vvvfr+w==} - engines: {node: ^22.22.2 || ^24.15.0 || >=26.0.0} - - ioredis@5.10.1: - resolution: {integrity: sha512-HuEDBTI70aYdx1v6U97SbNx9F1+svQKBDo30o0b9fw055LMepzpOOd0Ccg9Q6tbqmBSJaMuY0fB7yw9/vjBYCA==} + ioredis@5.11.1: + resolution: {integrity: sha512-ehuGcf94bQXhfagULNXrJdfnWO38v070jxSx/qE87Kjzmu2fU7ro5EFAb+OPituLqgfyuQaym5DlrNydW2sJ9A==} engines: {node: '>=12.22.0'} is-extglob@2.1.1: @@ -1122,12 +1115,6 @@ packages: resolution: {integrity: sha512-WkUDrojuJs0xkgGf2udWxa3yGBRxPtxUkB79i6aCZLRgc7PM8fZe9TosfPDcvEpQZbuFASnHYmRLBLUbmLOIIA==} engines: {node: '>= 12.0.0'} - lodash.defaults@4.2.0: - resolution: {integrity: sha512-qjxPLHd3r5DnsdGacqOMU6pb/avJzdh9tFX2ymgoZE27BmjXrNy/y4LoaiTeAb+O3gL8AfpJGtqfX/ae2leYYQ==} - - lodash.isarguments@3.1.0: - resolution: {integrity: sha512-chi4NHZlZqZD18a0imDHnZPrDeBbTtVN7GXMwuGdRH9qotxAjYs3aVLKc7zNOG9eddR5Ksd8rvFEBc9SsggPpg==} - lz-string@1.5.0: resolution: {integrity: sha512-h5bgJWpxJNswbU7qCrV0tIKQCaS3blPDrqKWx+QxzuzL1zGUzij9XCWLrSLsJPu5t+eWA/ycetzYAO5IOMcWAQ==} hasBin: true @@ -1159,14 +1146,11 @@ packages: resolution: {integrity: sha512-4kmO/MdyUIkLIvTPr8VHLil4AtoKIoniWPIEk5+CDy0xnWC84azhSFmuJ7PxZdsYtiP5kEeQsORAVIeMgxT+Hw==} hasBin: true - msgpackr@2.0.4: - resolution: {integrity: sha512-o1C5KRmuRt+apqMr1HuGSqWStZoRBUpEsCsl15uM9VdAF1qHLtvMOU2En747EnTyEl6c4pzPewRMFF31s1CNbA==} + msgpackr@2.0.5: + resolution: {integrity: sha512-cef05H/dSYpLpqp3sj/qyZh5vhUYCalnaLO7j1yOmpsR0y/XwLVtK7r5gn+U/F7CTEfMowcGhlUQJDLcLf7jcA==} - multipasta@0.2.8: - resolution: {integrity: sha512-ZPWuMKyv0cSO29f7hozp+k6+crZbQijV8ipMvxNxRf2SwtYGTX1ZX89Kd20VV4H9Znonx+EQn+iy1wGQsJ+b+Q==} - - nanoid@3.3.15: - resolution: {integrity: sha512-y7Wygv/7mEOvxTuEQDB8StXdMRBWf1kR/tlhAzBRUFkB2jfcLOAxO/SHmOO2zgz1pVgK29/kyupn059/bCHdjA==} + nanoid@3.3.18: + resolution: {integrity: sha512-DTg4MJbGMWkfi6VZFdNt2/caMbQy4Ou+Op/hJQvGEWcnVfoA1QA+xzRKAzw9jD6+GVOOeYr/mIcuDSdug6F6+w==} engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} hasBin: true @@ -1179,8 +1163,8 @@ packages: resolution: {integrity: sha512-s+w+rBWnpTMwSFbaE0UXsRlg7hU4FjekKU4eyAih5T8nJuNZT1nNsskXpxmeqSK9UzkBl6UgRlnKc8hz8IEqOw==} hasBin: true - obug@2.1.3: - resolution: {integrity: sha512-9miFgM2OFba7hB+pRgvtV84pYTBaoTHohvmIgiRt6dRIzbwEOIaNaP+dIlGs2fNFoB0SeISs0Jz5WFVRid6Xyg==} + obug@2.1.4: + resolution: {integrity: sha512-4a+OsYv9UktOJKE+l1A4OufDgdRF9PifWj+tJnHURo/P+WOxpG4GzUFL9qCalmWauao6ogiG+QvnCovwPoyAWA==} engines: {node: '>=12.20.0'} oxfmt@0.60.0: @@ -1234,8 +1218,8 @@ packages: resolution: {integrity: sha512-LKWqWJRhstyYo9pGvgor/ivk2w94eSjE3RGVuzLGlr3NmD8bf7RcYGze1mNdEHRP6TRP6rMuDHk5t44hnTRyow==} engines: {node: '>=14.19.0'} - postcss@8.5.16: - resolution: {integrity: sha512-vuwillviilfKZsg0VGj5R/YwwcHx4SLsIOI/7K6mQkWx+l5cUHTjj5g0AasTBcyXsbfTgrwsUNmVUb5xVwyPwg==} + postcss@8.5.26: + resolution: {integrity: sha512-u82N74LFzG8ca+dD8puPnplTXoGH4fTPpVGuIbt36G3qvNlkvfD0lEAZSxaly3KX8TS/L1A1gsCEmvKmBcVbkQ==} engines: {node: ^10 || ^12 || >=14} pretty-format@27.5.1: @@ -1245,8 +1229,8 @@ packages: process-nextick-args@2.0.1: resolution: {integrity: sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==} - pure-rand@8.4.0: - resolution: {integrity: sha512-IoM8YF/jY0hiugFo/wOWqfmarlE6J0wc6fDK1PhftMk7MGhVZl88sZimmqBBFomLOCSmcCCpsfj7wXASCpvK9A==} + pure-rand@8.4.2: + resolution: {integrity: sha512-vvuOGgcuPJAirlHvuQw1TrOiw7ptaIXXmIbNuiNOY6lNGJJH49PQ1Kj4nd783nPdQhQdicgOjVI2yI/9BD6/Ng==} queue-microtask@1.2.3: resolution: {integrity: sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==} @@ -1282,8 +1266,8 @@ packages: safer-buffer@2.1.2: resolution: {integrity: sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==} - sax@1.6.0: - resolution: {integrity: sha512-6R3J5M4AcbtLUdZmRv2SygeVaM7IhrLXu9BmnOGmmACak8fiUtOsYNWUS4uK7upbmHIBbLBeFeI//477BKLBzA==} + sax@1.6.1: + resolution: {integrity: sha512-42tBVwLWnaQvW5zc4HbZrTuWccECCZfBi92FDuwtqxasH+JbPB3/FOKb1m222K42R4WxuxzzMsTswfzgtSu64Q==} engines: {node: '>=11.0.0'} semver@7.8.5: @@ -1328,8 +1312,8 @@ packages: tinybench@2.9.0: resolution: {integrity: sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==} - tinyexec@1.2.4: - resolution: {integrity: sha512-SHf/r48b7vOrjve9PxJo3MN5v5yuyjHvdUcrQffT3WXMUfnGmHDVbC4k3sHJaJTgZCwpUplIaAo5ANtMyp3YHg==} + tinyexec@1.3.0: + resolution: {integrity: sha512-QKAl9m8gWWGHV8jZcPeym6j+XULi6tOf1mT83WYJ4Lk2ytW/uwAWkrP0uFsdoYMdueVJ0qs26wZ+23xeB4ibNQ==} engines: {node: '>=18'} tinyglobby@0.2.17: @@ -1340,18 +1324,14 @@ packages: resolution: {integrity: sha512-Pugqs6M0m7Lv1I7FtxN4aoyToKg1C4tu+/381vH35y8oENM/Ai7f7C4StcoK4/+BSw9ebcS8jRiVrORFKCALLw==} engines: {node: ^20.0.0 || >=22.0.0} - tinyrainbow@3.1.0: - resolution: {integrity: sha512-Bf+ILmBgretUrdJxzXM0SgXLZ3XfiaUuOj/IKQHuTXip+05Xn+uyEYdVg0kYDipTBcLrCVyUzAPz7QmArb0mmw==} + tinyrainbow@3.1.1: + resolution: {integrity: sha512-yau8yJdTt989Mm0Bd/236QnzEiPf2xLLTqUZRUJOo/3CB078LSwzei343DgtJVmfJKJE3TMINY1u42SQsP6mXw==} engines: {node: '>=14.0.0'} to-regex-range@5.0.1: resolution: {integrity: sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==} engines: {node: '>=8.0'} - toml@4.1.2: - resolution: {integrity: sha512-m0vXfHODcw3gk+KONAOlVQ5yNHc3yS3B1ybM3HS1vqDoS0RWTDDVBVVTYi8hH0k+2OM1vmo9fb1WX9EVqjqfHA==} - engines: {node: '>=20'} - totalist@3.0.1: resolution: {integrity: sha512-sf4i37nQ2LBx4m3wB74y+ubopq6W/dIzXg0FDGjsYnZHVa1Da8FH853wlL2gtUhg+xJXjfk3kUZS3BRoQeoQBQ==} engines: {node: '>=6'} @@ -1364,8 +1344,8 @@ packages: undici-types@8.3.0: resolution: {integrity: sha512-j375ScV60dom+YkPFIfTLcOiPxkN/buHz5GobjLhixFuANaNs3C9l4GmrWqejgXWJ7BbJcFYpTEUkS1Ge8bpZQ==} - undici@8.7.0: - resolution: {integrity: sha512-N7iQtfyLhIMOFgQubvmLV26svHpO0bqKnAiWotTQCVKCmWrcGbBotPuW1x+xwYZ2VHdSTVUfPQQnlEt1/LouTQ==} + undici@8.10.0: + resolution: {integrity: sha512-HvltHd7avK13QIw/oLe4qoOLyoVSoafqJ2jYOrtMRBkbYT31eiBQ8O0ehRKZiEZCMEyLFQNIADpgCWC5fALvYQ==} engines: {node: '>=22.19.0'} universalify@0.1.2: @@ -1438,8 +1418,8 @@ packages: engines: {node: '>=8'} hasBin: true - ws@8.21.0: - resolution: {integrity: sha512-Vsp28b7DRcimFQvrqu2Wek3z1iYxDCWqHYB8Qsnk/S4RfaCQzPGPyBNuVjJV3cd6UiKtUtp6sNM77gWvzcCH+g==} + ws@8.21.3: + resolution: {integrity: sha512-201TZ/kPWxoPr/OKWjquZR1SWKXcvxdH+e1xrx89b3YbmzLMFCLfnaG1HFIgWzJOEWZ7MvpK++odZufgYR50Rw==} engines: {node: '>=10.0.0'} peerDependencies: bufferutil: ^4.0.1 @@ -1458,11 +1438,6 @@ packages: resolution: {integrity: sha512-fDlsI/kFEx7gLvbecc0/ohLG50fugQp8ryHzMTuW9vSa1GJ0XYWKnhsUx7oie3G98+r56aTQIUB4kht42R3JvA==} engines: {node: '>=4.0'} - yaml@2.9.0: - resolution: {integrity: sha512-2AvhNX3mb8zd6Zy7INTtSpl1F15HW6Wnqj0srWlkKLcpYl/gMIMJiyuGq2KeI2YFxUPjdlB+3Lc10seMLtL4cA==} - engines: {node: '>= 14.6'} - hasBin: true - yuku-codegen@0.5.48: resolution: {integrity: sha512-p7HxD5Xl4jzDzqMrGePAOeSHmRY4g58h4HuGq15weQFPxuPWd/W6e7nqp/+Lea6JfpOdBwJOAyXFqIZ/J9Zfnw==} @@ -1483,32 +1458,32 @@ snapshots: '@blazediff/core@1.9.1': {} - '@effect/platform-node-shared@4.0.0-beta.102(effect@4.0.0-beta.102)': + '@effect/platform-node-shared@4.0.0-beta.107(effect@4.0.0-beta.107)': dependencies: '@types/ws': 8.18.1 - effect: 4.0.0-beta.102 - ws: 8.21.0 + effect: 4.0.0-beta.107 + ws: 8.21.3 transitivePeerDependencies: - bufferutil - utf-8-validate - '@effect/platform-node@4.0.0-beta.102(effect@4.0.0-beta.102)(ioredis@5.10.1)': + '@effect/platform-node@4.0.0-beta.107(effect@4.0.0-beta.107)(ioredis@5.11.1)': dependencies: - '@effect/platform-node-shared': 4.0.0-beta.102(effect@4.0.0-beta.102) - effect: 4.0.0-beta.102 - ioredis: 5.10.1 + '@effect/platform-node-shared': 4.0.0-beta.107(effect@4.0.0-beta.107) + effect: 4.0.0-beta.107 + ioredis: 5.11.1 mime: 4.1.0 - undici: 8.7.0 + undici: 8.10.0 transitivePeerDependencies: - bufferutil - utf-8-validate - '@effect/vitest@4.0.0-beta.102(effect@4.0.0-beta.102)(vitest@4.1.10)': + '@effect/vitest@4.0.0-beta.107(effect@4.0.0-beta.107)(vitest@4.1.10(@types/node@26.2.0)(@vitest/browser-preview@4.1.10)(@voidzero-dev/vite-plus-core@0.2.6(@types/node@26.2.0)(typescript@7.0.2)))': dependencies: - effect: 4.0.0-beta.102 - vitest: 4.1.10(@types/node@26.1.2)(@vitest/browser-preview@4.1.10)(@voidzero-dev/vite-plus-core@0.2.6(@types/node@26.1.2)(typescript@7.0.2)(yaml@2.9.0)) + effect: 4.0.0-beta.107 + vitest: 4.1.10(@types/node@26.2.0)(@vitest/browser-preview@4.1.10)(@voidzero-dev/vite-plus-core@0.2.6(@types/node@26.2.0)(typescript@7.0.2)) - '@ioredis/commands@1.5.1': {} + '@ioredis/commands@1.10.0': {} '@jridgewell/sourcemap-codec@1.5.5': {} @@ -1699,7 +1674,7 @@ snapshots: picocolors: 1.1.1 pretty-format: 27.5.1 - '@testing-library/user-event@14.6.1(@testing-library/dom@10.4.1)': + '@testing-library/user-event@14.6.3(@testing-library/dom@10.4.1)': dependencies: '@testing-library/dom': 10.4.1 @@ -1714,13 +1689,13 @@ snapshots: '@types/estree@1.0.9': {} - '@types/node@26.1.2': + '@types/node@26.2.0': dependencies: undici-types: 8.3.0 '@types/ws@8.18.1': dependencies: - '@types/node': 26.1.2 + '@types/node': 26.2.0 '@typescript/typescript-aix-ppc64@7.0.2': optional: true @@ -1782,29 +1757,29 @@ snapshots: '@typescript/typescript-win32-x64@7.0.2': optional: true - '@vitest/browser-preview@4.1.10(@voidzero-dev/vite-plus-core@0.2.6(@types/node@26.1.2)(typescript@7.0.2)(yaml@2.9.0))(vitest@4.1.10)': + '@vitest/browser-preview@4.1.10(@voidzero-dev/vite-plus-core@0.2.6(@types/node@26.2.0)(typescript@7.0.2))(vitest@4.1.10)': dependencies: '@testing-library/dom': 10.4.1 - '@testing-library/user-event': 14.6.1(@testing-library/dom@10.4.1) - '@vitest/browser': 4.1.10(@voidzero-dev/vite-plus-core@0.2.6(@types/node@26.1.2)(typescript@7.0.2)(yaml@2.9.0))(vitest@4.1.10) - vitest: 4.1.10(@types/node@26.1.2)(@vitest/browser-preview@4.1.10)(@voidzero-dev/vite-plus-core@0.2.6(@types/node@26.1.2)(typescript@7.0.2)(yaml@2.9.0)) + '@testing-library/user-event': 14.6.3(@testing-library/dom@10.4.1) + '@vitest/browser': 4.1.10(@voidzero-dev/vite-plus-core@0.2.6(@types/node@26.2.0)(typescript@7.0.2))(vitest@4.1.10) + vitest: 4.1.10(@types/node@26.2.0)(@vitest/browser-preview@4.1.10)(@voidzero-dev/vite-plus-core@0.2.6(@types/node@26.2.0)(typescript@7.0.2)) transitivePeerDependencies: - bufferutil - msw - utf-8-validate - vite - '@vitest/browser@4.1.10(@voidzero-dev/vite-plus-core@0.2.6(@types/node@26.1.2)(typescript@7.0.2)(yaml@2.9.0))(vitest@4.1.10)': + '@vitest/browser@4.1.10(@voidzero-dev/vite-plus-core@0.2.6(@types/node@26.2.0)(typescript@7.0.2))(vitest@4.1.10)': dependencies: '@blazediff/core': 1.9.1 - '@vitest/mocker': 4.1.10(@voidzero-dev/vite-plus-core@0.2.6(@types/node@26.1.2)(typescript@7.0.2)(yaml@2.9.0)) + '@vitest/mocker': 4.1.10(@voidzero-dev/vite-plus-core@0.2.6(@types/node@26.2.0)(typescript@7.0.2)) '@vitest/utils': 4.1.10 magic-string: 0.30.21 pngjs: 7.0.0 sirv: 3.0.2 - tinyrainbow: 3.1.0 - vitest: 4.1.10(@types/node@26.1.2)(@vitest/browser-preview@4.1.10)(@voidzero-dev/vite-plus-core@0.2.6(@types/node@26.1.2)(typescript@7.0.2)(yaml@2.9.0)) - ws: 8.21.0 + tinyrainbow: 3.1.1 + vitest: 4.1.10(@types/node@26.2.0)(@vitest/browser-preview@4.1.10)(@voidzero-dev/vite-plus-core@0.2.6(@types/node@26.2.0)(typescript@7.0.2)) + ws: 8.21.3 transitivePeerDependencies: - bufferutil - msw @@ -1818,19 +1793,19 @@ snapshots: '@vitest/spy': 4.1.10 '@vitest/utils': 4.1.10 chai: 6.2.2 - tinyrainbow: 3.1.0 + tinyrainbow: 3.1.1 - '@vitest/mocker@4.1.10(@voidzero-dev/vite-plus-core@0.2.6(@types/node@26.1.2)(typescript@7.0.2)(yaml@2.9.0))': + '@vitest/mocker@4.1.10(@voidzero-dev/vite-plus-core@0.2.6(@types/node@26.2.0)(typescript@7.0.2))': dependencies: '@vitest/spy': 4.1.10 estree-walker: 3.0.3 magic-string: 0.30.21 optionalDependencies: - vite: '@voidzero-dev/vite-plus-core@0.2.6(@types/node@26.1.2)(typescript@7.0.2)(yaml@2.9.0)' + vite: '@voidzero-dev/vite-plus-core@0.2.6(@types/node@26.2.0)(typescript@7.0.2)' '@vitest/pretty-format@4.1.10': dependencies: - tinyrainbow: 3.1.0 + tinyrainbow: 3.1.1 '@vitest/runner@4.1.10': dependencies: @@ -1850,21 +1825,20 @@ snapshots: dependencies: '@vitest/pretty-format': 4.1.10 convert-source-map: 2.0.0 - tinyrainbow: 3.1.0 + tinyrainbow: 3.1.1 - '@voidzero-dev/vite-plus-core@0.2.6(@types/node@26.1.2)(typescript@7.0.2)(yaml@2.9.0)': + '@voidzero-dev/vite-plus-core@0.2.6(@types/node@26.2.0)(typescript@7.0.2)': dependencies: '@oxc-project/runtime': 0.141.0 '@oxc-project/types': 0.141.0 lightningcss: 1.33.0 - postcss: 8.5.16 + postcss: 8.5.26 yuku-codegen: 0.5.48 yuku-parser: 0.5.48 optionalDependencies: - '@types/node': 26.1.2 + '@types/node': 26.2.0 fsevents: 2.3.3 typescript: 7.0.2 - yaml: 2.9.0 '@voidzero-dev/vite-plus-darwin-arm64@0.2.6': optional: true @@ -1993,7 +1967,7 @@ snapshots: ansi-styles: 4.3.0 supports-color: 7.2.0 - cluster-key-slot@1.1.2: {} + cluster-key-slot@1.1.1: {} color-convert@1.9.3: dependencies: @@ -2023,20 +1997,15 @@ snapshots: dom-accessibility-api@0.5.16: {} - effect@4.0.0-beta.102: + effect@4.0.0-beta.107: dependencies: '@standard-schema/spec': 1.1.0 fast-check: 4.9.0 - find-my-way-ts: 0.1.6 - ini: 7.0.0 kubernetes-types: 1.30.0 - msgpackr: 2.0.4 - multipasta: 0.2.8 - toml: 4.1.2 + msgpackr: 2.0.5 uuid: 14.0.1 - yaml: 2.9.0 - es-module-lexer@2.3.0: {} + es-module-lexer@2.3.1: {} escape-string-regexp@1.0.5: {} @@ -2048,7 +2017,7 @@ snapshots: fast-check@4.9.0: dependencies: - pure-rand: 8.4.0 + pure-rand: 8.4.2 fast-glob@3.3.3: dependencies: @@ -2070,8 +2039,6 @@ snapshots: dependencies: to-regex-range: 5.0.1 - find-my-way-ts@0.1.6: {} - fs-extra@7.0.1: dependencies: graceful-fs: 4.2.11 @@ -2099,16 +2066,12 @@ snapshots: inherits@2.0.4: {} - ini@7.0.0: {} - - ioredis@5.10.1: + ioredis@5.11.1: dependencies: - '@ioredis/commands': 1.5.1 - cluster-key-slot: 1.1.2 + '@ioredis/commands': 1.10.0 + cluster-key-slot: 1.1.1 debug: 4.4.3 denque: 2.1.0 - lodash.defaults: 4.2.0 - lodash.isarguments: 3.1.0 redis-errors: 1.2.0 redis-parser: 3.0.0 standard-as-callback: 2.1.0 @@ -2195,10 +2158,6 @@ snapshots: lightningcss-win32-arm64-msvc: 1.33.0 lightningcss-win32-x64-msvc: 1.33.0 - lodash.defaults@4.2.0: {} - - lodash.isarguments@3.1.0: {} - lz-string@1.5.0: {} magic-string@0.30.21: @@ -2230,27 +2189,25 @@ snapshots: '@msgpackr-extract/msgpackr-extract-win32-x64': 3.0.4 optional: true - msgpackr@2.0.4: + msgpackr@2.0.5: optionalDependencies: msgpackr-extract: 3.0.4 - multipasta@0.2.8: {} - - nanoid@3.3.15: {} + nanoid@3.3.18: {} needle@3.5.0: dependencies: iconv-lite: 0.6.3 - sax: 1.6.0 + sax: 1.6.1 node-gyp-build-optional-packages@5.2.2: dependencies: detect-libc: 2.1.2 optional: true - obug@2.1.3: {} + obug@2.1.4: {} - oxfmt@0.60.0(vite-plus@0.2.6(@types/node@26.1.2)(@voidzero-dev/vite-plus-core@0.2.6(@types/node@26.1.2)(typescript@7.0.2)(yaml@2.9.0))(typescript@7.0.2)(yaml@2.9.0)): + oxfmt@0.60.0(vite-plus@0.2.6(@types/node@26.2.0)(@voidzero-dev/vite-plus-core@0.2.6(@types/node@26.2.0)(typescript@7.0.2))(typescript@7.0.2)): dependencies: tinypool: 2.1.0 optionalDependencies: @@ -2273,7 +2230,7 @@ snapshots: '@oxfmt/binding-win32-arm64-msvc': 0.60.0 '@oxfmt/binding-win32-ia32-msvc': 0.60.0 '@oxfmt/binding-win32-x64-msvc': 0.60.0 - vite-plus: 0.2.6(@types/node@26.1.2)(@voidzero-dev/vite-plus-core@0.2.6(@types/node@26.1.2)(typescript@7.0.2)(yaml@2.9.0))(typescript@7.0.2)(yaml@2.9.0) + vite-plus: 0.2.6(@types/node@26.2.0)(@voidzero-dev/vite-plus-core@0.2.6(@types/node@26.2.0)(typescript@7.0.2))(typescript@7.0.2) oxlint-tsgolint@7.0.2001: optionalDependencies: @@ -2284,7 +2241,7 @@ snapshots: '@oxlint-tsgolint/win32-arm64': 7.0.2001 '@oxlint-tsgolint/win32-x64': 7.0.2001 - oxlint@1.75.0(oxlint-tsgolint@7.0.2001)(vite-plus@0.2.6(@types/node@26.1.2)(@voidzero-dev/vite-plus-core@0.2.6(@types/node@26.1.2)(typescript@7.0.2)(yaml@2.9.0))(typescript@7.0.2)(yaml@2.9.0)): + oxlint@1.75.0(oxlint-tsgolint@7.0.2001)(vite-plus@0.2.6(@types/node@26.2.0)(@voidzero-dev/vite-plus-core@0.2.6(@types/node@26.2.0)(typescript@7.0.2))(typescript@7.0.2)): optionalDependencies: '@oxlint/binding-android-arm-eabi': 1.75.0 '@oxlint/binding-android-arm64': 1.75.0 @@ -2306,7 +2263,7 @@ snapshots: '@oxlint/binding-win32-ia32-msvc': 1.75.0 '@oxlint/binding-win32-x64-msvc': 1.75.0 oxlint-tsgolint: 7.0.2001 - vite-plus: 0.2.6(@types/node@26.1.2)(@voidzero-dev/vite-plus-core@0.2.6(@types/node@26.1.2)(typescript@7.0.2)(yaml@2.9.0))(typescript@7.0.2)(yaml@2.9.0) + vite-plus: 0.2.6(@types/node@26.2.0)(@voidzero-dev/vite-plus-core@0.2.6(@types/node@26.2.0)(typescript@7.0.2))(typescript@7.0.2) pako@1.0.11: {} @@ -2320,9 +2277,9 @@ snapshots: pngjs@7.0.0: {} - postcss@8.5.16: + postcss@8.5.26: dependencies: - nanoid: 3.3.15 + nanoid: 3.3.18 picocolors: 1.1.1 source-map-js: 1.2.1 @@ -2334,7 +2291,7 @@ snapshots: process-nextick-args@2.0.1: {} - pure-rand@8.4.0: {} + pure-rand@8.4.2: {} queue-microtask@1.2.3: {} @@ -2381,7 +2338,7 @@ snapshots: safer-buffer@2.1.2: {} - sax@1.6.0: {} + sax@1.6.1: {} semver@7.8.5: {} @@ -2417,7 +2374,7 @@ snapshots: tinybench@2.9.0: {} - tinyexec@1.2.4: {} + tinyexec@1.3.0: {} tinyglobby@0.2.17: dependencies: @@ -2426,14 +2383,12 @@ snapshots: tinypool@2.1.0: {} - tinyrainbow@3.1.0: {} + tinyrainbow@3.1.1: {} to-regex-range@5.0.1: dependencies: is-number: 7.0.0 - toml@4.1.2: {} - totalist@3.0.1: {} typescript@7.0.2: @@ -2461,7 +2416,7 @@ snapshots: undici-types@8.3.0: {} - undici@8.7.0: {} + undici@8.10.0: {} universalify@0.1.2: {} @@ -2469,24 +2424,24 @@ snapshots: uuid@14.0.1: {} - vite-plus@0.2.6(@types/node@26.1.2)(@voidzero-dev/vite-plus-core@0.2.6(@types/node@26.1.2)(typescript@7.0.2)(yaml@2.9.0))(typescript@7.0.2)(yaml@2.9.0): + vite-plus@0.2.6(@types/node@26.2.0)(@voidzero-dev/vite-plus-core@0.2.6(@types/node@26.2.0)(typescript@7.0.2))(typescript@7.0.2): dependencies: '@oxc-project/types': 0.141.0 '@oxlint/plugins': 1.73.0 - '@vitest/browser': 4.1.10(@voidzero-dev/vite-plus-core@0.2.6(@types/node@26.1.2)(typescript@7.0.2)(yaml@2.9.0))(vitest@4.1.10) - '@vitest/browser-preview': 4.1.10(@voidzero-dev/vite-plus-core@0.2.6(@types/node@26.1.2)(typescript@7.0.2)(yaml@2.9.0))(vitest@4.1.10) + '@vitest/browser': 4.1.10(@voidzero-dev/vite-plus-core@0.2.6(@types/node@26.2.0)(typescript@7.0.2))(vitest@4.1.10) + '@vitest/browser-preview': 4.1.10(@voidzero-dev/vite-plus-core@0.2.6(@types/node@26.2.0)(typescript@7.0.2))(vitest@4.1.10) '@vitest/expect': 4.1.10 - '@vitest/mocker': 4.1.10(@voidzero-dev/vite-plus-core@0.2.6(@types/node@26.1.2)(typescript@7.0.2)(yaml@2.9.0)) + '@vitest/mocker': 4.1.10(@voidzero-dev/vite-plus-core@0.2.6(@types/node@26.2.0)(typescript@7.0.2)) '@vitest/pretty-format': 4.1.10 '@vitest/runner': 4.1.10 '@vitest/snapshot': 4.1.10 '@vitest/spy': 4.1.10 '@vitest/utils': 4.1.10 - '@voidzero-dev/vite-plus-core': 0.2.6(@types/node@26.1.2)(typescript@7.0.2)(yaml@2.9.0) - oxfmt: 0.60.0(vite-plus@0.2.6(@types/node@26.1.2)(@voidzero-dev/vite-plus-core@0.2.6(@types/node@26.1.2)(typescript@7.0.2)(yaml@2.9.0))(typescript@7.0.2)(yaml@2.9.0)) - oxlint: 1.75.0(oxlint-tsgolint@7.0.2001)(vite-plus@0.2.6(@types/node@26.1.2)(@voidzero-dev/vite-plus-core@0.2.6(@types/node@26.1.2)(typescript@7.0.2)(yaml@2.9.0))(typescript@7.0.2)(yaml@2.9.0)) + '@voidzero-dev/vite-plus-core': 0.2.6(@types/node@26.2.0)(typescript@7.0.2) + oxfmt: 0.60.0(vite-plus@0.2.6(@types/node@26.2.0)(@voidzero-dev/vite-plus-core@0.2.6(@types/node@26.2.0)(typescript@7.0.2))(typescript@7.0.2)) + oxlint: 1.75.0(oxlint-tsgolint@7.0.2001)(vite-plus@0.2.6(@types/node@26.2.0)(@voidzero-dev/vite-plus-core@0.2.6(@types/node@26.2.0)(typescript@7.0.2))(typescript@7.0.2)) oxlint-tsgolint: 7.0.2001 - vitest: 4.1.10(@types/node@26.1.2)(@vitest/browser-preview@4.1.10)(@voidzero-dev/vite-plus-core@0.2.6(@types/node@26.1.2)(typescript@7.0.2)(yaml@2.9.0)) + vitest: 4.1.10(@types/node@26.2.0)(@vitest/browser-preview@4.1.10)(@voidzero-dev/vite-plus-core@0.2.6(@types/node@26.2.0)(typescript@7.0.2)) optionalDependencies: '@voidzero-dev/vite-plus-darwin-arm64': 0.2.6 '@voidzero-dev/vite-plus-darwin-x64': 0.2.6 @@ -2527,31 +2482,31 @@ snapshots: - vite - yaml - vitest@4.1.10(@types/node@26.1.2)(@vitest/browser-preview@4.1.10)(@voidzero-dev/vite-plus-core@0.2.6(@types/node@26.1.2)(typescript@7.0.2)(yaml@2.9.0)): + vitest@4.1.10(@types/node@26.2.0)(@vitest/browser-preview@4.1.10)(@voidzero-dev/vite-plus-core@0.2.6(@types/node@26.2.0)(typescript@7.0.2)): dependencies: '@vitest/expect': 4.1.10 - '@vitest/mocker': 4.1.10(@voidzero-dev/vite-plus-core@0.2.6(@types/node@26.1.2)(typescript@7.0.2)(yaml@2.9.0)) + '@vitest/mocker': 4.1.10(@voidzero-dev/vite-plus-core@0.2.6(@types/node@26.2.0)(typescript@7.0.2)) '@vitest/pretty-format': 4.1.10 '@vitest/runner': 4.1.10 '@vitest/snapshot': 4.1.10 '@vitest/spy': 4.1.10 '@vitest/utils': 4.1.10 - es-module-lexer: 2.3.0 + es-module-lexer: 2.3.1 expect-type: 1.4.0 magic-string: 0.30.21 - obug: 2.1.3 + obug: 2.1.4 pathe: 2.0.3 picomatch: 4.0.5 std-env: 4.2.0 tinybench: 2.9.0 - tinyexec: 1.2.4 + tinyexec: 1.3.0 tinyglobby: 0.2.17 - tinyrainbow: 3.1.0 - vite: '@voidzero-dev/vite-plus-core@0.2.6(@types/node@26.1.2)(typescript@7.0.2)(yaml@2.9.0)' + tinyrainbow: 3.1.1 + vite: '@voidzero-dev/vite-plus-core@0.2.6(@types/node@26.2.0)(typescript@7.0.2)' why-is-node-running: 2.3.0 optionalDependencies: - '@types/node': 26.1.2 - '@vitest/browser-preview': 4.1.10(@voidzero-dev/vite-plus-core@0.2.6(@types/node@26.1.2)(typescript@7.0.2)(yaml@2.9.0))(vitest@4.1.10) + '@types/node': 26.2.0 + '@vitest/browser-preview': 4.1.10(@voidzero-dev/vite-plus-core@0.2.6(@types/node@26.2.0)(typescript@7.0.2))(vitest@4.1.10) transitivePeerDependencies: - msw @@ -2560,17 +2515,15 @@ snapshots: siginfo: 2.0.0 stackback: 0.0.2 - ws@8.21.0: {} + ws@8.21.3: {} xml2js@0.5.0: dependencies: - sax: 1.6.0 + sax: 1.6.1 xmlbuilder: 11.0.1 xmlbuilder@11.0.1: {} - yaml@2.9.0: {} - yuku-codegen@0.5.48: dependencies: '@yuku-toolchain/types': 0.5.43 diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index 60f341c..fe138fc 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -13,6 +13,10 @@ minimumReleaseAgeExclude: - "@effect/platform-node@4.0.0-beta.78" - "@effect/vitest@4.0.0-beta.78" - effect@4.0.0-beta.78 + - "@effect/platform-node-shared@4.0.0-beta.107" + - "@effect/platform-node@4.0.0-beta.107" + - "@effect/vitest@4.0.0-beta.107" + - effect@4.0.0-beta.107 catalog: vite: npm:@voidzero-dev/vite-plus-core@0.2.6 vitest: 4.1.10 diff --git a/scripts/prepare-effect.sh b/scripts/prepare-effect.sh index 27d30e1..135333d 100755 --- a/scripts/prepare-effect.sh +++ b/scripts/prepare-effect.sh @@ -7,8 +7,17 @@ if [ -n "${CI:-}" ]; then fi effect_upstream="https://github.com/Effect-TS/effect.git" -effect_ref="effect@4.0.0-beta.102" script_dir="$(CDPATH= cd -- "$(dirname -- "$0")" && pwd)" +effect_ref="$(node -e ' +const { readFileSync } = require("node:fs"); +const packageJson = JSON.parse(readFileSync(process.argv[1], "utf8")); +const version = packageJson.dependencies?.effect; +if (typeof version !== "string" || !/^\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?$/.test(version)) { + console.error("package.json dependencies.effect must be an exact version"); + process.exit(1); +} +process.stdout.write(`effect@${version}`); +' "$script_dir/../package.json")" repo_dir="$(dirname "$script_dir")/.repos/effect" if [ -e "$repo_dir" ] && [ ! -d "$repo_dir/.git" ]; then diff --git a/src/cli-artifact-commands.ts b/src/cli-artifact-commands.ts index d891294..8d0af2b 100644 --- a/src/cli-artifact-commands.ts +++ b/src/cli-artifact-commands.ts @@ -68,11 +68,11 @@ const packageOutputPath = ( out: string | undefined, ): Effect.Effect => { if (positional !== undefined && out !== undefined) { - return Effect.fail(InvalidInput.make({ message: packageUsage })); + return Effect.fail(new InvalidInput({ message: packageUsage })); } const outputPath = positional ?? out; return outputPath === undefined - ? Effect.fail(InvalidInput.make({ message: packageUsage })) + ? Effect.fail(new InvalidInput({ message: packageUsage })) : Effect.succeed(outputPath); }; diff --git a/src/cli-command.ts b/src/cli-command.ts index e703139..9ad6e3d 100644 --- a/src/cli-command.ts +++ b/src/cli-command.ts @@ -79,7 +79,7 @@ export const parseEffectCliEffect: ( yield* runRokitCli(argv, capture, silentConsole).pipe(Effect.mapError(normalizeCliError)); if (parsedCli === undefined) { - return yield* Effect.fail(InvalidInput.make({ message: "No command parsed" })); + return yield* Effect.fail(new InvalidInput({ message: "No command parsed" })); } return parsedCli; @@ -112,7 +112,7 @@ const runRokitCli = ( const normalizeCliError = (error: unknown): RokitError => { if (CliError.isCliError(error) && error._tag === "ShowHelp") { - return InvalidInput.make({ message: formatShowHelpError(error) }); + return new InvalidInput({ message: formatShowHelpError(error) }); } return normalizeError(error); diff --git a/src/cli-input-json-read.ts b/src/cli-input-json-read.ts index ef19264..5f1de0a 100644 --- a/src/cli-input-json-read.ts +++ b/src/cli-input-json-read.ts @@ -36,7 +36,7 @@ export const parseJsonRecordEffect: ( ) => Effect.Effect = Effect.fn("parseJsonRecord")(function* (value) { const parsed = yield* Effect.try({ try: (): unknown => JSON.parse(value), - catch: () => InvalidInput.make({ message: "input JSON must be valid JSON" }), + catch: () => new InvalidInput({ message: "input JSON must be valid JSON" }), }); return yield* readRecordEffect(parsed, "input JSON must be an object"); @@ -54,8 +54,8 @@ export const readStringEffect: ( } return yield* Schema.decodeUnknownEffect(NonEmptyString)(value).pipe( - Effect.mapError(() => - InvalidInput.make({ message: `input JSON is missing string field: ${key}` }), + Effect.mapError( + () => new InvalidInput({ message: `input JSON is missing string field: ${key}` }), ), ); }, @@ -73,8 +73,8 @@ export const readOptionalStringEffect: ( } return yield* Schema.decodeUnknownEffect(Schema.String)(value).pipe( - Effect.mapError(() => - InvalidInput.make({ message: `input JSON field must be a string: ${key}` }), + Effect.mapError( + () => new InvalidInput({ message: `input JSON field must be a string: ${key}` }), ), ); }, @@ -92,8 +92,8 @@ export const readOptionalBooleanEffect: ( } return yield* Schema.decodeUnknownEffect(Schema.Boolean)(value).pipe( - Effect.mapError(() => - InvalidInput.make({ message: `input JSON field must be a boolean: ${key}` }), + Effect.mapError( + () => new InvalidInput({ message: `input JSON field must be a boolean: ${key}` }), ), ); }, @@ -111,8 +111,8 @@ export const readOptionalNumberEffect: ( } return yield* Schema.decodeUnknownEffect(PositiveInteger)(value).pipe( - Effect.mapError(() => - InvalidInput.make({ message: `input JSON field must be a positive integer: ${key}` }), + Effect.mapError( + () => new InvalidInput({ message: `input JSON field must be a positive integer: ${key}` }), ), ); }, @@ -131,8 +131,9 @@ export const readOptionalNonNegativeNumberEffect: ( } return yield* Schema.decodeUnknownEffect(NonNegativeInteger)(value).pipe( - Effect.mapError(() => - InvalidInput.make({ message: `input JSON field must be a non-negative integer: ${key}` }), + Effect.mapError( + () => + new InvalidInput({ message: `input JSON field must be a non-negative integer: ${key}` }), ), ); }); @@ -143,8 +144,8 @@ export const readStringArrayEffect: ( ) => Effect.Effect = Effect.fn("readInputJsonStringArray")( function* (record, key) { return yield* Schema.decodeUnknownEffect(StringArray)(record[key]).pipe( - Effect.mapError(() => - InvalidInput.make({ message: `input JSON field must be a string array: ${key}` }), + Effect.mapError( + () => new InvalidInput({ message: `input JSON field must be a string array: ${key}` }), ), ); }, @@ -163,8 +164,8 @@ export const readOptionalStringArrayEffect: ( } return yield* Schema.decodeUnknownEffect(StringArray)(value).pipe( - Effect.mapError(() => - InvalidInput.make({ message: `input JSON field must be a string array: ${key}` }), + Effect.mapError( + () => new InvalidInput({ message: `input JSON field must be a string array: ${key}` }), ), ); }); @@ -222,8 +223,8 @@ export const readNodeExpectationEffect: ( if ((state === "visible" || state === "hidden" || state === "absent") && text !== undefined) { yield* Schema.decodeUnknownEffect(Schema.String)(text).pipe( - Effect.mapError(() => - InvalidInput.make({ message: "input JSON node expectation text must be a string" }), + Effect.mapError( + () => new InvalidInput({ message: "input JSON node expectation text must be a string" }), ), ); } @@ -238,7 +239,7 @@ const readRecordEffect: ( ) => Effect.Effect = Effect.fn("readInputJsonRecord")( function* (value, message) { return yield* Schema.decodeUnknownEffect(InputJsonRecord)(value).pipe( - Effect.mapError(() => InvalidInput.make({ message })), + Effect.mapError(() => new InvalidInput({ message })), ); }, ); diff --git a/src/cli-input-source.ts b/src/cli-input-source.ts index d45ca97..241d895 100644 --- a/src/cli-input-source.ts +++ b/src/cli-input-source.ts @@ -15,7 +15,7 @@ export const resolveInputJsonSource: ( if (path.length === 0) { return yield* Effect.fail( - InvalidInput.make({ message: "--input-json @file requires a file path" }), + new InvalidInput({ message: "--input-json @file requires a file path" }), ); } diff --git a/src/cli-node-condition.ts b/src/cli-node-condition.ts index b694ccc..daff114 100644 --- a/src/cli-node-condition.ts +++ b/src/cli-node-condition.ts @@ -43,7 +43,7 @@ export const optionalNodeConditionFromCliInputEffect: ( if (parsedNodeName === undefined) { if (parsedState !== undefined || parsedValue !== undefined || parsedTimeoutMs !== undefined) { return yield* Effect.fail( - InvalidInput.make({ message: `usage: rokit ${commandName} ` }), + new InvalidInput({ message: `usage: rokit ${commandName} ` }), ); } diff --git a/src/cli-runner.ts b/src/cli-runner.ts index fc5f0c9..e12e084 100644 --- a/src/cli-runner.ts +++ b/src/cli-runner.ts @@ -50,7 +50,7 @@ export const runCommandEffect: ( if (data === undefined) { return yield* Effect.fail( - InvalidInput.make({ message: `Unknown described command: ${command.commandName ?? ""}` }), + new InvalidInput({ message: `Unknown described command: ${command.commandName ?? ""}` }), ); } @@ -297,7 +297,7 @@ export const runCommandEffect: ( return { command: command.name, data: { message }, message, status: "ok" }; } - return yield* Effect.fail(InvalidInput.make({ message: "unsupported command" })); + return yield* Effect.fail(new InvalidInput({ message: "unsupported command" })); }); export const runCommand = async ( diff --git a/src/cli.ts b/src/cli.ts index f028750..be1fd66 100644 --- a/src/cli.ts +++ b/src/cli.ts @@ -110,7 +110,7 @@ const parseCommandEffect: ( )(function* (parsed) { if (parsed.inputJson !== undefined && parsed.command !== undefined) { return yield* Effect.fail( - InvalidInput.make({ message: "usage: rokit --input-json " }), + new InvalidInput({ message: "usage: rokit --input-json " }), ); } @@ -120,7 +120,7 @@ const parseCommandEffect: ( } if (parsed.command === undefined) { - return yield* Effect.fail(InvalidInput.make({ message: "No command parsed" })); + return yield* Effect.fail(new InvalidInput({ message: "No command parsed" })); } return parsed.command; diff --git a/src/debug.ts b/src/debug.ts index bc8a415..aa3250f 100644 --- a/src/debug.ts +++ b/src/debug.ts @@ -1,5 +1,5 @@ import { createConnection } from "node:net"; -import { Effect } from "effect"; +import { Clock, Effect } from "effect"; import { DebugPortUnavailable, normalizeError, type RokitError } from "./errors.js"; import { fail, formatErrorMessage, rejectUnsafeInput } from "./runtime.js"; import type { RokuContext } from "./roku-context.js"; @@ -110,14 +110,14 @@ export const runDebugCommandEffect: ( idleTimeoutMs: number, ) { const safeCommand = yield* buildDebugCommandEffect(command.command, command.args); - const startedAt = yield* Effect.sync(() => Date.now()); + const startedAt = yield* Clock.currentTimeMillis; const port = resolveDebugPort(context, safeCommand.port); const body = yield* readDebugSocketEffect(context, port, { durationMs, idleTimeoutMs, request: safeCommand.request, }); - const elapsedMs = yield* Effect.sync(() => Date.now() - startedAt); + const elapsedMs = (yield* Clock.currentTimeMillis) - startedAt; return { args: safeCommand.args, @@ -134,10 +134,10 @@ export const captureDebugConsoleEffect: ( durationMs: number, ) => Effect.Effect = Effect.fn("captureDebugConsole")( function* (context: RokuContext, durationMs: number) { - const startedAt = yield* Effect.sync(() => Date.now()); + const startedAt = yield* Clock.currentTimeMillis; const port = resolveDebugPort(context, brightScriptConsolePort); const body = yield* readDebugSocketEffect(context, port, { durationMs }); - const elapsedMs = yield* Effect.sync(() => Date.now() - startedAt); + const elapsedMs = (yield* Clock.currentTimeMillis) - startedAt; return { body, @@ -220,7 +220,7 @@ const readDebugSocketEffect = Effect.fn("readDebugSocket")(function* ( }; const failRead = (detail: string): void => { - complete(Effect.fail(DebugPortUnavailable.make({ detail, port }))); + complete(Effect.fail(new DebugPortUnavailable({ detail, port }))); }; const scheduleIdleTimer = (): void => { diff --git a/src/device.ts b/src/device.ts index bfe4b7e..3a5e7a4 100644 --- a/src/device.ts +++ b/src/device.ts @@ -4,6 +4,7 @@ import type { DeviceInfo } from "roku-deploy"; import { ecpPort, fetchEcpTextEffect } from "./ecp.js"; import { normalizeError } from "./errors.js"; import type { RokuContext } from "./roku-context.js"; +import { abortSignalWithTimeout } from "./timing.js"; import { readXmlTag } from "./xml.js"; export type DeviceSummary = { @@ -54,9 +55,9 @@ const fetchInstallerStatusEffect = Effect.fn("fetchInstallerStatus")(function* ( ) { const response = yield* Effect.tryPromise({ catch: normalizeError, - try: () => + try: (signal) => fetch(`http://${context.target}`, { - signal: AbortSignal.timeout(context.timeoutMs), + signal: abortSignalWithTimeout(signal, context.timeoutMs), }), }); diff --git a/src/discovery.ts b/src/discovery.ts index 52f7e79..6882671 100644 --- a/src/discovery.ts +++ b/src/discovery.ts @@ -36,7 +36,7 @@ const validateDiscoveryTimeoutEffect = Effect.fn("validateDiscoveryTimeout")(fun ) { if (!Number.isInteger(timeoutMs) || timeoutMs <= 0) { return yield* Effect.fail( - InvalidInput.make({ message: `Invalid discovery timeout: ${timeoutMs}` }), + new InvalidInput({ message: `Invalid discovery timeout: ${timeoutMs}` }), ); } diff --git a/src/ecp.ts b/src/ecp.ts index f60d248..310bd89 100644 --- a/src/ecp.ts +++ b/src/ecp.ts @@ -1,5 +1,6 @@ import { Effect, Schema } from "effect"; import type { RokuContext } from "./roku-context.js"; +import { abortSignalWithTimeout } from "./timing.js"; export const ecpPort = 8060; @@ -36,7 +37,7 @@ const remoteKeySet: ReadonlySet = new Set(remoteKeys); export type RemoteKey = (typeof remoteKeys)[number] | `Lit_${string}`; -class EcpTransportError extends Schema.TaggedErrorClass()("EcpTransportError", { +class EcpTransportError extends Schema.TaggedError()("EcpTransportError", { detail: Schema.String, method: Schema.String, path: Schema.String, @@ -46,7 +47,7 @@ class EcpTransportError extends Schema.TaggedErrorClass()("Ec } } -class EcpHttpError extends Schema.TaggedErrorClass()("EcpHttpError", { +class EcpHttpError extends Schema.TaggedError()("EcpHttpError", { method: Schema.String, path: Schema.String, status: Schema.Number, @@ -56,7 +57,7 @@ class EcpHttpError extends Schema.TaggedErrorClass()("EcpHttpError } } -class EcpPathError extends Schema.TaggedErrorClass()("EcpPathError", { +class EcpPathError extends Schema.TaggedError()("EcpPathError", { detail: Schema.String, path: Schema.String, }) { @@ -74,21 +75,21 @@ export const fetchEcpTextEffect = Effect.fn("fetchEcpText")(function* ( const safePath = yield* safeDeviceRelativePath(path); const url = ecpUrl(context, safePath); const response = yield* Effect.tryPromise({ - try: () => fetch(url, { signal: AbortSignal.timeout(context.timeoutMs) }), + try: (signal) => fetch(url, { signal: abortSignalWithTimeout(signal, context.timeoutMs) }), catch: (error) => - EcpTransportError.make({ detail: formatErrorMessage(error), method: "GET", path: safePath }), + new EcpTransportError({ detail: formatErrorMessage(error), method: "GET", path: safePath }), }); if (!response.ok) { return yield* Effect.fail( - EcpHttpError.make({ method: "GET", path: safePath, status: response.status }), + new EcpHttpError({ method: "GET", path: safePath, status: response.status }), ); } return yield* Effect.tryPromise({ try: () => response.text(), catch: (error) => - EcpTransportError.make({ detail: formatErrorMessage(error), method: "GET", path: safePath }), + new EcpTransportError({ detail: formatErrorMessage(error), method: "GET", path: safePath }), }); }); @@ -103,17 +104,17 @@ const postEcpUrlEffect = Effect.fn("postEcpUrl")(function* ( path: string, ) { const response = yield* Effect.tryPromise({ - try: () => + try: (signal) => fetch(url, { method: "POST", - signal: AbortSignal.timeout(context.timeoutMs), + signal: abortSignalWithTimeout(signal, context.timeoutMs), }), catch: (error) => - EcpTransportError.make({ detail: formatErrorMessage(error), method: "POST", path }), + new EcpTransportError({ detail: formatErrorMessage(error), method: "POST", path }), }); if (!response.ok) { - return yield* Effect.fail(EcpHttpError.make({ method: "POST", path, status: response.status })); + return yield* Effect.fail(new EcpHttpError({ method: "POST", path, status: response.status })); } }); @@ -128,13 +129,13 @@ export const postKeypressEffect = Effect.fn("postKeypress")(function* ( export const postLaunchEffect = Effect.fn("postLaunch")(function* (context: RokuContext, url: URL) { const response = yield* Effect.tryPromise({ - try: () => + try: (signal) => fetch(url, { method: "POST", - signal: AbortSignal.timeout(context.timeoutMs), + signal: abortSignalWithTimeout(signal, context.timeoutMs), }), catch: (error) => - EcpTransportError.make({ + new EcpTransportError({ detail: formatErrorMessage(error), method: "POST", path: url.pathname, @@ -152,7 +153,7 @@ export const postLaunchEffect = Effect.fn("postLaunch")(function* (context: Roku } return yield* Effect.fail( - EcpHttpError.make({ method: "POST", path: url.pathname, status: response.status }), + new EcpHttpError({ method: "POST", path: url.pathname, status: response.status }), ); }); @@ -180,7 +181,7 @@ const safeDeviceRelativePath = (path: string): Effect.Effect EcpPathError.make({ detail: formatErrorMessage(error), path }), + catch: (error) => new EcpPathError({ detail: formatErrorMessage(error), path }), }); const remoteKeyEffect = (key: string): Effect.Effect => @@ -190,7 +191,7 @@ const remoteKeyEffect = (key: string): Effect.Effect => return key; }, catch: (error) => - EcpPathError.make({ detail: formatErrorMessage(error), path: `/keypress/${key}` }), + new EcpPathError({ detail: formatErrorMessage(error), path: `/keypress/${key}` }), }); export const validateEcpPath = (path: string): string => { diff --git a/src/errors.ts b/src/errors.ts index 3029aff..f55850a 100644 --- a/src/errors.ts +++ b/src/errors.ts @@ -1,25 +1,22 @@ import { Schema } from "effect"; -export class InvalidInput extends Schema.TaggedErrorClass()("InvalidInput", { +export class InvalidInput extends Schema.TaggedError()("InvalidInput", { message: Schema.String, }) {} -export class MissingTarget extends Schema.TaggedErrorClass()("MissingTarget", {}) { +export class MissingTarget extends Schema.TaggedError()("MissingTarget", {}) { override get message(): string { return "ROKIT_TARGET is not set"; } } -export class MissingPassword extends Schema.TaggedErrorClass()( - "MissingPassword", - {}, -) { +export class MissingPassword extends Schema.TaggedError()("MissingPassword", {}) { override get message(): string { return "ROKIT_PASSWORD is not set"; } } -export class DebugPortUnavailable extends Schema.TaggedErrorClass()( +export class DebugPortUnavailable extends Schema.TaggedError()( "DebugPortUnavailable", { detail: Schema.String, @@ -31,7 +28,7 @@ export class DebugPortUnavailable extends Schema.TaggedErrorClass()( +export class UnexpectedRokitFailure extends Schema.TaggedError()( "UnexpectedRokitFailure", { message: Schema.String, @@ -52,7 +49,7 @@ export const normalizeError = (error: unknown): RokitError => { return error; } - return UnexpectedRokitFailure.make({ + return new UnexpectedRokitFailure({ message: error instanceof Error ? error.message : String(error), }); }; diff --git a/src/installer.ts b/src/installer.ts index ff55908..b1227f6 100644 --- a/src/installer.ts +++ b/src/installer.ts @@ -9,13 +9,14 @@ import { readInstallerMessage, } from "./installer-message.js"; import type { RokuContext } from "./roku-context.js"; +import { abortSignalWithTimeout } from "./timing.js"; type InstallerAction = "delete" | "install"; type InstallerSubmitValue = "Delete" | "Install" | "Replace"; const nodeInstallerLayer = Layer.merge(NodeFileSystem.layer, NodePath.layer); -class RokuInstallerTransportError extends Schema.TaggedErrorClass()( +class RokuInstallerTransportError extends Schema.TaggedError()( "RokuInstallerTransportError", { action: Schema.String, @@ -27,7 +28,7 @@ class RokuInstallerTransportError extends Schema.TaggedErrorClass()( +class RokuInstallerHttpError extends Schema.TaggedError()( "RokuInstallerHttpError", { action: Schema.String, @@ -52,11 +53,12 @@ export const installPackageEffect: ( const path = yield* Path.Path; const resolvedZip = withPackageArchiveExtension(path.resolve(zipPath)); const archive = yield* fs.readFile(resolvedZip).pipe( - Effect.mapError((error) => - RokuInstallerTransportError.make({ - action: "install", - detail: formatErrorMessage(error), - }), + Effect.mapError( + (error) => + new RokuInstallerTransportError({ + action: "install", + detail: formatErrorMessage(error), + }), ), ); const fileName = path.basename(resolvedZip); @@ -87,7 +89,7 @@ export const deleteInstalledChannelEffect: ( } return yield* Effect.fail( - RokuInstallerHttpError.make({ + new RokuInstallerHttpError({ action: "delete", detail: message, status: 200, @@ -119,17 +121,17 @@ const postInstallerForm = Effect.fn("postInstallerForm")(function* ( const challenge = yield* readDigestChallenge(context, action, url); const authorization = yield* digestAuthHeaderEffect(context, "POST", url.pathname, challenge); const response = yield* Effect.tryPromise({ - try: () => + try: (signal) => fetch(url, { body: makeBody(), headers: { Authorization: authorization, }, method: "POST", - signal: AbortSignal.timeout(installerPostTimeoutMs(context, action)), + signal: abortSignalWithTimeout(signal, installerPostTimeoutMs(context, action)), }), catch: (error) => - RokuInstallerTransportError.make({ + new RokuInstallerTransportError({ action, detail: formatErrorMessage(error), }), @@ -137,7 +139,7 @@ const postInstallerForm = Effect.fn("postInstallerForm")(function* ( const responseBody = yield* Effect.tryPromise({ try: () => response.text(), catch: (error) => - RokuInstallerTransportError.make({ + new RokuInstallerTransportError({ action, detail: formatErrorMessage(error), }), @@ -146,7 +148,7 @@ const postInstallerForm = Effect.fn("postInstallerForm")(function* ( if (!response.ok) { return yield* Effect.fail( - RokuInstallerHttpError.make({ + new RokuInstallerHttpError({ action, detail: message === "OK" ? fallbackHttpDetail(responseBody, response.statusText) : message, status: response.status, @@ -163,9 +165,13 @@ const readDigestChallenge = Effect.fn("readDigestChallenge")(function* ( url: URL, ) { const response = yield* Effect.tryPromise({ - try: () => fetch(url, { method: "GET", signal: AbortSignal.timeout(context.timeoutMs) }), + try: (signal) => + fetch(url, { + method: "GET", + signal: abortSignalWithTimeout(signal, context.timeoutMs), + }), catch: (error) => - RokuInstallerTransportError.make({ + new RokuInstallerTransportError({ action, detail: formatErrorMessage(error), }), @@ -174,7 +180,7 @@ const readDigestChallenge = Effect.fn("readDigestChallenge")(function* ( if (challenge === undefined) { return yield* Effect.fail( - RokuInstallerTransportError.make({ + new RokuInstallerTransportError({ action, detail: "Roku developer installer did not provide a Digest auth challenge", }), @@ -211,7 +217,7 @@ const postValidatedInstallPackage = Effect.fn("postValidatedInstallPackage")(fun } return yield* Effect.fail( - RokuInstallerHttpError.make({ + new RokuInstallerHttpError({ action: "install", detail: message, status: 200, diff --git a/src/package-zip.ts b/src/package-zip.ts index f6dcfa0..b19ce34 100644 --- a/src/package-zip.ts +++ b/src/package-zip.ts @@ -38,7 +38,7 @@ export type PackageZipResult = { readonly path: string; }; -class PackageZipError extends Schema.TaggedErrorClass()("PackageZipError", { +class PackageZipError extends Schema.TaggedError()("PackageZipError", { detail: Schema.String, }) { override get message(): string { @@ -73,7 +73,7 @@ export const packageChannelEffect = Effect.fn("packageChannel")(function* ( yield* Effect.tryPromise({ try: () => rokuDeploy.createPackage(options), catch: (error) => - PackageZipError.make({ + new PackageZipError({ detail: `failed to package Roku channel: ${formatErrorMessage(error)}`, }), }); @@ -165,15 +165,23 @@ export const createPackageZipEffect = Effect.fn("createPackageZip")(function* ( type: "uint8array", }), catch: (error) => - PackageZipError.make({ + new PackageZipError({ detail: `failed to generate package zip: ${formatErrorMessage(error)}`, }), }); yield* fs.makeDirectory(path.dirname(outFile), { recursive: true }); yield* assertPackageOutputFile(outFile); - yield* fs.remove(outFile, { force: true }); - yield* fs.writeFile(outFile, output); + yield* Effect.scoped( + Effect.gen(function* () { + const tempFile = yield* fs.makeTempFileScoped({ + directory: path.dirname(outFile), + prefix: `.${path.basename(outFile)}-`, + }); + yield* fs.writeFile(tempFile, output); + yield* fs.rename(tempFile, outFile); + }), + ); return { fileCount: sortedFiles.length, @@ -289,7 +297,7 @@ const rejectPackageSymlink = Effect.fn("rejectPackageSymlink")(function* (absolu } return yield* Effect.fail( - PackageZipError.make({ detail: `package path must not be a symlink: ${absolutePath}` }), + new PackageZipError({ detail: `package path must not be a symlink: ${absolutePath}` }), ); }); @@ -307,7 +315,7 @@ const assertInsidePackageRoot = Effect.fn("assertInsidePackageRoot")(function* ( } return yield* Effect.fail( - PackageZipError.make({ detail: `package path resolves outside root: ${absolutePath}` }), + new PackageZipError({ detail: `package path resolves outside root: ${absolutePath}` }), ); }); @@ -326,7 +334,7 @@ const assertPackageOutputFile = Effect.fn("assertPackageOutputFile")(function* ( } return yield* Effect.fail( - PackageZipError.make({ + new PackageZipError({ detail: `package output path already exists and is not a file: ${outputPath}`, }), ); @@ -344,7 +352,7 @@ const normalizePackagePath = Effect.fn("normalizePackagePath")(function* (packag segments.some((segment) => segment === "" || segment === "." || segment === "..") ) { return yield* Effect.fail( - PackageZipError.make({ detail: `unsafe package path: ${packagePath}` }), + new PackageZipError({ detail: `unsafe package path: ${packagePath}` }), ); } @@ -359,7 +367,7 @@ const rejectUnsafeInput = Effect.fn("rejectUnsafeInput")(function* (value: strin }) ) { return yield* Effect.fail( - PackageZipError.make({ detail: `${label} contains control characters` }), + new PackageZipError({ detail: `${label} contains control characters` }), ); } }); @@ -385,7 +393,7 @@ const resolvePackageZipOutputPath = Effect.fn("resolvePackageZipOutputPath")(fun const extension = path.extname(resolvedOutput).toLowerCase(); if (!allowedExtensions.includes(extension)) { return yield* Effect.fail( - PackageZipError.make({ + new PackageZipError({ detail: `package output path must end with ${formatAllowedExtensions(allowedExtensions)}: ${outputPath}`, }), ); @@ -411,7 +419,7 @@ const resolvePackageZipOutputPath = Effect.fn("resolvePackageZipOutputPath")(fun if (!realOutputIsInsideSandbox) { return yield* Effect.fail( - PackageZipError.make({ + new PackageZipError({ detail: `package output path must stay within the app root: ${outputPath}`, }), ); @@ -484,7 +492,7 @@ const rejectPackageOutputInsideRoots = Effect.fn("rejectPackageOutputInsideRoots if (includedByRules === true) { return yield* Effect.fail( - PackageZipError.make({ + new PackageZipError({ detail: `package output path must be outside packaged roots: ${outputPath}`, }), ); @@ -502,7 +510,7 @@ const rejectPackageOutputInsideRoots = Effect.fn("rejectPackageOutputInsideRoots for (const packageRoot of packageRoots.included) { if (packageRoot === "") { return yield* Effect.fail( - PackageZipError.make({ + new PackageZipError({ detail: `package output path must be outside packaged roots: ${outputPath}`, }), ); @@ -510,7 +518,7 @@ const rejectPackageOutputInsideRoots = Effect.fn("rejectPackageOutputInsideRoots if (isInsidePackageRoots(relativeOutput, [packageRoot])) { return yield* Effect.fail( - PackageZipError.make({ + new PackageZipError({ detail: `package output path must be outside packaged roots: ${outputPath}`, }), ); @@ -530,7 +538,7 @@ const resolveRealOutputCandidatePath = Effect.fn("resolveRealOutputCandidatePath if (linkTarget !== undefined) { return yield* Effect.fail( - PackageZipError.make({ + new PackageZipError({ detail: `package output path must not be a symlink: ${outputPath}`, }), ); @@ -634,7 +642,7 @@ const getRokuDeployOptionsEffect = Effect.fn("getRokuDeployOptions")(function* ( return yield* Effect.try({ try: () => withCwd(configRootDir, () => rokuDeploy.getOptions(options)), catch: (error) => - PackageZipError.make({ + new PackageZipError({ detail: `failed to read Roku package options: ${formatErrorMessage(error)}`, }), }); @@ -647,7 +655,7 @@ const getRokuDeployOutputZipFilePathEffect = Effect.fn("getRokuDeployOutputZipFi return yield* Effect.try({ try: () => withCwd(configRootDir, () => rokuDeploy.getOutputZipFilePath(options)), catch: (error) => - PackageZipError.make({ + new PackageZipError({ detail: `failed to resolve Roku package output path: ${formatErrorMessage(error)}`, }), }); diff --git a/src/polling.ts b/src/polling.ts index 8838a26..b47bf9f 100644 --- a/src/polling.ts +++ b/src/polling.ts @@ -31,25 +31,24 @@ export const pollPending = (state: S): PollDecision => ({ status: "pending", }); -export const pollUntilEffect = ( +export const pollUntilEffect = Effect.fn("pollUntil")(function* ( options: PollOptions, -): Effect.Effect => - Effect.gen(function* () { - const start = yield* Clock.currentTimeMillis; - let state = options.initialState; - - while (true) { - const decision = yield* options.poll(state); - if (decision.status === "done") { - return decision.value; - } - - state = decision.state; - const now = yield* Clock.currentTimeMillis; - if (now - start >= options.timeoutMs) { - return yield* options.timeout(state); - } - - yield* Effect.sleep(options.intervalMs); +): Effect.fn.Return { + const start = yield* Clock.currentTimeMillis; + let state = options.initialState; + + while (true) { + const decision = yield* options.poll(state); + if (decision.status === "done") { + return decision.value; } - }); + + state = decision.state; + const now = yield* Clock.currentTimeMillis; + if (now - start >= options.timeoutMs) { + return yield* options.timeout(state); + } + + yield* Effect.sleep(options.intervalMs); + } +}); diff --git a/src/rokit-effect.ts b/src/rokit-effect.ts index 1b16817..475a4f7 100644 --- a/src/rokit-effect.ts +++ b/src/rokit-effect.ts @@ -5,4 +5,4 @@ export const syncRokit = (trySync: () => A): Effect.Effect => Effect.try({ catch: normalizeError, try: trySync }); export const failRokit = (message: string): Effect.Effect => - Effect.fail(InvalidInput.make({ message })); + Effect.fail(new InvalidInput({ message })); diff --git a/src/roku-context-requirements.ts b/src/roku-context-requirements.ts index 516f273..0e195ff 100644 --- a/src/roku-context-requirements.ts +++ b/src/roku-context-requirements.ts @@ -5,11 +5,11 @@ import type { RokuContext } from "./roku-context.js"; export const requireRokuContextEffect = ( context: RokuContext | undefined, ): Effect.Effect => - context === undefined ? Effect.fail(MissingTarget.make({})) : Effect.succeed(context); + context === undefined ? Effect.fail(new MissingTarget({})) : Effect.succeed(context); export const requirePasswordEffect = ( context: RokuContext, ): Effect.Effect => context.password === undefined || context.password === "" - ? Effect.fail(MissingPassword.make({})) + ? Effect.fail(new MissingPassword({})) : Effect.succeed(context.password); diff --git a/src/runtime.ts b/src/runtime.ts index 3a9473f..d9e0b0e 100644 --- a/src/runtime.ts +++ b/src/runtime.ts @@ -31,7 +31,7 @@ export const requireTarget = (env: RokitEnv): string => { const target = env.target?.trim(); if (!target) { - throw MissingTarget.make({}); + throw new MissingTarget({}); } return normalizeTarget(target); @@ -41,7 +41,7 @@ export const requirePassword = (env: RokitEnv): string => { const password = env.password; if (!password) { - throw MissingPassword.make({}); + throw new MissingPassword({}); } return password; @@ -90,7 +90,7 @@ export const rejectUnsafeEcpPath = (value: string): void => { }; export const fail = (message: string): never => { - throw InvalidInput.make({ message }); + throw new InvalidInput({ message }); }; export const formatErrorMessage = (error: unknown): string => { diff --git a/src/screenshot.ts b/src/screenshot.ts index d91e256..7da8728 100644 --- a/src/screenshot.ts +++ b/src/screenshot.ts @@ -3,6 +3,7 @@ import { Effect, FileSystem, Layer, Path, Schema } from "effect"; import type { PlatformError } from "effect/PlatformError"; import { digestAuthHeaderEffect, parseDigestChallenge } from "./digest-auth.js"; import type { RokuContext } from "./roku-context.js"; +import { abortSignalWithTimeout } from "./timing.js"; export type ScreenshotCaptureOptions = { readonly attempts?: number; @@ -10,7 +11,7 @@ export type ScreenshotCaptureOptions = { readonly tempDirPrefix?: string; }; -class ScreenshotCaptureError extends Schema.TaggedErrorClass()( +class ScreenshotCaptureError extends Schema.TaggedError()( "ScreenshotCaptureError", { detail: Schema.String, @@ -49,11 +50,12 @@ export const takeScreenshotEffect: ( yield* fs.makeDirectory(path.dirname(outputWithDeviceExtension), { recursive: true }); yield* fs.writeFile(outputWithDeviceExtension, image).pipe( - Effect.mapError((error) => - ScreenshotCaptureError.make({ - detail: formatErrorMessage(error), - outputPath: path.basename(outputWithDeviceExtension), - }), + Effect.mapError( + (error) => + new ScreenshotCaptureError({ + detail: formatErrorMessage(error), + outputPath: path.basename(outputWithDeviceExtension), + }), ), ); @@ -82,7 +84,7 @@ const createScreenshotOnDeviceEffect = Effect.fn("createScreenshotOnDevice")(fun } return yield* Effect.fail( - ScreenshotCaptureError.make({ + new ScreenshotCaptureError({ detail: "Roku did not return a screenshot URL", outputPath: "screenshot", }), @@ -97,17 +99,17 @@ const postPluginInspectFormEffect = Effect.fn("postPluginInspectForm")(function* const challenge = yield* readScreenshotDigestChallengeEffect(context, url); const authorization = yield* digestAuthHeaderEffect(context, "POST", url.pathname, challenge); const response = yield* Effect.tryPromise({ - try: () => + try: (signal) => fetch(url, { body: makeBody(), headers: { Authorization: authorization, }, method: "POST", - signal: AbortSignal.timeout(context.timeoutMs), + signal: abortSignalWithTimeout(signal, context.timeoutMs), }), catch: (error) => - ScreenshotCaptureError.make({ + new ScreenshotCaptureError({ detail: formatErrorMessage(error), outputPath: "screenshot", }), @@ -128,7 +130,7 @@ const fetchScreenshotImageEffect = Effect.fn("fetchScreenshotImage")(function* ( if (!response.ok) { return yield* Effect.fail( - ScreenshotCaptureError.make({ + new ScreenshotCaptureError({ detail: `image request failed with HTTP ${response.status}`, outputPath: url.pathname, }), @@ -138,7 +140,7 @@ const fetchScreenshotImageEffect = Effect.fn("fetchScreenshotImage")(function* ( return yield* Effect.tryPromise({ try: async () => new Uint8Array(await response.arrayBuffer()), catch: (error) => - ScreenshotCaptureError.make({ + new ScreenshotCaptureError({ detail: formatErrorMessage(error), outputPath: url.pathname, }), @@ -153,7 +155,7 @@ const fetchAuthorizedScreenshotImageEffect = Effect.fn("fetchAuthorizedScreensho const challenge = parseDigestChallenge(challengeResponse.headers.get("www-authenticate")); if (challenge === undefined) { return yield* Effect.fail( - ScreenshotCaptureError.make({ + new ScreenshotCaptureError({ detail: "Roku screenshot image did not provide a Digest auth challenge", outputPath: url.pathname, }), @@ -170,13 +172,13 @@ const fetchScreenshotImageRequestEffect = Effect.fn("fetchScreenshotImageRequest timeoutMs: number, ) { return yield* Effect.tryPromise({ - try: () => + try: (signal) => fetch(url, { headers: authorization === undefined ? undefined : { Authorization: authorization }, - signal: AbortSignal.timeout(timeoutMs), + signal: abortSignalWithTimeout(signal, timeoutMs), }), catch: (error) => - ScreenshotCaptureError.make({ + new ScreenshotCaptureError({ detail: formatErrorMessage(error), outputPath: url.pathname, }), @@ -188,9 +190,13 @@ const readScreenshotDigestChallengeEffect = Effect.fn("readScreenshotDigestChall url: URL, ) { const response = yield* Effect.tryPromise({ - try: () => fetch(url, { method: "GET", signal: AbortSignal.timeout(context.timeoutMs) }), + try: (signal) => + fetch(url, { + method: "GET", + signal: abortSignalWithTimeout(signal, context.timeoutMs), + }), catch: (error) => - ScreenshotCaptureError.make({ + new ScreenshotCaptureError({ detail: formatErrorMessage(error), outputPath: "screenshot", }), @@ -199,7 +205,7 @@ const readScreenshotDigestChallengeEffect = Effect.fn("readScreenshotDigestChall if (challenge === undefined) { return yield* Effect.fail( - ScreenshotCaptureError.make({ + new ScreenshotCaptureError({ detail: "Roku developer inspector did not provide a Digest auth challenge", outputPath: "screenshot", }), @@ -216,7 +222,7 @@ const readScreenshotResponseTextEffect = Effect.fn("readScreenshotResponseText") const body = yield* Effect.tryPromise({ try: () => response.text(), catch: (error) => - ScreenshotCaptureError.make({ + new ScreenshotCaptureError({ detail: formatErrorMessage(error), outputPath, }), @@ -224,7 +230,7 @@ const readScreenshotResponseTextEffect = Effect.fn("readScreenshotResponseText") if (!response.ok) { return yield* Effect.fail( - ScreenshotCaptureError.make({ + new ScreenshotCaptureError({ detail: `HTTP ${response.status}: ${body.trim() || response.statusText}`, outputPath, }), @@ -310,19 +316,16 @@ export const captureScreenshotEffect: ( } return yield* Effect.fail( - ScreenshotCaptureError.make({ + new ScreenshotCaptureError({ detail: "screenshot capture succeeded without writing an image file", outputPath: outputFileName, }), ); }), ).pipe( - Effect.match({ - onFailure: (error) => { - lastError = formatErrorMessage(error); - return undefined; - }, - onSuccess: (result) => result, + Effect.catchTag("ScreenshotCaptureError", (error) => { + lastError = error.message; + return Effect.succeed(undefined); }), ); @@ -336,7 +339,7 @@ export const captureScreenshotEffect: ( } return yield* Effect.fail( - ScreenshotCaptureError.make({ + new ScreenshotCaptureError({ detail: lastError, outputPath: outputFileName, }), diff --git a/src/timing.ts b/src/timing.ts index 40fbd96..2503a73 100644 --- a/src/timing.ts +++ b/src/timing.ts @@ -1,5 +1,8 @@ import { Effect } from "effect"; +export const abortSignalWithTimeout = (signal: AbortSignal, timeoutMs: number): AbortSignal => + AbortSignal.any([signal, AbortSignal.timeout(timeoutMs)]); + export const sleepEffect = Effect.fn("sleep")(function* (ms: number) { yield* Effect.sleep(ms); }); diff --git a/test/errors.test.ts b/test/errors.test.ts index 2dcf6a7..98aa9f9 100644 --- a/test/errors.test.ts +++ b/test/errors.test.ts @@ -11,10 +11,10 @@ import { describe("rokit errors", () => { it.effect("renders schema-backed CLI errors", () => Effect.sync(() => { - assert.strictEqual(renderError(InvalidInput.make({ message: "bad command" })), "bad command"); - assert.strictEqual(renderError(MissingTarget.make({})), "ROKIT_TARGET is not set"); + assert.strictEqual(renderError(new InvalidInput({ message: "bad command" })), "bad command"); + assert.strictEqual(renderError(new MissingTarget({})), "ROKIT_TARGET is not set"); assert.strictEqual( - renderError(DebugPortUnavailable.make({ detail: "connection refused", port: 8085 })), + renderError(new DebugPortUnavailable({ detail: "connection refused", port: 8085 })), "Roku debug port 8085 unavailable: connection refused", ); }), diff --git a/test/roku.test.ts b/test/roku.test.ts index a7504cb..4dd02cd 100644 --- a/test/roku.test.ts +++ b/test/roku.test.ts @@ -1,5 +1,5 @@ import { resolve } from "node:path"; -import { Effect } from "effect"; +import { Effect, Fiber } from "effect"; import * as rokuDeploy from "roku-deploy"; import { describe, expect, it, vi } from "vite-plus/test"; import { discoverRokuDevicesEffect, readSsdpHeaders } from "../src/discovery.js"; @@ -57,6 +57,23 @@ describe("Roku helpers", () => { expect(fetchCalls).toEqual(["http://192.0.2.1:8060/keypress/Lit_%2F"]); }); + it("aborts in-flight ECP requests when the Effect is interrupted", async () => { + const originalFetch = globalThis.fetch; + let requestSignal: AbortSignal | undefined; + globalThis.fetch = (_input, init) => { + requestSignal = init?.signal ?? undefined; + return new Promise(() => {}); + }; + + try { + const fiber = Effect.runFork(fetchEcpTextEffect(context, "/query/device-info")); + await Effect.runPromise(Fiber.interrupt(fiber)); + expect(requestSignal?.aborted).toBe(true); + } finally { + globalThis.fetch = originalFetch; + } + }); + it("reads enhanced device info through roku-deploy", async () => { const getDeviceInfoSpy = vi.spyOn(rokuDeploy, "getDeviceInfo").mockResolvedValueOnce({ friendlyDeviceName: "Living Room & Lab", From cadce939fdac257687fd478d8ec89a660787aea3 Mon Sep 17 00:00:00 2001 From: Altay Date: Mon, 10 Aug 2026 10:45:10 +0300 Subject: [PATCH 2/8] chore: remove Effect source readiness tooling --- .worktreeinclude | 1 - AGENTS.md | 11 ++-- README.md | 1 - docs/DISTRIBUTION.md | 4 +- docs/READINESS.md | 90 --------------------------- package.json | 1 - scripts/prepare-effect.sh | 54 ---------------- skills/rokit/SKILL.md | 2 - skills/rokit/references/live-proof.md | 7 +-- test/cli.test.ts | 17 +---- 10 files changed, 11 insertions(+), 177 deletions(-) delete mode 100644 docs/READINESS.md delete mode 100755 scripts/prepare-effect.sh diff --git a/.worktreeinclude b/.worktreeinclude index ab96260..cebd3f4 100644 --- a/.worktreeinclude +++ b/.worktreeinclude @@ -1,4 +1,3 @@ /.env /.env.local .rokit/.env -/.repos/effect/ diff --git a/AGENTS.md b/AGENTS.md index 346d3e1..1486eca 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -72,8 +72,8 @@ guide doesn't cover, search through the source code in `node_modules/effect/src` - Avoid sleeps in generic commands. App repos can add meaningful wait/assert loops around `rokit` primitives. - Release details live in `docs/DISTRIBUTION.md`; debugging details live in - `docs/DEBUGGING.md`; readiness details live in `docs/READINESS.md`; - consumer skill guidance lives in `skills/rokit/SKILL.md`. + `docs/DEBUGGING.md`; consumer skill guidance lives in + `skills/rokit/SKILL.md`. ## When Contracts Change @@ -85,10 +85,9 @@ guide doesn't cover, search through the source code in `node_modules/effect/src` ## Worktrees -`.worktreeinclude` carries env and `.repos/effect` into managed worktrees; -Claude symlinks `.repos`. Run `vp install`, `vp run hooks:install`, and -`vp run verify`. If device config is missing, copy `.env.example` to `.env` or -`.rokit/.env`. +`.worktreeinclude` carries local env files into managed worktrees. Run +`vp install`, `vp run hooks:install`, and `vp run verify`. If device config is +missing, copy `.env.example` to `.env` or `.rokit/.env`. ## Checks diff --git a/README.md b/README.md index d43a716..5b070af 100644 --- a/README.md +++ b/README.md @@ -181,7 +181,6 @@ artifacts. - [Contributing](./CONTRIBUTING.md) - [Roku debugging](./docs/DEBUGGING.md) - [Distribution](./docs/DISTRIBUTION.md) -- [Agent readiness](./docs/READINESS.md) - [rokit skill](./skills/rokit/SKILL.md) - [Security](./SECURITY.md) diff --git a/docs/DISTRIBUTION.md b/docs/DISTRIBUTION.md index 06e1c5e..6761100 100644 --- a/docs/DISTRIBUTION.md +++ b/docs/DISTRIBUTION.md @@ -41,8 +41,8 @@ Release writes use the `putio-releaser` installation token. The default `GITHUB_ The npm package includes `dist`, `README.md`, `docs`, `examples`, `skills`, `AGENTS.md`, `CONTRIBUTING.md`, and `SECURITY.md`. The docs, consumer skill, and generic live probe are included so agents consuming the package can inspect -readiness, distribution, security, and generic Roku proof mechanics without -cloning extra private context. +distribution, security, and generic Roku proof mechanics without cloning extra +private context. ## Release Smoke diff --git a/docs/READINESS.md b/docs/READINESS.md deleted file mode 100644 index 60ef6f2..0000000 --- a/docs/READINESS.md +++ /dev/null @@ -1,90 +0,0 @@ -# Agent Readiness - -`rokit` is a CLI/package repo. There is no long-running app to boot; readiness is based on deterministic package verification plus optional live Roku proof. - -## Grade - -Overall: B+ - -| Dimension | Status | Evidence | Gap | -| ---------- | ------ | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------- | -| bootable | pass | `pnpm smoke` builds the CLI and checks `--version` plus `--help` | no server boot surface, by design | -| testable | pass | `pnpm verify` runs TypeScript, bundle, unit tests, and npm pack dry run | live Roku checks require local hardware | -| observable | pass | CLI commands print active app, device info, raw ECP state, SceneGraph XML, BrightScript console logs, debug-server output, snapshots, proof bundles, screenshots, compact assertion failures, and JSON output by default in non-TTY runs | no CI hardware lane | -| verifiable | pass | CI runs `pnpm verify`; `pnpm live:smoke` proves a configured Roku responds; `pnpm live:probe` packages, installs, launches, drives, asserts, screenshots, and proofs a generic channel | no CI hardware lane | - -## Layers - -- Boot: `pnpm smoke`, `rokit describe` -- Smoke: `pnpm smoke`, `rokit check` -- Interact: `rokit press`, `rokit launch`, `rokit query`, `rokit sgnodes` -- E2e: `pnpm live:probe` for a configured developer-enabled Roku -- Enforce: GitHub Actions `verify`, optional `.git-hooks/pre-push` -- Observe: ECP responses, SceneGraph XML, `rokit console`, `rokit debug-command`, `rokit snapshot`, `rokit proof`, screenshots, concise command output -- Isolate: `.rokit/` and `.env` stay local per worktree - -## Agent-Facing CLI Contract - -- The CLI entrypoint runs through Effect's Node runtime, and expected CLI - failures are schema-backed errors rendered without stack traces. -- `rokit describe` exposes the machine-readable command surface, parameters, - JSON payload fields, and global options without a Roku target. `rokit -describe ` returns one command schema for context-window control. -- Non-TTY command output defaults to JSON. Use `--output text` when a human - transcript is required. -- `--input-json` lets agents provide typed command payloads inline, from a file - with `@path`, or from stdin with `-` without translating everything into - flags. -- `--fields` trims JSON output for context-window control. -- `--dry-run` validates mutating commands without touching a device or writing - files. -- ECP paths and generated output paths are hardened against common agent - mistakes: query strings, fragments, traversal, backslashes, control - characters, encoded path segments, and writes outside the current app root. - -## Agent DX Score - -Current score: 16/21, agent-first lower bound. - -| Axis | Score | Evidence | Gap | -| ----------------------- | ----- | -------------------------------------------------- | ------------------------------ | -| machine output | 2 | consistent JSON output and structured JSON errors | no NDJSON streaming surface | -| raw payload input | 2 | `--input-json` covers command payloads | schemas are local, not API-gen | -| schema introspection | 2 | `rokit describe` and `rokit describe ` | not runtime-resolved from Roku | -| context discipline | 2 | `--fields` trims JSON output everywhere | no pagination surface | -| input hardening | 3 | hardened ECP paths and output-path sandboxing | none known | -| safety rails | 2 | mutating commands support `--dry-run` | no response sanitization layer | -| agent knowledge package | 3 | `AGENTS.md` plus `skills/rokit/SKILL.md` ship cues | none known | - -## Setup For Agents - -```bash -pnpm install -pnpm hooks:install -pnpm verify -``` - -`pnpm install` checks out the exact `effect` dependency tag into -`.repos/effect` outside CI only. When `CI` is set to any non-empty value, -`scripts/prepare-effect.sh` exits without cloning the source so install, pack, -publish, and CI/CD workspaces stay predictable. - -Optional live smoke: - -```bash -ROKIT_TARGET= pnpm live:smoke -``` - -Full local live probe: - -```bash -cp .env.example .env -# Fill ROKIT_TARGET and ROKIT_PASSWORD. -pnpm live:probe -``` - -`pnpm live:probe` reads `.env` and `.env.local`, accepts `ROKU_DEV_TARGET` and -`ROKU_DEV_PASSWORD` as fallbacks, copies `examples/live-probe-channel` into -`.rokit/live-probe/app`, packages it, installs it into the Roku developer slot, -launches `dev`, checks generic SceneGraph labels, sends `Info Back`, and writes -proof artifacts to `.rokit/live-probe/artifacts`. diff --git a/package.json b/package.json index 8056484..29d831c 100644 --- a/package.json +++ b/package.json @@ -50,7 +50,6 @@ "hooks:install": "git config core.hooksPath .git-hooks", "live:smoke": "vp pack src/index.ts src/rokit.ts --dts && node dist/rokit.mjs check", "live:probe": "scripts/live-probe.sh", - "prepare": "./scripts/prepare-effect.sh", "prepack": "vp pack src/index.ts src/rokit.ts --dts", "smoke": "vp pack src/index.ts src/rokit.ts --dts && node dist/rokit.mjs --version && node dist/rokit.mjs --help >/dev/null", "test": "vp pack src/index.ts src/rokit.ts --dts && vp test run --passWithNoTests", diff --git a/scripts/prepare-effect.sh b/scripts/prepare-effect.sh deleted file mode 100755 index 135333d..0000000 --- a/scripts/prepare-effect.sh +++ /dev/null @@ -1,54 +0,0 @@ -#!/usr/bin/env sh - -set -eu - -if [ -n "${CI:-}" ]; then - exit 0 -fi - -effect_upstream="https://github.com/Effect-TS/effect.git" -script_dir="$(CDPATH= cd -- "$(dirname -- "$0")" && pwd)" -effect_ref="$(node -e ' -const { readFileSync } = require("node:fs"); -const packageJson = JSON.parse(readFileSync(process.argv[1], "utf8")); -const version = packageJson.dependencies?.effect; -if (typeof version !== "string" || !/^\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?$/.test(version)) { - console.error("package.json dependencies.effect must be an exact version"); - process.exit(1); -} -process.stdout.write(`effect@${version}`); -' "$script_dir/../package.json")" -repo_dir="$(dirname "$script_dir")/.repos/effect" - -if [ -e "$repo_dir" ] && [ ! -d "$repo_dir/.git" ]; then - echo "Effect source path exists but is not a Git checkout: $repo_dir" >&2 - exit 1 -fi - -if [ ! -d "$repo_dir/.git" ]; then - mkdir -p "$repo_dir" - git -C "$repo_dir" init --quiet -fi - -if [ -n "$(git -C "$repo_dir" status --porcelain=v1)" ]; then - echo "Effect source checkout has local changes: $repo_dir" >&2 - echo "Commit, stash, or remove those changes before running this command again." >&2 - exit 1 -fi - -current_remote="$(git -C "$repo_dir" remote get-url origin 2>/dev/null || true)" -if [ -z "$current_remote" ]; then - git -C "$repo_dir" remote add origin "$effect_upstream" -elif [ "$current_remote" != "$effect_upstream" ]; then - git -C "$repo_dir" remote set-url origin "$effect_upstream" -fi - -git -C "$repo_dir" fetch --depth 1 --force origin "refs/tags/$effect_ref:refs/tags/$effect_ref" -target_commit="$(git -C "$repo_dir" rev-list -n 1 "$effect_ref")" -current_commit="$(git -C "$repo_dir" rev-parse HEAD 2>/dev/null || true)" - -if [ "$current_commit" != "$target_commit" ]; then - git -C "$repo_dir" checkout --detach "$target_commit" -fi - -printf 'Effect source ready at %s (%s)\n' "$repo_dir" "$effect_ref" diff --git a/skills/rokit/SKILL.md b/skills/rokit/SKILL.md index 2053de3..609ae1f 100644 --- a/skills/rokit/SKILL.md +++ b/skills/rokit/SKILL.md @@ -68,5 +68,3 @@ rokit console artifacts/live/console.log --duration-ms 30000 expectations, and review narratives. - ECP query paths must be plain paths. Do not include query strings, fragments, traversal, backslashes, or encoded path segments. -- The local Effect source setup is skipped whenever `CI` is set. Do not make CI, - install, pack, or publish flows depend on cloning `.repos/effect`. diff --git a/skills/rokit/references/live-proof.md b/skills/rokit/references/live-proof.md index 65da825..06fc244 100644 --- a/skills/rokit/references/live-proof.md +++ b/skills/rokit/references/live-proof.md @@ -12,10 +12,9 @@ rokit describe rokit describe proof ``` -Use `README.md` for the public command overview and `docs/READINESS.md` for the -agent-readiness model. Use the one-command form to keep context small when you -already know the command family. Use `src/cli.ts` when command behavior and docs -disagree. +Use `README.md` for the public command overview. Use the one-command form to +keep context small when you already know the command family. Use `src/cli.ts` +when command behavior and docs disagree. ## Quick Reads diff --git a/test/cli.test.ts b/test/cli.test.ts index 000c8ea..9a249f9 100644 --- a/test/cli.test.ts +++ b/test/cli.test.ts @@ -1,6 +1,5 @@ import { spawnSync } from "node:child_process"; -import { cpSync, existsSync, mkdirSync, mkdtempSync, rmSync } from "node:fs"; -import { tmpdir } from "node:os"; +import { cpSync, mkdirSync, mkdtempSync, rmSync } from "node:fs"; import { dirname, join, resolve } from "node:path"; import { fileURLToPath } from "node:url"; import { NodeServices } from "@effect/platform-node"; @@ -19,7 +18,6 @@ const testDistParent = resolve(repoRoot, ".rokit"); mkdirSync(testDistParent, { recursive: true }); const testDistDir = mkdtempSync(join(testDistParent, "cli-test-dist-")); const cliPath = resolve(testDistDir, "rokit.mjs"); -const prepareEffectPath = resolve(repoRoot, "scripts/prepare-effect.sh"); beforeAll(() => { cpSync(builtDistDir, testDistDir, { recursive: true }); @@ -405,17 +403,4 @@ describe("rokit CLI functionality", () => { it("does not run target-required commands without a context", async () => { await expect(runCommand({ name: "check" }, false)).rejects.toThrow("ROKIT_TARGET is not set"); }); - - it("does not clone the local Effect checkout in CI", () => { - const cwd = mkdtempSync(join(tmpdir(), "rokit-effect-ci-")); - const result = spawnSync(prepareEffectPath, { - cwd, - encoding: "utf8", - env: { ...process.env, CI: "true" }, - }); - - expect(result.status).toBe(0); - expect(result.stderr).toBe(""); - expect(existsSync(join(cwd, ".repos"))).toBe(false); - }); }); From 2fa911854a6b1f568e158aafb91c38fecf1eeba8 Mon Sep 17 00:00:00 2001 From: Altay Date: Mon, 10 Aug 2026 10:55:06 +0300 Subject: [PATCH 3/8] test: cover Effect retry and ZIP cleanup --- test/package-zip.test.ts | 69 ++++++++++++++++++++++++++++++++++++++- test/roku-capture.test.ts | 19 +++++++++++ 2 files changed, 87 insertions(+), 1 deletion(-) diff --git a/test/package-zip.test.ts b/test/package-zip.test.ts index 9b8c1ef..f6fc810 100644 --- a/test/package-zip.test.ts +++ b/test/package-zip.test.ts @@ -1,8 +1,11 @@ -import { mkdir, mkdtemp, readFile, rm, symlink, writeFile } from "node:fs/promises"; +import { mkdir, mkdtemp, readFile, readdir, rm, symlink, writeFile } from "node:fs/promises"; import { tmpdir } from "node:os"; import { join, resolve } from "node:path"; +import { NodeFileSystem, NodePath } from "@effect/platform-node"; +import { Effect, FileSystem, Layer } from "effect"; import JSZip from "jszip"; import { describe, expect, it } from "vite-plus/test"; +import { createPackageZipEffect } from "../src/package-zip.js"; import { createPackageZip, packageChannel, resolveSafePackageOutputPath } from "../src/roku.js"; describe("package zip helper", () => { @@ -49,6 +52,70 @@ describe("package zip helper", () => { } }); + it("atomically replaces an existing package without leaving temporary files", async () => { + const root = await mkdtemp(join(tmpdir(), "rokit-package-")); + + try { + await mkdir(join(root, "source"), { recursive: true }); + await mkdir(join(root, "dist"), { recursive: true }); + await writeFile(join(root, "manifest"), "title=Replacement\n"); + await writeFile(join(root, "source/Main.brs"), "sub Main()\nend sub\n"); + await writeFile(join(root, "dist/channel.zip"), "previous package"); + + await createPackageZip({ + outFile: "dist/channel.zip", + rootDir: root, + }); + + const zip = await JSZip.loadAsync(await readFile(join(root, "dist/channel.zip"))); + expect(await zip.file("manifest")?.async("string")).toBe("title=Replacement\n"); + expect(await readdir(join(root, "dist"))).toEqual(["channel.zip"]); + } finally { + await rm(root, { force: true, recursive: true }); + } + }); + + it("preserves an existing package and cleans the temporary file when writing fails", async () => { + const root = await mkdtemp(join(tmpdir(), "rokit-package-")); + + try { + await mkdir(join(root, "source"), { recursive: true }); + await mkdir(join(root, "dist"), { recursive: true }); + await writeFile(join(root, "manifest"), "title=Example\n"); + await writeFile(join(root, "source/Main.brs"), "sub Main()\nend sub\n"); + await writeFile(join(root, "dist/channel.zip"), "previous package"); + + const failingFileSystemLayer = Layer.effect( + FileSystem.FileSystem, + Effect.map(FileSystem.FileSystem, (fs) => + FileSystem.FileSystem.of({ + ...fs, + writeFile: (path, data, options) => + path.includes(".channel.zip-") + ? Effect.die(new Error("simulated package write failure")) + : fs.writeFile(path, data, options), + }), + ), + ).pipe(Layer.provide(NodeFileSystem.layer)); + const testLayer = Layer.merge(failingFileSystemLayer, NodePath.layer); + + await expect( + Effect.runPromise( + createPackageZipEffect({ + outFile: "dist/channel.zip", + rootDir: root, + }).pipe(Effect.provide(testLayer)), + ), + ).rejects.toThrow("simulated package write failure"); + await expect(readFile(join(root, "dist/channel.zip"), "utf8")).resolves.toBe( + "previous package", + ); + expect(await readdir(join(root, "dist"))).toEqual(["channel.zip"]); + } finally { + await rm(root, { force: true, recursive: true }); + } + }); + it("rejects unsafe override paths", async () => { await expect( createPackageZip({ diff --git a/test/roku-capture.test.ts b/test/roku-capture.test.ts index e5374ad..1a87138 100644 --- a/test/roku-capture.test.ts +++ b/test/roku-capture.test.ts @@ -85,6 +85,25 @@ describe("Roku retry helpers", () => { } }); + it("surfaces the underlying screenshot failure after retries are exhausted", async () => { + const root = await mkdtemp(join(tmpdir(), "rokit-capture-test-")); + + try { + const fetchMock = vi.fn().mockRejectedValue(new Error("transport detail")); + vi.stubGlobal("fetch", fetchMock); + + await expect( + captureScreenshot({ ...context, password: "pass" }, join(root, "story.jpg"), { + attempts: 2, + retryDelayMs: 1, + }), + ).rejects.toThrow("transport detail"); + expect(fetchMock).toHaveBeenCalledTimes(2); + } finally { + await rm(root, { force: true, recursive: true }); + } + }); + it("deletes the installed developer channel through the native Roku installer form", async () => { const fetchMock = vi .fn() From 23061b02439c63fd9dffb5576566e45e12febac8 Mon Sep 17 00:00:00 2001 From: Altay Date: Mon, 10 Aug 2026 11:04:27 +0300 Subject: [PATCH 4/8] ci: allow pinned Effect beta packages --- pnpm-workspace.yaml | 12 ++++-------- 1 file changed, 4 insertions(+), 8 deletions(-) diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index fe138fc..b421ed8 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -9,14 +9,10 @@ allowBuilds: "@parcel/watcher": true msgpackr-extract: true minimumReleaseAgeExclude: - - "@effect/platform-node-shared@4.0.0-beta.78" - - "@effect/platform-node@4.0.0-beta.78" - - "@effect/vitest@4.0.0-beta.78" - - effect@4.0.0-beta.78 - - "@effect/platform-node-shared@4.0.0-beta.107" - - "@effect/platform-node@4.0.0-beta.107" - - "@effect/vitest@4.0.0-beta.107" - - effect@4.0.0-beta.107 + - "@effect/platform-node-shared" + - "@effect/platform-node" + - "@effect/vitest" + - effect catalog: vite: npm:@voidzero-dev/vite-plus-core@0.2.6 vitest: 4.1.10 From 6d2a185d0710db20922506beb1a1a8309d5de377 Mon Sep 17 00:00:00 2001 From: Altay Date: Mon, 10 Aug 2026 11:09:22 +0300 Subject: [PATCH 5/8] docs: explain Effect release-age exception --- pnpm-workspace.yaml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index b421ed8..6cd08c5 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -9,6 +9,8 @@ allowBuilds: "@parcel/watcher": true msgpackr-extract: true minimumReleaseAgeExclude: + # Vite+ 0.2.6 ignores package@version entries in its supply-chain verifier. + # Keep Effect exact in package.json and re-pin here when the verifier supports it. - "@effect/platform-node-shared" - "@effect/platform-node" - "@effect/vitest" From 0a685c4659d22d7547991a933546904d40614050 Mon Sep 17 00:00:00 2001 From: Altay Date: Mon, 10 Aug 2026 11:19:43 +0300 Subject: [PATCH 6/8] fix: preserve screenshot retry detail --- src/screenshot.ts | 2 +- test/roku-capture.test.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/screenshot.ts b/src/screenshot.ts index 7da8728..3cb5fb2 100644 --- a/src/screenshot.ts +++ b/src/screenshot.ts @@ -324,7 +324,7 @@ export const captureScreenshotEffect: ( }), ).pipe( Effect.catchTag("ScreenshotCaptureError", (error) => { - lastError = error.message; + lastError = error.detail; return Effect.succeed(undefined); }), ); diff --git a/test/roku-capture.test.ts b/test/roku-capture.test.ts index 1a87138..a3454df 100644 --- a/test/roku-capture.test.ts +++ b/test/roku-capture.test.ts @@ -97,7 +97,7 @@ describe("Roku retry helpers", () => { attempts: 2, retryDelayMs: 1, }), - ).rejects.toThrow("transport detail"); + ).rejects.toThrow("failed to capture story.jpg: transport detail"); expect(fetchMock).toHaveBeenCalledTimes(2); } finally { await rm(root, { force: true, recursive: true }); From 9571e5fff95f86b2a651004450475121ea4f7197 Mon Sep 17 00:00:00 2001 From: Altay Date: Mon, 10 Aug 2026 11:25:53 +0300 Subject: [PATCH 7/8] fix: cancel Roku response body reads --- src/ecp.ts | 9 +++++---- src/installer.ts | 9 +++++---- src/response-body.ts | 46 ++++++++++++++++++++++++++++++++++++++++++++ src/screenshot.ts | 17 ++++++++-------- test/roku.test.ts | 27 ++++++++++++++++++++++++++ 5 files changed, 92 insertions(+), 16 deletions(-) create mode 100644 src/response-body.ts diff --git a/src/ecp.ts b/src/ecp.ts index 310bd89..5347c7d 100644 --- a/src/ecp.ts +++ b/src/ecp.ts @@ -1,4 +1,5 @@ import { Effect, Schema } from "effect"; +import { readResponseTextEffect } from "./response-body.js"; import type { RokuContext } from "./roku-context.js"; import { abortSignalWithTimeout } from "./timing.js"; @@ -86,11 +87,11 @@ export const fetchEcpTextEffect = Effect.fn("fetchEcpText")(function* ( ); } - return yield* Effect.tryPromise({ - try: () => response.text(), - catch: (error) => + return yield* readResponseTextEffect( + response, + (error) => new EcpTransportError({ detail: formatErrorMessage(error), method: "GET", path: safePath }), - }); + ); }); export const postEcpEffect = Effect.fn("postEcp")(function* (context: RokuContext, path: string) { diff --git a/src/installer.ts b/src/installer.ts index b1227f6..a27f021 100644 --- a/src/installer.ts +++ b/src/installer.ts @@ -8,6 +8,7 @@ import { normalizeInstallSuccessMessage, readInstallerMessage, } from "./installer-message.js"; +import { readResponseTextEffect } from "./response-body.js"; import type { RokuContext } from "./roku-context.js"; import { abortSignalWithTimeout } from "./timing.js"; @@ -136,14 +137,14 @@ const postInstallerForm = Effect.fn("postInstallerForm")(function* ( detail: formatErrorMessage(error), }), }); - const responseBody = yield* Effect.tryPromise({ - try: () => response.text(), - catch: (error) => + const responseBody = yield* readResponseTextEffect( + response, + (error) => new RokuInstallerTransportError({ action, detail: formatErrorMessage(error), }), - }); + ); const message = readInstallerMessage(responseBody); if (!response.ok) { diff --git a/src/response-body.ts b/src/response-body.ts new file mode 100644 index 0000000..0ab23be --- /dev/null +++ b/src/response-body.ts @@ -0,0 +1,46 @@ +import { Effect } from "effect"; + +export const readResponseBytesEffect = ( + response: Response, + onError: (error: unknown) => E, +): Effect.Effect => + Effect.tryPromise({ + try: (signal) => readResponseBytes(response, signal), + catch: onError, + }); + +export const readResponseTextEffect = ( + response: Response, + onError: (error: unknown) => E, +): Effect.Effect => + Effect.tryPromise({ + try: async (signal) => new TextDecoder().decode(await readResponseBytes(response, signal)), + catch: onError, + }); + +const readResponseBytes = async (response: Response, signal: AbortSignal): Promise => { + if (response.body === null) { + return new Uint8Array(); + } + + const chunks: Uint8Array[] = []; + await response.body.pipeTo( + new WritableStream({ + write: (chunk) => { + chunks.push(chunk); + }, + }), + { signal }, + ); + + const length = chunks.reduce((total, chunk) => total + chunk.byteLength, 0); + const output = new Uint8Array(length); + let offset = 0; + + for (const chunk of chunks) { + output.set(chunk, offset); + offset += chunk.byteLength; + } + + return output; +}; diff --git a/src/screenshot.ts b/src/screenshot.ts index 3cb5fb2..3ad22bd 100644 --- a/src/screenshot.ts +++ b/src/screenshot.ts @@ -2,6 +2,7 @@ import { NodeFileSystem, NodePath } from "@effect/platform-node"; import { Effect, FileSystem, Layer, Path, Schema } from "effect"; import type { PlatformError } from "effect/PlatformError"; import { digestAuthHeaderEffect, parseDigestChallenge } from "./digest-auth.js"; +import { readResponseBytesEffect, readResponseTextEffect } from "./response-body.js"; import type { RokuContext } from "./roku-context.js"; import { abortSignalWithTimeout } from "./timing.js"; @@ -137,14 +138,14 @@ const fetchScreenshotImageEffect = Effect.fn("fetchScreenshotImage")(function* ( ); } - return yield* Effect.tryPromise({ - try: async () => new Uint8Array(await response.arrayBuffer()), - catch: (error) => + return yield* readResponseBytesEffect( + response, + (error) => new ScreenshotCaptureError({ detail: formatErrorMessage(error), outputPath: url.pathname, }), - }); + ); }); const fetchAuthorizedScreenshotImageEffect = Effect.fn("fetchAuthorizedScreenshotImage")(function* ( @@ -219,14 +220,14 @@ const readScreenshotResponseTextEffect = Effect.fn("readScreenshotResponseText") response: Response, outputPath: string, ) { - const body = yield* Effect.tryPromise({ - try: () => response.text(), - catch: (error) => + const body = yield* readResponseTextEffect( + response, + (error) => new ScreenshotCaptureError({ detail: formatErrorMessage(error), outputPath, }), - }); + ); if (!response.ok) { return yield* Effect.fail( diff --git a/test/roku.test.ts b/test/roku.test.ts index 4dd02cd..a7d9df3 100644 --- a/test/roku.test.ts +++ b/test/roku.test.ts @@ -74,6 +74,33 @@ describe("Roku helpers", () => { } }); + it("cancels ECP response body reads when the Effect is interrupted", async () => { + const originalFetch = globalThis.fetch; + const bodyRead = Promise.withResolvers(); + const bodyCanceled = Promise.withResolvers(); + globalThis.fetch = async () => + new Response( + new ReadableStream({ + pull: () => { + bodyRead.resolve(); + return new Promise(() => {}); + }, + cancel: () => { + bodyCanceled.resolve(); + }, + }), + ); + + try { + const fiber = Effect.runFork(fetchEcpTextEffect(context, "/query/device-info")); + await bodyRead.promise; + await Effect.runPromise(Fiber.interrupt(fiber)); + await bodyCanceled.promise; + } finally { + globalThis.fetch = originalFetch; + } + }); + it("reads enhanced device info through roku-deploy", async () => { const getDeviceInfoSpy = vi.spyOn(rokuDeploy, "getDeviceInfo").mockResolvedValueOnce({ friendlyDeviceName: "Living Room & Lab", From ecb020e994ae368744f10950ce764a7db2cccaff Mon Sep 17 00:00:00 2001 From: Altay Date: Mon, 10 Aug 2026 11:33:27 +0300 Subject: [PATCH 8/8] ci: pin shared Effect runtime --- pnpm-lock.yaml | 1 + pnpm-workspace.yaml | 1 + 2 files changed, 2 insertions(+) diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 0af8604..1cf4228 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -5,6 +5,7 @@ settings: excludeLinksFromLockfile: false overrides: + '@effect/platform-node-shared': 4.0.0-beta.107 vite: npm:@voidzero-dev/vite-plus-core@0.2.6 vitest: 4.1.10 diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index 6cd08c5..3ba4426 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -2,6 +2,7 @@ packages: - "." overrides: + "@effect/platform-node-shared": 4.0.0-beta.107 vite: "catalog:" vitest: "catalog:"