diff --git a/.github/security-exceptions.yml b/.github/security-exceptions.yml index 95b0c66..89117ae 100644 --- a/.github/security-exceptions.yml +++ b/.github/security-exceptions.yml @@ -87,6 +87,30 @@ "rationale": "This is the fingerprint of a deliberately public deterministic JWT signing key used only by the hermetic e2e-slim fixture.", "compensating_control": "The key is confined to test fixtures, is not referenced by deployment manifests, and production startup requires externally supplied secrets.", "expires": "2026-12-05" + }, + { + "scanner": "npm-audit", + "identifier": "GHSA-965c-763c-88jm", + "owner": "platform-security", + "rationale": "The dashboard does not enable the better-auth Magic Link plugin, which this OAuth-state-as-magic-link advisory requires. Patched better-auth (>=1.7.7) drops the account.issuer column that services/control-api/migrations/0002_better_auth.sql declares NOT NULL, so the upgrade needs a coordinated schema migration and is tracked separately.", + "compensating_control": "apps/dashboard/lib/auth.ts registers only the admin, jwt, organization, genericOAuth and oauth-provider plugins; adding magicLink before the upgrade lands must remove this exception.", + "expires": "2026-12-05" + }, + { + "scanner": "npm-audit", + "identifier": "GHSA-r4xp-prcw-77qf", + "owner": "platform-security", + "rationale": "The dashboard does not enable the better-auth OAuth Proxy plugin, which this account-takeover advisory requires. Patched better-auth (>=1.7.7) drops the account.issuer column that services/control-api/migrations/0002_better_auth.sql declares NOT NULL, so the upgrade needs a coordinated schema migration and is tracked separately.", + "compensating_control": "apps/dashboard/lib/auth.ts registers only the admin, jwt, organization, genericOAuth and oauth-provider plugins; adding oAuthProxy before the upgrade lands must remove this exception.", + "expires": "2026-12-05" + }, + { + "scanner": "npm-audit", + "identifier": "GHSA-44jh-23m7-hpcf", + "owner": "platform-security", + "rationale": "Low-severity rate-limit race in @better-auth/drizzle-adapter, reported only because better-auth carries critical advisories; the dashboard passes a pg Pool to better-auth and never loads the Drizzle adapter. Patched better-auth (>=1.7.7) drops the account.issuer column that services/control-api/migrations/0002_better_auth.sql declares NOT NULL, so the upgrade needs a coordinated schema migration and is tracked separately.", + "compensating_control": "apps/dashboard/lib/auth.ts configures database: authPool (node-postgres), so better-auth's built-in Kysely adapter handles rate-limit storage instead of the affected Drizzle adapter.", + "expires": "2026-12-05" } ] } diff --git a/AGENTS.md b/AGENTS.md index 7ed4436..9a66d92 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -20,7 +20,7 @@ tracks: | Codex | `codex` | `@openai/codex` | [openai/codex](https://github.com/openai/codex) | | Claude | `claude` | `@anthropic-ai/claude-code` | [anthropics/claude-code](https://github.com/anthropics/claude-code) | | Kimi | `kimi` | `@moonshot-ai/kimi-code` | [MoonshotAI/kimi-code](https://github.com/MoonshotAI/kimi-code) | -| OpenCode | `opencode` | `opencode-ai` | [sst/opencode](https://github.com/sst/opencode) | +| OpenCode | `opencode` | `@opencode/cli` (2.x), `opencode-ai` (1.x) | [anomalyco/opencode](https://github.com/anomalyco/opencode) | ## Repo map diff --git a/Cargo.lock b/Cargo.lock index 766a86d..87e7cfc 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1130,9 +1130,9 @@ dependencies = [ [[package]] name = "ctutils" -version = "0.4.2" +version = "0.4.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7d5515a3834141de9eafb9717ad39eea8247b5674e6066c404e8c4b365d2a29e" +checksum = "261af4dc7a06ea8a527a8717d8cbba3417530d6ea14e1edb3de6b67806be474f" dependencies = [ "cmov", ] @@ -1279,7 +1279,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" dependencies = [ "libc", - "windows-sys 0.61.2", + "windows-sys 0.52.0", ] [[package]] @@ -1598,6 +1598,7 @@ dependencies = [ "semver", "serde", "serde_json", + "serde_yaml", "sha2 0.10.9", "tar", "thiserror 1.0.69", @@ -1903,7 +1904,7 @@ dependencies = [ "tokio", "tokio-rustls", "tower-service", - "webpki-roots 1.0.9", + "webpki-roots 1.0.11", ] [[package]] @@ -2439,7 +2440,7 @@ version = "0.50.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7957b9740744892f114936ab4a57b3f487491bbeafaf8083688b16841a4240e5" dependencies = [ - "windows-sys 0.61.2", + "windows-sys 0.59.0", ] [[package]] @@ -2817,7 +2818,7 @@ dependencies = [ "once_cell", "socket2", "tracing", - "windows-sys 0.61.2", + "windows-sys 0.52.0", ] [[package]] @@ -2992,7 +2993,7 @@ dependencies = [ "wasm-bindgen-futures", "wasm-streams", "web-sys", - "webpki-roots 1.0.9", + "webpki-roots 1.0.11", ] [[package]] @@ -3067,14 +3068,14 @@ dependencies = [ "errno", "libc", "linux-raw-sys 0.12.1", - "windows-sys 0.61.2", + "windows-sys 0.52.0", ] [[package]] name = "rustls" -version = "0.23.45" +version = "0.23.46" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0d41d731c7d2f962d1ccc364cec258de3c0e93b38c2fb3ba97ac74513048d634" +checksum = "48e13bd8c0e9365c43cfa5c9e8f9ad49d3c8444926c9aac819e0e4dc503c8fdf" dependencies = [ "aws-lc-rs", "log", @@ -3119,9 +3120,9 @@ dependencies = [ [[package]] name = "rustls-webpki" -version = "0.103.15" +version = "0.103.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f3c3cf1d8b1e7d4927e2d154c3fcb02979afb9939629c62cd9048d4f07b60ac2" +checksum = "4443b1b6d39d7c663139942cc4558262562d2c84123bb6282ef177009ca8b281" dependencies = [ "aws-lc-rs", "ring", @@ -3974,9 +3975,9 @@ checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" [[package]] name = "tokio" -version = "1.53.1" +version = "1.54.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "202caea871b69668250d242070849eb495be178ed697a3e98aebce5bc81a0bed" +checksum = "b52fd3255258d26fac7264ab5a76b53c13367136dc011d3d6ad4f8ed070396a3" dependencies = [ "bytes", "libc", @@ -4530,14 +4531,14 @@ version = "0.26.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "521bc38abb08001b01866da9f51eb7c5d647a19260e00054a8c7fd5f9e57f7a9" dependencies = [ - "webpki-roots 1.0.9", + "webpki-roots 1.0.11", ] [[package]] name = "webpki-roots" -version = "1.0.9" +version = "1.0.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7dcd9d09a39985f5344844e66b0c530a33843579125f23e21e9f0f220850f22a" +checksum = "f5f280bbb2b287bc8e7de7e3852945a0ff50d1bbcb5ed42b6c7a568c546df171" dependencies = [ "rustls-pki-types", ] @@ -4574,7 +4575,7 @@ version = "0.1.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22" dependencies = [ - "windows-sys 0.61.2", + "windows-sys 0.48.0", ] [[package]] diff --git a/apps/dashboard/package-lock.json b/apps/dashboard/package-lock.json index d233162..0867d0a 100644 --- a/apps/dashboard/package-lock.json +++ b/apps/dashboard/package-lock.json @@ -13,7 +13,7 @@ "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", "lucide-react": "^1.34.0", - "next": "^16.3.4", + "next": "^16.4.5", "next-themes": "^0.4.6", "pg": "8.23.0", "pg-connection-string": "2.14.0", @@ -927,18 +927,6 @@ "integrity": "sha512-HpCo8tmWzLVad5s2d19EhAz5zqrrQ6s69qd6moPMQvkOuSwDT1YgRfWSVuc4ennqrgv3OHppiOGMQ7oC13yIww==", "license": "MIT" }, - "node_modules/@hono/node-server": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/@hono/node-server/-/node-server-2.1.1.tgz", - "integrity": "sha512-ELuehkj5VCBdgEw9zs+ivkKwyzzUCSQuE96YmiPvn1ECBoZCczbFXJLeEGMTYjphP6gydh4pHMqEYPVMYUVgQg==", - "license": "MIT", - "engines": { - "node": ">=20" - }, - "peerDependencies": { - "hono": "^4" - } - }, "node_modules/@img/colour": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/@img/colour/-/colour-1.1.0.tgz", @@ -950,9 +938,9 @@ } }, "node_modules/@img/sharp-darwin-arm64": { - "version": "0.35.4", - "resolved": "https://registry.npmjs.org/@img/sharp-darwin-arm64/-/sharp-darwin-arm64-0.35.4.tgz", - "integrity": "sha512-Uhfl4V4lhP2nbUVF9+hyH1+luj86f1gUFeo8ALYxFoULoU+G87D43BfeMP8XHsk9boxAnCY/bf2EHwhA7MuGsA==", + "version": "0.35.5", + "resolved": "https://registry.npmjs.org/@img/sharp-darwin-arm64/-/sharp-darwin-arm64-0.35.5.tgz", + "integrity": "sha512-QRUlFQ0WxvdWyqqG/WtI3iupfD5rBzmCHXSdPsY91sAtVtTo7Q4cb6zOccZ3gqEqkr0f1As1ehLqmEpDsRf+lg==", "cpu": [ "arm64" ], @@ -968,13 +956,13 @@ "url": "https://opencollective.com/libvips" }, "optionalDependencies": { - "@img/sharp-libvips-darwin-arm64": "1.3.3" + "@img/sharp-libvips-darwin-arm64": "1.3.4" } }, "node_modules/@img/sharp-darwin-x64": { - "version": "0.35.4", - "resolved": "https://registry.npmjs.org/@img/sharp-darwin-x64/-/sharp-darwin-x64-0.35.4.tgz", - "integrity": "sha512-hWniXY3bG5qKpkKrAwPe4y+VTPmf086YQAnkxWh7uA1YrlRouWGa0M0Mxj3ZjnXFkv7/TD1bTy9lGUK26vRvWw==", + "version": "0.35.5", + "resolved": "https://registry.npmjs.org/@img/sharp-darwin-x64/-/sharp-darwin-x64-0.35.5.tgz", + "integrity": "sha512-+BR255RhDlpygUpOc/Jdt1nT6DQ3XG/ERo5wbcdOf5Q320dKtPCKPLR1LJs9VGXRaMa8l1uUa0tkCNOXiAxZUw==", "cpu": [ "x64" ], @@ -990,20 +978,20 @@ "url": "https://opencollective.com/libvips" }, "optionalDependencies": { - "@img/sharp-libvips-darwin-x64": "1.3.3" + "@img/sharp-libvips-darwin-x64": "1.3.4" } }, "node_modules/@img/sharp-freebsd-wasm32": { - "version": "0.35.4", - "resolved": "https://registry.npmjs.org/@img/sharp-freebsd-wasm32/-/sharp-freebsd-wasm32-0.35.4.tgz", - "integrity": "sha512-lIsKw/BU+kjB4eZjxrYrZmwOJYi3Ajrv66iAlBmUPyKc3HpnloevB1g3wxGD9P/5BbQ1brBGl65VRRrCvQDEqA==", + "version": "0.35.5", + "resolved": "https://registry.npmjs.org/@img/sharp-freebsd-wasm32/-/sharp-freebsd-wasm32-0.35.5.tgz", + "integrity": "sha512-Y/z91nEZ4uIBX5X3nfTovjU9lHNKFYbL2lpHCLVNmXQK03VIZvXBBt0KxbPGp2SdGSF+2mQU4e+hQaWOt86iAw==", "license": "Apache-2.0", "optional": true, "os": [ "freebsd" ], "dependencies": { - "@img/sharp-wasm32": "0.35.4" + "@img/sharp-wasm32": "0.35.5" }, "engines": { "node": ">=20.9.0" @@ -1013,9 +1001,9 @@ } }, "node_modules/@img/sharp-libvips-darwin-arm64": { - "version": "1.3.3", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-arm64/-/sharp-libvips-darwin-arm64-1.3.3.tgz", - "integrity": "sha512-suTBPTDGrI9WodccaDdwZItTSaBYASlBk1NSfElSHrUfzu3szG6lvIF58+WiFvnfzuK8ZBFS5zE00PxqxnRiPg==", + "version": "1.3.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-arm64/-/sharp-libvips-darwin-arm64-1.3.4.tgz", + "integrity": "sha512-5R89nBYiRdUlSWJxPhO+GVtaXzXSxKnRu/xqMn3KTA3L9EB9Oy/P+Nn2f2vlhPuUdy/Zusb2DarbyTpGCfEDuw==", "cpu": [ "arm64" ], @@ -1029,9 +1017,9 @@ } }, "node_modules/@img/sharp-libvips-darwin-x64": { - "version": "1.3.3", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-x64/-/sharp-libvips-darwin-x64-1.3.3.tgz", - "integrity": "sha512-FVJZ5mITMobmXIz/hPDTw0EintTW5H3WfrxwLqEqjiIihlu+hVRyGrFQ60xl0Lxn7Bt3zdpevPaQi0HEzqz9fw==", + "version": "1.3.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-x64/-/sharp-libvips-darwin-x64-1.3.4.tgz", + "integrity": "sha512-iR2OKH80yi0U+dUplyh3/xdpFvps6YkCwsXenIJxqxR1v9o+xtKTGbS9H7cps+2Vxjc8B1j96p75NmTGjIhtpQ==", "cpu": [ "x64" ], @@ -1045,9 +1033,9 @@ } }, "node_modules/@img/sharp-libvips-linux-arm": { - "version": "1.3.3", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm/-/sharp-libvips-linux-arm-1.3.3.tgz", - "integrity": "sha512-3rbU4vqXXc3hY/OiXdl52xZvT0F1yEngWfvqudtPJg/KkyiaQw2DRsFrNzpmLvfavbwOq3qXn36GP8obHRULQA==", + "version": "1.3.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm/-/sharp-libvips-linux-arm-1.3.4.tgz", + "integrity": "sha512-LmRtTsOHuvM2+wlO2Db37dx5MiZhB0FvSunciw48YjdOkZz9KAiRbm8ujeMOA1INqmei5NapFxYEK1D1ZSidmw==", "cpu": [ "arm" ], @@ -1061,9 +1049,9 @@ } }, "node_modules/@img/sharp-libvips-linux-arm64": { - "version": "1.3.3", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm64/-/sharp-libvips-linux-arm64-1.3.3.tgz", - "integrity": "sha512-0DaL0A6Xu6sQSQFwe4iVCrKWU2cCTItnRsYsCdxAMm9NF6twAA9BKnoqy4hqz4+azQ0JHuA26qiUKsf1XJ/v5A==", + "version": "1.3.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm64/-/sharp-libvips-linux-arm64-1.3.4.tgz", + "integrity": "sha512-Y3dgX/6lE2QhQb+Gxy0WZxfg9MEm/JBjamZpS2IklP7xIQoKN4hzAm7KcMVGtaVDt3neE9OKBC7vAfonA/Lr1A==", "cpu": [ "arm64" ], @@ -1077,9 +1065,9 @@ } }, "node_modules/@img/sharp-libvips-linux-ppc64": { - "version": "1.3.3", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-ppc64/-/sharp-libvips-linux-ppc64-1.3.3.tgz", - "integrity": "sha512-cdn1OvUBwsXhbC0zSzJnNzf5MZ/mTrobawDvNXBTxe8VtqKAm0sRuEY2Evzovb/w9JMk4TvRxqt1mekSuJz64w==", + "version": "1.3.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-ppc64/-/sharp-libvips-linux-ppc64-1.3.4.tgz", + "integrity": "sha512-Le6boB8Tai0Nis+gIxIpKx68UDVVIqdR8Tin5Yf1z2LJJQLDJvCDRqRu+jC2qCoD+eIomonmOwB4smBRxfVpYQ==", "cpu": [ "ppc64" ], @@ -1093,9 +1081,9 @@ } }, "node_modules/@img/sharp-libvips-linux-riscv64": { - "version": "1.3.3", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-riscv64/-/sharp-libvips-linux-riscv64-1.3.3.tgz", - "integrity": "sha512-HjPVx7yKz+0lqdhDlTw1tt90wamBoxhiXpvl1XZpJLiHH4RCJ5yDTqH+VlYPv2fwFs89JFw4c1IexYOcQUi4IQ==", + "version": "1.3.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-riscv64/-/sharp-libvips-linux-riscv64-1.3.4.tgz", + "integrity": "sha512-aHkkIEHPRdQEegJN20MLmGtxYD9R2wQr3Cwpddnu5+YKMt6Uzax7S9h5gpZTo8wyrGuZSlfQ63OevL5mTyOC7Q==", "cpu": [ "riscv64" ], @@ -1109,9 +1097,9 @@ } }, "node_modules/@img/sharp-libvips-linux-s390x": { - "version": "1.3.3", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-s390x/-/sharp-libvips-linux-s390x-1.3.3.tgz", - "integrity": "sha512-neWLh+3yCNThxnfy3c4BbVBeGgt9aftno+XbT56iK28RgeDs3UOFWviLWlUu0bArYVYJaFDK+RRohbicUNCm8Q==", + "version": "1.3.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-s390x/-/sharp-libvips-linux-s390x-1.3.4.tgz", + "integrity": "sha512-ra/mB6MikESDUO7Yg+Mi95bFBb9GsObURuhnOv3OqknjGe9sZrG8tCe9q0xSIGrtLgvgw0gKnFWcK4blSgQOuQ==", "cpu": [ "s390x" ], @@ -1125,9 +1113,9 @@ } }, "node_modules/@img/sharp-libvips-linux-x64": { - "version": "1.3.3", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-x64/-/sharp-libvips-linux-x64-1.3.3.tgz", - "integrity": "sha512-4vKmvAst9nrowcqquKFAyZJUDolUaIp8uRiN0mWFguJ1IplC9/pitXtlnnlU4aa/eJw3J7i67V+pwUL+wZGdsA==", + "version": "1.3.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-x64/-/sharp-libvips-linux-x64-1.3.4.tgz", + "integrity": "sha512-GJ//SSXbnwSDes02umB3nDJLFcQzw8a18V8fyhqr6tV515tOEMdImjjxj1AoafMRz56F3PHgftnj1QEKSU1zkw==", "cpu": [ "x64" ], @@ -1141,9 +1129,9 @@ } }, "node_modules/@img/sharp-libvips-linuxmusl-arm64": { - "version": "1.3.3", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-arm64/-/sharp-libvips-linuxmusl-arm64-1.3.3.tgz", - "integrity": "sha512-Y9kQaLMuNoB0bPYOOdcZMaseNrFpPodIWWMrx+CZyydf2xn68j9WYc6sWWRrDwNkzCQjKYfc68L7jKjGlHMibw==", + "version": "1.3.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-arm64/-/sharp-libvips-linuxmusl-arm64-1.3.4.tgz", + "integrity": "sha512-hvulFwtjUcagsis6BBxHwGFwWoNZjgYmULGVrZcyfNbjA8hKILbRxGg15/7w5HDyXHXUos/j6baAWqnCyQ2DWA==", "cpu": [ "arm64" ], @@ -1157,9 +1145,9 @@ } }, "node_modules/@img/sharp-libvips-linuxmusl-x64": { - "version": "1.3.3", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-x64/-/sharp-libvips-linuxmusl-x64-1.3.3.tgz", - "integrity": "sha512-fj8Mv0HHfD1Rr+4I68+3agJynxDWtBFgicTbSOb9Bke6pIwzGcJ+RX/yHjmiEGFMCavY/dxvem7MyNaJF+wDiw==", + "version": "1.3.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-x64/-/sharp-libvips-linuxmusl-x64-1.3.4.tgz", + "integrity": "sha512-6zXKeE/p39I1AmA3cJG35eyBGNqNddLnUXjhwBnsGjFPWqf5VKkDBEqaEkPDoTEtkxwi2vv8Tcr2mDyP4So7Fg==", "cpu": [ "x64" ], @@ -1173,9 +1161,9 @@ } }, "node_modules/@img/sharp-linux-arm": { - "version": "0.35.4", - "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm/-/sharp-linux-arm-0.35.4.tgz", - "integrity": "sha512-7OAS8gI0EReKGVN2HssHlM6umJgxF5VI3xN0p9FA91p/YO+ou5hiNghLdZ5BEHztwaaK5+bLKRf8x/o2L2nk9A==", + "version": "0.35.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm/-/sharp-linux-arm-0.35.5.tgz", + "integrity": "sha512-LEaXK2WdXVK5ykcw0buWyPMsmLLL2vpHLD6yrNSW+JGEL3BZPA4tpKN6iaMc4AxTTAoaX/sU1rOL51lcIz48ZQ==", "cpu": [ "arm" ], @@ -1191,13 +1179,13 @@ "url": "https://opencollective.com/libvips" }, "optionalDependencies": { - "@img/sharp-libvips-linux-arm": "1.3.3" + "@img/sharp-libvips-linux-arm": "1.3.4" } }, "node_modules/@img/sharp-linux-arm64": { - "version": "0.35.4", - "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm64/-/sharp-linux-arm64-0.35.4.tgz", - "integrity": "sha512-De4jpEnAU8Hd5oT0j1G3uL4ZvTuipVMn7YC6vPaJhy6/7EwEae0SVAoBrUMYQbkLGDm85taVWwuPc1a44LTzCQ==", + "version": "0.35.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm64/-/sharp-linux-arm64-0.35.5.tgz", + "integrity": "sha512-LYVx5JTsOM2CBzmxreh+nl64/3H6Xb09iSLknqH47z2T2DFFxDeFLP5y4dJwe6H7uGQlHPyEEtIqyo3DYsRwdQ==", "cpu": [ "arm64" ], @@ -1213,13 +1201,13 @@ "url": "https://opencollective.com/libvips" }, "optionalDependencies": { - "@img/sharp-libvips-linux-arm64": "1.3.3" + "@img/sharp-libvips-linux-arm64": "1.3.4" } }, "node_modules/@img/sharp-linux-ppc64": { - "version": "0.35.4", - "resolved": "https://registry.npmjs.org/@img/sharp-linux-ppc64/-/sharp-linux-ppc64-0.35.4.tgz", - "integrity": "sha512-2oYZJeIl4kCcMGk4ouZVjnkCtFrpQFlNEtJ6GbxzhHQchwH0NH/qEb9ykmOl29dqwMq+JhFdZn+1ak2FKhI9fQ==", + "version": "0.35.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-ppc64/-/sharp-linux-ppc64-0.35.5.tgz", + "integrity": "sha512-QVxAAq8evVRI9ia2vqgwrmWucn5Dfv+JdWzj75pD8omHLPSP7f8p20O8jxzjCcuCEQEOtYOZUmX1hkiZ0kdevA==", "cpu": [ "ppc64" ], @@ -1235,13 +1223,13 @@ "url": "https://opencollective.com/libvips" }, "optionalDependencies": { - "@img/sharp-libvips-linux-ppc64": "1.3.3" + "@img/sharp-libvips-linux-ppc64": "1.3.4" } }, "node_modules/@img/sharp-linux-riscv64": { - "version": "0.35.4", - "resolved": "https://registry.npmjs.org/@img/sharp-linux-riscv64/-/sharp-linux-riscv64-0.35.4.tgz", - "integrity": "sha512-cPbNChoRURAWdebDIHSenxRpgEdy7JkPydSnUxRm9VvKD7m0/xVaR/8Fzlu81pk5nHEvHH87UZUA7cTtwnbJSA==", + "version": "0.35.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-riscv64/-/sharp-linux-riscv64-0.35.5.tgz", + "integrity": "sha512-LtdreXguaavKODPIfzJ4kffx7UNt1omwtK0rch4EBbbSTXPnxWmYSayXdLJw0fJzQ97kHt1gL/yh4tvU+nCyRQ==", "cpu": [ "riscv64" ], @@ -1257,13 +1245,13 @@ "url": "https://opencollective.com/libvips" }, "optionalDependencies": { - "@img/sharp-libvips-linux-riscv64": "1.3.3" + "@img/sharp-libvips-linux-riscv64": "1.3.4" } }, "node_modules/@img/sharp-linux-s390x": { - "version": "0.35.4", - "resolved": "https://registry.npmjs.org/@img/sharp-linux-s390x/-/sharp-linux-s390x-0.35.4.tgz", - "integrity": "sha512-RY0JFY8Fd6RonCBtHz+DvadaPkXDSI1AUn6yWL9TipqkZ1vY8w8evqdgyDFnkm4/K1ve1TvZiaePP5oSd4+WVQ==", + "version": "0.35.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-s390x/-/sharp-linux-s390x-0.35.5.tgz", + "integrity": "sha512-UZasTOFiYzotTsGOCu42BfUzP6Tu6Do/947iRm1RsLKvlllxwGcn4RN27LibGWceix4Y+Pmw3jsnTcCQIgWjqA==", "cpu": [ "s390x" ], @@ -1279,13 +1267,13 @@ "url": "https://opencollective.com/libvips" }, "optionalDependencies": { - "@img/sharp-libvips-linux-s390x": "1.3.3" + "@img/sharp-libvips-linux-s390x": "1.3.4" } }, "node_modules/@img/sharp-linux-x64": { - "version": "0.35.4", - "resolved": "https://registry.npmjs.org/@img/sharp-linux-x64/-/sharp-linux-x64-0.35.4.tgz", - "integrity": "sha512-9qvvEAuk8k89TfWUoX2htWjbAMX8p+NxCppjpcg5k6xMsjhBQPTsoIh36h9Qde4WRuGpJeYnOjdosDn/cnv+OA==", + "version": "0.35.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-x64/-/sharp-linux-x64-0.35.5.tgz", + "integrity": "sha512-SxFtLTeJInhAA9Q836kux2vZNeOBQEx658qvbboZScr0wIARym3IcGmW7KpVD5sbVg0Ojy+udFQdayYIZyoNog==", "cpu": [ "x64" ], @@ -1301,13 +1289,13 @@ "url": "https://opencollective.com/libvips" }, "optionalDependencies": { - "@img/sharp-libvips-linux-x64": "1.3.3" + "@img/sharp-libvips-linux-x64": "1.3.4" } }, "node_modules/@img/sharp-linuxmusl-arm64": { - "version": "0.35.4", - "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-arm64/-/sharp-linuxmusl-arm64-0.35.4.tgz", - "integrity": "sha512-KB5jxpfWQTr0nc3xdHtWChdbifHrBGsd2SM62Eyxrl8afikm+f5qGBU75SJIZBT/S1MC8XyacdlXBMSWq6OURA==", + "version": "0.35.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-arm64/-/sharp-linuxmusl-arm64-0.35.5.tgz", + "integrity": "sha512-9HbMclmI1zlNkFRs3z9/eBtDjfD0sGlrX1z6b1qwmiFY5ElDLh4BC0LPBdVp7z1DXFiKlIcznf+ZlsuZzLxQqg==", "cpu": [ "arm64" ], @@ -1323,13 +1311,13 @@ "url": "https://opencollective.com/libvips" }, "optionalDependencies": { - "@img/sharp-libvips-linuxmusl-arm64": "1.3.3" + "@img/sharp-libvips-linuxmusl-arm64": "1.3.4" } }, "node_modules/@img/sharp-linuxmusl-x64": { - "version": "0.35.4", - "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-x64/-/sharp-linuxmusl-x64-0.35.4.tgz", - "integrity": "sha512-f+eZJZIQNEEd26RPSW+76chwOf1XtA2Y/O+5ocVyLliHkeih3e+jhLVBdNTd2rS3IbNXK8+ug93Vf5ZXtF5Lxg==", + "version": "0.35.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-x64/-/sharp-linuxmusl-x64-0.35.5.tgz", + "integrity": "sha512-4KOphqB035HrVdqLZfCgMzzERrQkkzOwRhl4OAkRO1YCldbaFjySXMaK534Mo0V+LndnlJk+sbUyLeU0ULyD1A==", "cpu": [ "x64" ], @@ -1345,13 +1333,13 @@ "url": "https://opencollective.com/libvips" }, "optionalDependencies": { - "@img/sharp-libvips-linuxmusl-x64": "1.3.3" + "@img/sharp-libvips-linuxmusl-x64": "1.3.4" } }, "node_modules/@img/sharp-wasm32": { - "version": "0.35.4", - "resolved": "https://registry.npmjs.org/@img/sharp-wasm32/-/sharp-wasm32-0.35.4.tgz", - "integrity": "sha512-zQnl4Kwp7Q6NHsENtU2T/00Zi+w3AQNwz3+UaTyVBy2FpXrzXzGjndpK61onhZjRtRpQXxCTeqw19bVyXOh7jA==", + "version": "0.35.5", + "resolved": "https://registry.npmjs.org/@img/sharp-wasm32/-/sharp-wasm32-0.35.5.tgz", + "integrity": "sha512-Ptsga1su4tQx+LLF1ECS9U6nz5kmrXKo6XVbtR48Ke3ZRxxgaWBu7IDtEe1quo8hiupwm6WFqxVlXaSf7IINGQ==", "license": "Apache-2.0 AND LGPL-3.0-or-later AND MIT", "optional": true, "dependencies": { @@ -1365,16 +1353,16 @@ } }, "node_modules/@img/sharp-webcontainers-wasm32": { - "version": "0.35.4", - "resolved": "https://registry.npmjs.org/@img/sharp-webcontainers-wasm32/-/sharp-webcontainers-wasm32-0.35.4.tgz", - "integrity": "sha512-ESfNkywmCfPNyaZjxooddJQiQ+l/nTpGEOGthxiLnIHXC/CmcBixnfwUleX9mCz9ovrUUvKMap/pm8RYbzfwaA==", + "version": "0.35.5", + "resolved": "https://registry.npmjs.org/@img/sharp-webcontainers-wasm32/-/sharp-webcontainers-wasm32-0.35.5.tgz", + "integrity": "sha512-hfhF/FmoQyTUkA0bIKFOtw536BQSeBMe6BF6QyWlrPxT754+TFLaZ7sKKTfvvM0yJgKgaYTwnFCIZ/GuDw5SUA==", "cpu": [ "wasm32" ], "license": "Apache-2.0", "optional": true, "dependencies": { - "@img/sharp-wasm32": "0.35.4" + "@img/sharp-wasm32": "0.35.5" }, "engines": { "node": ">=20.9.0" @@ -1384,9 +1372,9 @@ } }, "node_modules/@img/sharp-win32-arm64": { - "version": "0.35.4", - "resolved": "https://registry.npmjs.org/@img/sharp-win32-arm64/-/sharp-win32-arm64-0.35.4.tgz", - "integrity": "sha512-iNdlBX9gLVvqe2I3uIJSIKTq6wckP/DYxZtcqxm09x5Gi24DnFBmPAWZmr60ZyYMG0xlzo6goG3670ar+RXvRw==", + "version": "0.35.5", + "resolved": "https://registry.npmjs.org/@img/sharp-win32-arm64/-/sharp-win32-arm64-0.35.5.tgz", + "integrity": "sha512-X4t7g+7ZA5DKblCBEXGjUqqemj4vczING/5viFwAL8h4N3qYeyjwdCvRLHi4EdOUI+2Z7UFlp1VM+p/AuEtm6Q==", "cpu": [ "arm64" ], @@ -1403,9 +1391,9 @@ } }, "node_modules/@img/sharp-win32-ia32": { - "version": "0.35.4", - "resolved": "https://registry.npmjs.org/@img/sharp-win32-ia32/-/sharp-win32-ia32-0.35.4.tgz", - "integrity": "sha512-kqRsbaa5CS6KHlpxnN7WhE6vAAugXyZButpRdvDWetlv6Qv4N9WTcrWzF7tXfB9T7MsoadqdI8hmwLq6UlLvtw==", + "version": "0.35.5", + "resolved": "https://registry.npmjs.org/@img/sharp-win32-ia32/-/sharp-win32-ia32-0.35.5.tgz", + "integrity": "sha512-5Zm82LoBc43nhwNybZlG7Y1KO//Zhsn306fQl29ZOuStHLGTo3BWL83q3cznX0poxSAMuYL1On/BHBxkBeKr6A==", "cpu": [ "ia32" ], @@ -1422,9 +1410,9 @@ } }, "node_modules/@img/sharp-win32-x64": { - "version": "0.35.4", - "resolved": "https://registry.npmjs.org/@img/sharp-win32-x64/-/sharp-win32-x64-0.35.4.tgz", - "integrity": "sha512-XtmnYhBcrORsJ4XJngyzr/EWP0hRZLAZRFaApdKuviyqF78+ylxh2y06ZmtULAMOnObJ3ucpN0AcwSWnMowTRg==", + "version": "0.35.5", + "resolved": "https://registry.npmjs.org/@img/sharp-win32-x64/-/sharp-win32-x64-0.35.5.tgz", + "integrity": "sha512-x76eH0vEiHlcMQu8Y8IenntaACtddpT6W0wmXtWrnKcnKI7ME5DdgqhAD6SEWOEl1v2zDvkZDhFA9KnURwpfqg==", "cpu": [ "x64" ], @@ -1485,56 +1473,41 @@ "@jridgewell/sourcemap-codec": "^1.4.14" } }, - "node_modules/@modelcontextprotocol/sdk": { - "version": "1.30.0", - "resolved": "https://registry.npmjs.org/@modelcontextprotocol/sdk/-/sdk-1.30.0.tgz", - "integrity": "sha512-xKd8OIzlqNzcqcNumGAa6g+PW2kjD5vrpcKOnfldAUPP3j7lnqMPwlTXQm8gF+UwH72z0lqaRbjr9hqGz0eITA==", - "license": "MIT", + "node_modules/@modelcontextprotocol/core": { + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/@modelcontextprotocol/core/-/core-2.4.0.tgz", + "integrity": "sha512-z8QBKqdfXLPzkLQuG4xp8hrVEMm1PZ1IgxMN2sMbJiFiAKOWXnAzogXn2W4aqIBlP1ea6U5yiqVquvUNM1Cj6Q==", + "license": "Apache-2.0", "dependencies": { - "@hono/node-server": "^1.19.9 || ^2.0.5", - "ajv": "^8.17.1", - "ajv-formats": "^3.0.1", - "content-type": "^1.0.5", - "cors": "^2.8.5", - "cross-spawn": "^7.0.5", - "eventsource": "^3.0.2", - "eventsource-parser": "^3.0.0", - "express": "^5.2.1", - "express-rate-limit": "^8.2.1", - "hono": "^4.11.4", - "jose": "^6.1.3", - "json-schema-typed": "^8.0.2", - "pkce-challenge": "^5.0.0", - "raw-body": "^3.0.0", - "zod": "^3.25 || ^4.0", - "zod-to-json-schema": "^3.25.1" + "zod": "^4.2.0" }, "engines": { - "node": ">=18" - }, - "peerDependencies": { - "@cfworker/json-schema": "^4.1.1", - "zod": "^3.25 || ^4.0" + "node": ">=20" + } + }, + "node_modules/@modelcontextprotocol/server": { + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/@modelcontextprotocol/server/-/server-2.4.0.tgz", + "integrity": "sha512-rgR9yxSaAJ+bcsi/XroT/S2AfPiIWloXAnrdfYSCms9ivVju0ishu/wW0J21Ho6+ZOR1r7RyeRbjiRuF87ix5Q==", + "license": "Apache-2.0", + "dependencies": { + "@modelcontextprotocol/core": "2.4.0", + "zod": "^4.2.0" }, - "peerDependenciesMeta": { - "@cfworker/json-schema": { - "optional": true - }, - "zod": { - "optional": false - } + "engines": { + "node": ">=20" } }, "node_modules/@next/env": { - "version": "16.3.4", - "resolved": "https://registry.npmjs.org/@next/env/-/env-16.3.4.tgz", - "integrity": "sha512-cjWZnUUa6jZq2kFaNe/ZyJdZonOZ/QoN0Zka2nz/FLOrfx14pQuM9c5RaSVkWMqgdt4ksgPAMWPyHSs/CyV48Q==", + "version": "16.4.5", + "resolved": "https://registry.npmjs.org/@next/env/-/env-16.4.5.tgz", + "integrity": "sha512-JwSPwpdkZL5VMl2jA/q+JI8MZdweAL3xmw5/tb9y/TLtmS0YXZnmYDzSihwk8xTj1OL+WM53gTqaj96yuLp6Xw==", "license": "MIT" }, "node_modules/@next/swc-darwin-arm64": { - "version": "16.3.4", - "resolved": "https://registry.npmjs.org/@next/swc-darwin-arm64/-/swc-darwin-arm64-16.3.4.tgz", - "integrity": "sha512-iBr3I5LZNk5/bgl5//iTgD2tcym14MX0Xo7fD//u9dYAEgGzza1y9oywluPtf74YnOswVdH1908aK9xVz7zQTw==", + "version": "16.4.5", + "resolved": "https://registry.npmjs.org/@next/swc-darwin-arm64/-/swc-darwin-arm64-16.4.5.tgz", + "integrity": "sha512-W5TniBzWKASbNNwHZ/S/f1MxQUXU/LdjklVMWA0Ujbfl7SZnUzFls45hO0i/sF7Bx1jJBWp7WWmoQajXtX/O8A==", "cpu": [ "arm64" ], @@ -1548,9 +1521,9 @@ } }, "node_modules/@next/swc-darwin-x64": { - "version": "16.3.4", - "resolved": "https://registry.npmjs.org/@next/swc-darwin-x64/-/swc-darwin-x64-16.3.4.tgz", - "integrity": "sha512-2dpiSyl2Jw/NrBPaU2MAKGSa+2MR82pJIn4Sm5Rjr+gxAeuh0z158Su3Z2O8zn7UNNq+ej4bToed6RcRN/Lydg==", + "version": "16.4.5", + "resolved": "https://registry.npmjs.org/@next/swc-darwin-x64/-/swc-darwin-x64-16.4.5.tgz", + "integrity": "sha512-uWq4qkHapBDS1oFltWS4YxTb9pLHAtLNcpbuK6SmRsebLYZqpBML/j/C0op+50JYGTpZgVL/DWNt/6oWlamrrw==", "cpu": [ "x64" ], @@ -1564,9 +1537,9 @@ } }, "node_modules/@next/swc-linux-arm64-gnu": { - "version": "16.3.4", - "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-16.3.4.tgz", - "integrity": "sha512-+t+U8HZT+fApePCS5h89CSH3datz29MkzyfCn+6fpsZBG/oiEOhINcb9rtkv6sdpToLGFn2e6146NzaKCXkqrA==", + "version": "16.4.5", + "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-16.4.5.tgz", + "integrity": "sha512-FdRcyOx5u5P51mlAqLB9EXdd8jZO5fsdHtk4BAQSkqOywCsbTNN/rmw+jO7pnmjNdAtu2AG46NzbCL2FPDrrgw==", "cpu": [ "arm64" ], @@ -1580,9 +1553,9 @@ } }, "node_modules/@next/swc-linux-arm64-musl": { - "version": "16.3.4", - "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-16.3.4.tgz", - "integrity": "sha512-mx03GNs1ocQA5JQ4FxDMmIsNkdrZh8cuezKCrId28e5/gIPU/l7Kcy2+vmCCzdjnnmXJy+iOAu+7K0QppO6Urg==", + "version": "16.4.5", + "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-16.4.5.tgz", + "integrity": "sha512-cyvmmgtRXR8aGVT86wi9Py53IlfyCx7P2fZxuAXAoTummDnhKtUyjrMeXfapqp4qgD+gT5RqyGD/VfA16jEK7Q==", "cpu": [ "arm64" ], @@ -1596,9 +1569,9 @@ } }, "node_modules/@next/swc-linux-x64-gnu": { - "version": "16.3.4", - "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-16.3.4.tgz", - "integrity": "sha512-YIhGY6fSMfha52bnVxnzc9zaVBzJg+cqQTOD8tXIBSx4fuv0pVMxQTE0PaS59YhnMOiYiG09IMwxJAf/CFm/Dw==", + "version": "16.4.5", + "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-16.4.5.tgz", + "integrity": "sha512-hPc+Fl+N22DAvqZHq559NmamXboOX5Lu8I5Wl0DezfrhTk1Zz21NQeYoZDmNI/NjTcrdsVxM72Fzn2lI+Q+vAw==", "cpu": [ "x64" ], @@ -1612,9 +1585,9 @@ } }, "node_modules/@next/swc-linux-x64-musl": { - "version": "16.3.4", - "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-16.3.4.tgz", - "integrity": "sha512-+eaaX6axpDb0yF1GCpiERe6njplvdC+nks/fKfcHu3XPGRrald8P3/X7yv7QLdjA51knnxwl9pxdIJsg+w1L+Q==", + "version": "16.4.5", + "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-16.4.5.tgz", + "integrity": "sha512-gbACYE8IP/jlprNTpiWXHb8N9CofOzpnEwH2SrIz1dUbiJf1FbDssDupCAPeP1Taxe8mgG20sGZamJI7KFOdZw==", "cpu": [ "x64" ], @@ -1628,9 +1601,9 @@ } }, "node_modules/@next/swc-win32-arm64-msvc": { - "version": "16.3.4", - "resolved": "https://registry.npmjs.org/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-16.3.4.tgz", - "integrity": "sha512-0jcXW7Xs/uzICrmgV3MhDYDeRy++1CqnpDIerlPIqYO4bhzB4WNbX/aRnQclustsAyTkFKB0z6rbcjmNg5tR8A==", + "version": "16.4.5", + "resolved": "https://registry.npmjs.org/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-16.4.5.tgz", + "integrity": "sha512-+8gj++ZMo8H20Xh2ijjNqyFk5LPLw0AC4Fs4Tq/G94rjjQXhOjN4jgSWsWKPFsyY26hfIeemJIwKtMxOsp5GBA==", "cpu": [ "arm64" ], @@ -1644,9 +1617,9 @@ } }, "node_modules/@next/swc-win32-x64-msvc": { - "version": "16.3.4", - "resolved": "https://registry.npmjs.org/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-16.3.4.tgz", - "integrity": "sha512-vvBzwu1pYQCp92maZCFCIw/XgOTMR5tur9GjakwIo2cmwRTMKajRZZDS9+e4KsUZWKu1E007WUeAFXRRjZeuzw==", + "version": "16.4.5", + "resolved": "https://registry.npmjs.org/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-16.4.5.tgz", + "integrity": "sha512-qNZj66pYXnaWLiF9R9GUbfBdrCv7kC6TaZfIwB2KH5WPc/8pEM3nVOj8hpQhWJU8S94rlH5MPDcX15+RrgUhiw==", "cpu": [ "x64" ], @@ -1733,6 +1706,40 @@ "integrity": "sha512-831qok9r2t8AlxLko40y2ebgSDhenenCatLVeW/uBtnHPyhHOvG0C7TvfgecV+wHzIm5KUICgzmVpWS+IMEAeg==", "license": "MIT" }, + "node_modules/@shadcn/registry": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/@shadcn/registry/-/registry-0.2.1.tgz", + "integrity": "sha512-wMvZ3DGdnhCwtpeFbwKybSGfCcP52bZX77jMKgZvupmirgyA5KRLlkeQXkI2RGHIaD8TX2iWe2wcIEjjqzg2FQ==", + "license": "MIT", + "dependencies": { + "@babel/core": "^7.28.0", + "@babel/parser": "^7.28.0", + "@babel/plugin-transform-typescript": "^7.28.0", + "@babel/preset-typescript": "^7.27.1", + "@dotenvx/dotenvx": "^1.48.4", + "cn": "^0.2.4", + "code-block-writer": "^13.0.3", + "deepmerge": "^4.3.1", + "execa": "^9.6.0", + "fast-glob": "^3.3.3", + "fs-extra": "^11.3.1", + "fuzzysort": "^3.1.0", + "kleur": "^4.1.5", + "ora": "^8.2.0", + "postcss": "^8.5.23", + "prompts": "^2.4.2", + "recast": "^0.23.11", + "socks": "^2.8.8", + "stringify-object": "^5.0.0", + "tsconfig-paths": "^4.2.0", + "type-fest": "^4.41.0", + "undici": "^7.30.1", + "zod": "^4.2.0" + }, + "engines": { + "node": ">=20.18.1" + } + }, "node_modules/@sindresorhus/merge-streams": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/@sindresorhus/merge-streams/-/merge-streams-4.0.0.tgz", @@ -2138,19 +2145,6 @@ "integrity": "sha512-lrpDziQipxCEeK5kWxvljWYhUvOiB2A9izZd9B2AFarYAkqZshb4lPbRs7zKEic6eGtH8V/2qJW+dPp9OtF6bw==", "license": "MIT" }, - "node_modules/accepts": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/accepts/-/accepts-2.0.0.tgz", - "integrity": "sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng==", - "license": "MIT", - "dependencies": { - "mime-types": "^3.0.0", - "negotiator": "^1.0.0" - }, - "engines": { - "node": ">= 0.6" - } - }, "node_modules/ajv": { "version": "8.20.0", "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.20.0.tgz", @@ -2167,23 +2161,6 @@ "url": "https://github.com/sponsors/epoberezkin" } }, - "node_modules/ajv-formats": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/ajv-formats/-/ajv-formats-3.0.1.tgz", - "integrity": "sha512-8iUql50EUR+uUcdRQ3HDqa6EVyo3docL8g5WJ3FNcWmu62IbkGUue/pEyLBW8VGKKucTPgqeks4fIU1DA4yowQ==", - "license": "MIT", - "dependencies": { - "ajv": "^8.0.0" - }, - "peerDependencies": { - "ajv": "^8.0.0" - }, - "peerDependenciesMeta": { - "ajv": { - "optional": true - } - } - }, "node_modules/ansi-colors": { "version": "4.1.3", "resolved": "https://registry.npmjs.org/ansi-colors/-/ansi-colors-4.1.3.tgz", @@ -2202,12 +2179,6 @@ "node": ">=8" } }, - "node_modules/argparse": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", - "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", - "license": "Python-2.0" - }, "node_modules/ast-types": { "version": "0.16.1", "resolved": "https://registry.npmjs.org/ast-types/-/ast-types-0.16.1.tgz", @@ -2239,9 +2210,10 @@ } }, "node_modules/baseline-browser-mapping": { - "version": "2.11.17", - "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.11.17.tgz", - "integrity": "sha512-KAUDn1OSS0fmPlGO+NOUMRcOQ/b/shUBH3OgkG73mPgdf+JD/BQ6fHboGxNOxnUmlwcq+lLq3dTkayRPuSfXwg==", + "version": "2.11.35", + "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.11.35.tgz", + "integrity": "sha512-GNk8KX52CHqeNKGk8+7tZnQab/2k1t/BgrUHPKNdijzpEhF1saPQZHVKb6m5loSAxB39gyrTYw34Qf9CQ3u1qQ==", + "license": "Apache-2.0", "bin": { "baseline-browser-mapping": "dist/cli.cjs" }, @@ -2383,47 +2355,10 @@ "@noble/hashes": "^2.0.1" } }, - "node_modules/body-parser": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-2.3.0.tgz", - "integrity": "sha512-2cGmJupaNgg+QUwVLAucDuWuoMZ6EX9iHDRswZ5lsNYEmwPaRknMPCLZz07yTzVq/83p4o/wzbDZbBrTvGGTIw==", - "license": "MIT", - "dependencies": { - "bytes": "^3.1.2", - "content-type": "^2.0.0", - "debug": "^4.4.3", - "http-errors": "^2.0.1", - "iconv-lite": "^0.7.2", - "on-finished": "^2.4.1", - "qs": "^6.15.2", - "raw-body": "^3.0.2", - "type-is": "^2.1.0" - }, - "engines": { - "node": ">=18" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/express" - } - }, - "node_modules/body-parser/node_modules/content-type": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/content-type/-/content-type-2.1.0.tgz", - "integrity": "sha512-mj7UPXE0jaqaOsukNZRUEfEi2AcL7C/vwmwcHV0O97eO1E1pxBZuyjlZrx5seTaNBg1U6+o35wpa35Qfcc+7ag==", - "license": "MIT", - "engines": { - "node": ">=18" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/express" - } - }, "node_modules/brace-expansion": { - "version": "5.0.9", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.9.tgz", - "integrity": "sha512-ScQ4IuvIEF1TMlP7Zt+vjJ//9zlPb2SDcxWxM3bk8s6t6GGdJ7KO1dCcTidOPJKePW30LE/2cT7wCyPho9/Wxg==", + "version": "5.0.12", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.12.tgz", + "integrity": "sha512-YovQ3rzhaLMIrDjNDMkNS01tea93qhEhG5xy8f6+R0l+dw3Ki+5sCoIoI942iuLZTHWogWktgwVDhU09iNEimQ==", "license": "MIT", "dependencies": { "balanced-match": "^4.0.2" @@ -2445,9 +2380,9 @@ } }, "node_modules/browserslist": { - "version": "4.28.8", - "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.8.tgz", - "integrity": "sha512-V2NpofLblG64mfOtSgDhOJESZEGogzDMBv/q+W6oc4LXWP/q75eOXoOaaOu1EOadB9U4Bwx/e0yzbvwKH8zalA==", + "version": "4.29.5", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.29.5.tgz", + "integrity": "sha512-khTmKu5NGYTotqcYo8Bew2k4RAE9PuXRSFRXG2lQBrCEQHyeQ/vX6gKMxfokwywBsLiXZMnHsmWMG1pnO8J7uQ==", "funding": [ { "type": "opencollective", @@ -2464,11 +2399,11 @@ ], "license": "MIT", "dependencies": { - "baseline-browser-mapping": "^2.11.12", - "caniuse-lite": "^1.0.30001809", - "electron-to-chromium": "^1.5.402", - "node-releases": "^2.0.53", - "update-browserslist-db": "^1.3.0" + "baseline-browser-mapping": "^2.11.33", + "caniuse-lite": "^1.0.30001819", + "electron-to-chromium": "^1.5.464", + "node-releases": "^2.0.60", + "update-browserslist-db": "^1.3.4" }, "bin": { "browserslist": "cli.js" @@ -2478,9 +2413,9 @@ } }, "node_modules/bundle-name": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/bundle-name/-/bundle-name-4.1.0.tgz", - "integrity": "sha512-tjwM5exMg6BGRI+kNmTntNsvdZS1X8BFYS6tnJ2hdH0kVxM6/eVZ2xy+FqStSWvYmtfFMDLIxurorHwDKfDz5Q==", + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/bundle-name/-/bundle-name-4.1.1.tgz", + "integrity": "sha512-DdH81/zPLVS11EUgWq3tEu/xn+EzljlMYooDNdzWEnFha3R3NBMpMV1UqYIpjYHV/SgpFKMIX1Oh7o06SjM/oA==", "license": "MIT", "dependencies": { "run-applescript": "^7.0.0" @@ -2492,57 +2427,10 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/bytes": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", - "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==", - "license": "MIT", - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/call-bind-apply-helpers": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", - "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", - "license": "MIT", - "dependencies": { - "es-errors": "^1.3.0", - "function-bind": "^1.1.2" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/call-bound": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz", - "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==", - "license": "MIT", - "dependencies": { - "call-bind-apply-helpers": "^1.0.2", - "get-intrinsic": "^1.3.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/callsites": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", - "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", - "license": "MIT", - "engines": { - "node": ">=6" - } - }, "node_modules/caniuse-lite": { - "version": "1.0.30001809", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001809.tgz", - "integrity": "sha512-xxWVywk6a6Arlk+hymeycyn/VgqEfLDxupvhH/xiY5SJ/18kmi9o6MiO320DCUzypORHLtvh0I4i04tUhCNHNQ==", + "version": "1.0.30001823", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001823.tgz", + "integrity": "sha512-TQ5s/djDjNk1axreUW+4rULH+BmdCMLiJkhaXnO5wqivG7tITkSUj4ItIdzqdZHKaXcHAV8dhH3JxOM7/7bwqw==", "funding": [ { "type": "opencollective", @@ -2556,7 +2444,8 @@ "type": "github", "url": "https://github.com/sponsors/ai" } - ] + ], + "license": "CC-BY-4.0" }, "node_modules/chalk": { "version": "5.6.2", @@ -2623,6 +2512,18 @@ "node": ">=6" } }, + "node_modules/cn": { + "version": "0.2.6", + "resolved": "https://registry.npmjs.org/cn/-/cn-0.2.6.tgz", + "integrity": "sha512-+i4L0zUGgRcEnhsxueVrP7iBGxBx5iD0WOTYg1MwFEu2ZyCmH5Ov2V1cul2Ht5UchRQQgftCf4be/RxspuW6QQ==", + "license": "MIT", + "bin": { + "cn": "bin/cn.mjs" + }, + "engines": { + "node": ">=20" + } + }, "node_modules/code-block-writer": { "version": "13.0.3", "resolved": "https://registry.npmjs.org/code-block-writer/-/code-block-writer-13.0.3.tgz", @@ -2685,95 +2586,12 @@ "integrity": "sha512-7DE8mpG+/fVw+dTpjbxnx47TaMnDfOI1jwft9g1VybltZCduyRQPJPvc+zzKY9WPHxhPWczyFuYa6I8Mw4iU5A==", "license": "BSD-2-Clause" }, - "node_modules/content-disposition": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-1.1.0.tgz", - "integrity": "sha512-5jRCH9Z/+DRP7rkvY83B+yGIGX96OYdJmzngqnw2SBSxqCFPd0w2km3s5iawpGX8krnwSGmF0FW5Nhr0Hfai3g==", - "license": "MIT", - "engines": { - "node": ">=18" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/express" - } - }, - "node_modules/content-type": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz", - "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==", - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, "node_modules/convert-source-map": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", "license": "MIT" }, - "node_modules/cookie": { - "version": "0.7.2", - "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz", - "integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==", - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/cookie-signature": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.2.2.tgz", - "integrity": "sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg==", - "license": "MIT", - "engines": { - "node": ">=6.6.0" - } - }, - "node_modules/cors": { - "version": "2.8.6", - "resolved": "https://registry.npmjs.org/cors/-/cors-2.8.6.tgz", - "integrity": "sha512-tJtZBBHA6vjIAaF6EnIaq6laBBP9aq/Y3ouVJjEfoHbRBcHBAHYcMh/w8LDrk2PvIMMq8gmopa5D4V8RmbrxGw==", - "license": "MIT", - "dependencies": { - "object-assign": "^4", - "vary": "^1" - }, - "engines": { - "node": ">= 0.10" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/express" - } - }, - "node_modules/cosmiconfig": { - "version": "9.0.2", - "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-9.0.2.tgz", - "integrity": "sha512-gtTZxTDau1wL7Y7zifc2dd8jHSK/k6BTx/2Xp/BpdlAdnlYWFVt7qhJqgwi7637yRwRQ3qL4ZidbB4I8tA5VOg==", - "license": "MIT", - "dependencies": { - "env-paths": "^2.2.1", - "import-fresh": "^3.3.0", - "js-yaml": "^4.1.0", - "parse-json": "^5.2.0" - }, - "engines": { - "node": ">=14" - }, - "funding": { - "url": "https://github.com/sponsors/d-fischer" - }, - "peerDependencies": { - "typescript": ">=4.9.5" - }, - "peerDependenciesMeta": { - "typescript": { - "optional": true - } - } - }, "node_modules/cross-spawn": { "version": "7.0.6", "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", @@ -2928,15 +2746,6 @@ "integrity": "sha512-7z22QmUWiQ/2d0KkdYmANbRUVABpZ9SNYyH5vx6PZ+nE5bcC0l7uFvEfHlyld/HcGBFTL536ClDt3DEcSlEJAQ==", "license": "MIT" }, - "node_modules/depd": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", - "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==", - "license": "MIT", - "engines": { - "node": ">= 0.8" - } - }, "node_modules/detect-libc": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", @@ -2981,30 +2790,10 @@ "url": "https://dotenvx.com" } }, - "node_modules/dunder-proto": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", - "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", - "license": "MIT", - "dependencies": { - "call-bind-apply-helpers": "^1.0.1", - "es-errors": "^1.3.0", - "gopd": "^1.2.0" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/ee-first": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", - "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==", - "license": "MIT" - }, "node_modules/electron-to-chromium": { - "version": "1.5.415", - "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.415.tgz", - "integrity": "sha512-958V+Kbhtgz+SxXeEVKBjrlKRBIDAYvUJfwhjxMZ5S6ut9jAl7l9ZKBkBrvjyjZE36PabLUo2L8kEeV5O4vgJg==", + "version": "1.5.478", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.478.tgz", + "integrity": "sha512-pftG+ma5Lm8O6/p45bhYT6StBN5ziQymaRzmCZwFl8DwWHKPPD93FdOmP1aqftcepolQYvrcyTLa17kE8Yf+jg==", "license": "ISC" }, "node_modules/emoji-regex": { @@ -3013,15 +2802,6 @@ "integrity": "sha512-toUI84YS5YmxW219erniWD0CIVOo46xGKColeNQRgOzDorgBi1v4D71/OFzgD9GO2UGKIv1C3Sp8DAn0+j5w7A==", "license": "MIT" }, - "node_modules/encodeurl": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz", - "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==", - "license": "MIT", - "engines": { - "node": ">= 0.8" - } - }, "node_modules/enhanced-resolve": { "version": "5.24.5", "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.24.5.tgz", @@ -3057,45 +2837,6 @@ "node": ">=6" } }, - "node_modules/error-ex": { - "version": "1.3.4", - "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.4.tgz", - "integrity": "sha512-sqQamAnR14VgCr1A618A3sGrygcpK+HEbenA/HiEAkkUwcZIIB/tgWqHFxWgOyDh4nB4JCRimh79dR5Ywc9MDQ==", - "license": "MIT", - "dependencies": { - "is-arrayish": "^0.2.1" - } - }, - "node_modules/es-define-property": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", - "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", - "license": "MIT", - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/es-errors": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", - "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", - "license": "MIT", - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/es-object-atoms": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.2.tgz", - "integrity": "sha512-HWcBoN6NileqtSydK2FqHbS/LoDd2pqrnQHLyJzBj4kOp/ky2MWMN694xOfkK8/SnUsW2DH7EfyVlydKCsm1Zw==", - "license": "MIT", - "dependencies": { - "es-errors": "^1.3.0" - }, - "engines": { - "node": ">= 0.4" - } - }, "node_modules/escalade": { "version": "3.2.0", "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", @@ -3105,12 +2846,6 @@ "node": ">=6" } }, - "node_modules/escape-html": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", - "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==", - "license": "MIT" - }, "node_modules/esprima": { "version": "4.0.1", "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz", @@ -3124,40 +2859,10 @@ "node": ">=4" } }, - "node_modules/etag": { - "version": "1.8.1", - "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", - "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==", - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/eventsource": { - "version": "3.0.7", - "resolved": "https://registry.npmjs.org/eventsource/-/eventsource-3.0.7.tgz", - "integrity": "sha512-CRT1WTyuQoD771GW56XEZFQ/ZoSfWid1alKGDYMmkt2yl8UXrVR4pspqWNEcqKvVIzg6PAltWjxcSSPrboA4iA==", - "license": "MIT", - "dependencies": { - "eventsource-parser": "^3.0.1" - }, - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/eventsource-parser": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/eventsource-parser/-/eventsource-parser-3.1.1.tgz", - "integrity": "sha512-EKN1vKAMcZ8MlYMpaNuxN6R9yakzH6uajHcHVTqWJzvu5pWw9DyhbP35HH8MVBQ+dZjAfDxk+A8NiR9KWaXiyQ==", - "license": "MIT", - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/execa": { - "version": "9.6.1", - "resolved": "https://registry.npmjs.org/execa/-/execa-9.6.1.tgz", - "integrity": "sha512-9Be3ZoN4LmYR90tUoVu2te2BsbzHfhJyfEiAVfz7N5/zv+jduIfLrV2xdQXOHbaD6KgpGdO9PRPM1Y4Q9QkPkA==", + "node_modules/execa": { + "version": "9.6.1", + "resolved": "https://registry.npmjs.org/execa/-/execa-9.6.1.tgz", + "integrity": "sha512-9Be3ZoN4LmYR90tUoVu2te2BsbzHfhJyfEiAVfz7N5/zv+jduIfLrV2xdQXOHbaD6KgpGdO9PRPM1Y4Q9QkPkA==", "license": "MIT", "dependencies": { "@sindresorhus/merge-streams": "^4.0.0", @@ -3180,68 +2885,6 @@ "url": "https://github.com/sindresorhus/execa?sponsor=1" } }, - "node_modules/express": { - "version": "5.2.1", - "resolved": "https://registry.npmjs.org/express/-/express-5.2.1.tgz", - "integrity": "sha512-hIS4idWWai69NezIdRt2xFVofaF4j+6INOpJlVOLDO8zXGpUVEVzIYk12UUi2JzjEzWL3IOAxcTubgz9Po0yXw==", - "license": "MIT", - "dependencies": { - "accepts": "^2.0.0", - "body-parser": "^2.2.1", - "content-disposition": "^1.0.0", - "content-type": "^1.0.5", - "cookie": "^0.7.1", - "cookie-signature": "^1.2.1", - "debug": "^4.4.0", - "depd": "^2.0.0", - "encodeurl": "^2.0.0", - "escape-html": "^1.0.3", - "etag": "^1.8.1", - "finalhandler": "^2.1.0", - "fresh": "^2.0.0", - "http-errors": "^2.0.0", - "merge-descriptors": "^2.0.0", - "mime-types": "^3.0.0", - "on-finished": "^2.4.1", - "once": "^1.4.0", - "parseurl": "^1.3.3", - "proxy-addr": "^2.0.7", - "qs": "^6.14.0", - "range-parser": "^1.2.1", - "router": "^2.2.0", - "send": "^1.1.0", - "serve-static": "^2.2.0", - "statuses": "^2.0.1", - "type-is": "^2.0.1", - "vary": "^1.1.2" - }, - "engines": { - "node": ">= 18" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/express" - } - }, - "node_modules/express-rate-limit": { - "version": "8.6.2", - "resolved": "https://registry.npmjs.org/express-rate-limit/-/express-rate-limit-8.6.2.tgz", - "integrity": "sha512-YH4ru+eOJxQABscKFfRCy9R7x9QFGdezclVMwwgFFndzS2Xnm0uo6B0ABZsLhcpeptGv2qvuJVWlQr9gQZoC3A==", - "license": "MIT", - "dependencies": { - "debug": "^4.4.3", - "ip-address": "^10.2.0" - }, - "engines": { - "node": ">= 16" - }, - "funding": { - "url": "https://github.com/sponsors/express-rate-limit" - }, - "peerDependencies": { - "express": ">= 4.11" - } - }, "node_modules/fast-deep-equal": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", @@ -3265,9 +2908,9 @@ } }, "node_modules/fast-uri": { - "version": "3.1.6", - "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.6.tgz", - "integrity": "sha512-7Ical1vFEMr0onbVzEDIreM22I4khW+fzyQPwvAFWBp1iwdshSZRsL4jjRvPG9JP1uiqMHRto+YU6R2/CzDz5Q==", + "version": "3.1.9", + "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.9.tgz", + "integrity": "sha512-hS/0Wt3Ya043oKXs0sIxrMpKQITXhMLIrVAKKXi4gozGC9v7YRWVnXIZzjNPTO02M9BdxLCWqK1yRXgNziVyJQ==", "funding": [ { "type": "github", @@ -3333,27 +2976,6 @@ "node": ">=8" } }, - "node_modules/finalhandler": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-2.1.1.tgz", - "integrity": "sha512-S8KoZgRZN+a5rNwqTxlZZePjT/4cnm0ROV70LedRHZ0p8u9fRID0hJUZQpkKLzro8LfmC8sx23bY6tVNxv8pQA==", - "license": "MIT", - "dependencies": { - "debug": "^4.4.0", - "encodeurl": "^2.0.0", - "escape-html": "^1.0.3", - "on-finished": "^2.4.1", - "parseurl": "^1.3.3", - "statuses": "^2.0.1" - }, - "engines": { - "node": ">= 18.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/express" - } - }, "node_modules/find-up": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/find-up/-/find-up-3.0.0.tgz", @@ -3366,24 +2988,6 @@ "node": ">=6" } }, - "node_modules/forwarded": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", - "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==", - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/fresh": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/fresh/-/fresh-2.0.0.tgz", - "integrity": "sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A==", - "license": "MIT", - "engines": { - "node": ">= 0.8" - } - }, "node_modules/fs-extra": { "version": "11.4.0", "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-11.4.0.tgz", @@ -3398,15 +3002,6 @@ "node": ">=14.14" } }, - "node_modules/function-bind": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", - "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", - "license": "MIT", - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, "node_modules/fuzzysort": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/fuzzysort/-/fuzzysort-3.1.0.tgz", @@ -3434,30 +3029,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/get-intrinsic": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", - "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", - "license": "MIT", - "dependencies": { - "call-bind-apply-helpers": "^1.0.2", - "es-define-property": "^1.0.1", - "es-errors": "^1.3.0", - "es-object-atoms": "^1.1.1", - "function-bind": "^1.1.2", - "get-proto": "^1.0.1", - "gopd": "^1.2.0", - "has-symbols": "^1.1.0", - "hasown": "^2.0.2", - "math-intrinsics": "^1.1.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, "node_modules/get-own-enumerable-keys": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/get-own-enumerable-keys/-/get-own-enumerable-keys-1.0.0.tgz", @@ -3470,19 +3041,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/get-proto": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", - "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", - "license": "MIT", - "dependencies": { - "dunder-proto": "^1.0.1", - "es-object-atoms": "^1.0.0" - }, - "engines": { - "node": ">= 0.4" - } - }, "node_modules/get-stream": { "version": "9.0.1", "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-9.0.1.tgz", @@ -3511,77 +3069,12 @@ "node": ">= 6" } }, - "node_modules/gopd": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", - "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", - "license": "MIT", - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, "node_modules/graceful-fs": { "version": "4.2.11", "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", "license": "ISC" }, - "node_modules/has-symbols": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", - "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", - "license": "MIT", - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/hasown": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.4.tgz", - "integrity": "sha512-T2UbfbBEF32wiepXIsMlTW9+dDYC6wMh/t/vYA4tuOMKqWz/n3vr1NFSxQiyP+zk2mXsoMA/i/7qV6LKut1t1A==", - "license": "MIT", - "dependencies": { - "function-bind": "^1.1.2" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/hono": { - "version": "4.13.5", - "resolved": "https://registry.npmjs.org/hono/-/hono-4.13.5.tgz", - "integrity": "sha512-O6+/eCYRkzzzy0rPWwKLiGBR1nFuUPZynnwjxN1MBA62NNqbT0wQEzQyK2gSO5yDIDB336sXQleAhOHrzlYyKw==", - "license": "MIT", - "engines": { - "node": ">=16.9.0" - } - }, - "node_modules/http-errors": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.1.tgz", - "integrity": "sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==", - "license": "MIT", - "dependencies": { - "depd": "~2.0.0", - "inherits": "~2.0.4", - "setprototypeof": "~1.2.0", - "statuses": "~2.0.2", - "toidentifier": "~1.0.1" - }, - "engines": { - "node": ">= 0.8" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/express" - } - }, "node_modules/human-signals": { "version": "8.0.1", "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-8.0.1.tgz", @@ -3591,22 +3084,6 @@ "node": ">=18.18.0" } }, - "node_modules/iconv-lite": { - "version": "0.7.3", - "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.7.3.tgz", - "integrity": "sha512-IKXpvIzjnC9XTAUbVBcMfGS0EPaIXtW6v+zr+RRp+hqULEpo0owZax6wyRwPOJbWbzjYspQwusTsfVr0ifh4uQ==", - "license": "MIT", - "dependencies": { - "safer-buffer": ">= 2.1.2 < 3.0.0" - }, - "engines": { - "node": ">=0.10.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/express" - } - }, "node_modules/ignore": { "version": "5.3.2", "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", @@ -3616,52 +3093,15 @@ "node": ">= 4" } }, - "node_modules/import-fresh": { - "version": "3.3.1", - "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.1.tgz", - "integrity": "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==", - "license": "MIT", - "dependencies": { - "parent-module": "^1.0.0", - "resolve-from": "^4.0.0" - }, - "engines": { - "node": ">=6" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/inherits": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", - "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", - "license": "ISC" - }, "node_modules/ip-address": { - "version": "10.5.0", - "resolved": "https://registry.npmjs.org/ip-address/-/ip-address-10.5.0.tgz", - "integrity": "sha512-R5SnVLJmgYYvf2F2ZgwSBnelz5G4q5AxIC277GDfUaNbrZKNANcBC7RHqYYePlszf4kBolVkJauG0ZjHHFh55g==", + "version": "10.7.6", + "resolved": "https://registry.npmjs.org/ip-address/-/ip-address-10.7.6.tgz", + "integrity": "sha512-00LYkM6byILa9eQIx6qoaXCM40xTLd56AZ4vyaPW3YTyb49VZYPo5xsiAq4tuQ54GH4R7gVdYScJ1B7B77PuTg==", "license": "MIT", "engines": { "node": ">= 12" } }, - "node_modules/ipaddr.js": { - "version": "1.9.1", - "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", - "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==", - "license": "MIT", - "engines": { - "node": ">= 0.10" - } - }, - "node_modules/is-arrayish": { - "version": "0.2.1", - "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz", - "integrity": "sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==", - "license": "MIT" - }, "node_modules/is-docker": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/is-docker/-/is-docker-3.0.0.tgz", @@ -3770,12 +3210,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/is-promise": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/is-promise/-/is-promise-4.0.0.tgz", - "integrity": "sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ==", - "license": "MIT" - }, "node_modules/is-regexp": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/is-regexp/-/is-regexp-3.1.0.tgz", @@ -3860,28 +3294,6 @@ "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", "license": "MIT" }, - "node_modules/js-yaml": { - "version": "4.3.2", - "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.3.2.tgz", - "integrity": "sha512-SFNOvSJ+Dgf/9An904Yx+CgSlIPCkIpao4qo51lpee25TIRejdH3rhR4EZMGoNx3/TP3O+wzWuiTFl4sqbltzA==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/puzrin" - }, - { - "type": "github", - "url": "https://github.com/sponsors/nodeca" - } - ], - "license": "MIT", - "dependencies": { - "argparse": "^2.0.1" - }, - "bin": { - "js-yaml": "bin/js-yaml.js" - } - }, "node_modules/jsesc": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz", @@ -3894,24 +3306,12 @@ "node": ">=6" } }, - "node_modules/json-parse-even-better-errors": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz", - "integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==", - "license": "MIT" - }, "node_modules/json-schema-traverse": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", "license": "MIT" }, - "node_modules/json-schema-typed": { - "version": "8.0.2", - "resolved": "https://registry.npmjs.org/json-schema-typed/-/json-schema-typed-8.0.2.tgz", - "integrity": "sha512-fQhoXdcvc3V28x7C7BMs4P5+kNlgUURe2jmUT1T//oBRMDrqy1QPelJimwZGo7Hg9VPV3EQV5Bnq4hbFy2vetA==", - "license": "BSD-2-Clause" - }, "node_modules/json5": { "version": "2.2.3", "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", @@ -4203,12 +3603,6 @@ "url": "https://opencollective.com/parcel" } }, - "node_modules/lines-and-columns": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz", - "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==", - "license": "MIT" - }, "node_modules/locate-path": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-3.0.0.tgz", @@ -4277,40 +3671,6 @@ "@jridgewell/sourcemap-codec": "^1.5.5" } }, - "node_modules/math-intrinsics": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", - "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", - "license": "MIT", - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/media-typer": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-1.1.1.tgz", - "integrity": "sha512-yz3xRaG20c6/BOzvYoDaGtPmGscs7YivItZEEqe6GbwNfHuxu9YNmvnEkMzKldAGY4/80pRcQRZSEnhquk9XuQ==", - "license": "MIT", - "engines": { - "node": ">= 0.8" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/express" - } - }, - "node_modules/merge-descriptors": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-2.0.0.tgz", - "integrity": "sha512-Snk314V5ayFLhp3fkUREub6WtjBfPdCPY1Ln8/8munuLuiYhsABgBVWsozAG+MWMbVEvcdcpbi9R7ww22l9Q3g==", - "license": "MIT", - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/merge-stream": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz", @@ -4351,31 +3711,6 @@ "url": "https://github.com/sponsors/jonschlinkert" } }, - "node_modules/mime-db": { - "version": "1.54.0", - "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.54.0.tgz", - "integrity": "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==", - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/mime-types": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-3.0.2.tgz", - "integrity": "sha512-Lbgzdk0h4juoQ9fCKXW4by0UJqj+nOOrI9MJ1sSj4nI8aI2eo1qmvQEie4VD1glsS250n15LsWsYtCugiStS5A==", - "license": "MIT", - "dependencies": { - "mime-db": "^1.54.0" - }, - "engines": { - "node": ">=18" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/express" - } - }, "node_modules/mimic-fn": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-3.1.0.tgz", @@ -4459,42 +3794,13 @@ "node": "^20.0.0 || >=22.0.0" } }, - "node_modules/negotiator": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-1.1.0.tgz", - "integrity": "sha512-NMPBRMJgiQHjbd8phG3Vebdx4kZ1H121rbl5IkMqeOsahptB9BKo/d7oJ3zTXqTgagn2bWlNSXkh0QUGM31RYg==", - "license": "MIT", - "dependencies": { - "content-type": "^2.1.0" - }, - "engines": { - "node": ">=18" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/express" - } - }, - "node_modules/negotiator/node_modules/content-type": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/content-type/-/content-type-2.1.0.tgz", - "integrity": "sha512-mj7UPXE0jaqaOsukNZRUEfEi2AcL7C/vwmwcHV0O97eO1E1pxBZuyjlZrx5seTaNBg1U6+o35wpa35Qfcc+7ag==", - "license": "MIT", - "engines": { - "node": ">=18" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/express" - } - }, "node_modules/next": { - "version": "16.3.4", - "resolved": "https://registry.npmjs.org/next/-/next-16.3.4.tgz", - "integrity": "sha512-/Ztf6CeRH+ejEXUrYtqI4gkS66eFIHuSwqi60RgcpWKodxFZx2/dqVCMKBwILfAHXQ+F1b1vAudgj3mnxqtoIA==", + "version": "16.4.5", + "resolved": "https://registry.npmjs.org/next/-/next-16.4.5.tgz", + "integrity": "sha512-rmdfsWosjTUO+6R0S3KokoIk6Kj0sWDRTe86tZHdqaWH/oXq/udX3/4xb+YQi7aWbyEFoglDU54kaifLCdXbBw==", "license": "MIT", "dependencies": { - "@next/env": "16.3.4", + "@next/env": "16.4.5", "@swc/helpers": "0.5.23", "baseline-browser-mapping": "^2.9.19", "caniuse-lite": "^1.0.30001579", @@ -4508,15 +3814,15 @@ "node": ">=20.9.0" }, "optionalDependencies": { - "@next/swc-darwin-arm64": "16.3.4", - "@next/swc-darwin-x64": "16.3.4", - "@next/swc-linux-arm64-gnu": "16.3.4", - "@next/swc-linux-arm64-musl": "16.3.4", - "@next/swc-linux-x64-gnu": "16.3.4", - "@next/swc-linux-x64-musl": "16.3.4", - "@next/swc-win32-arm64-msvc": "16.3.4", - "@next/swc-win32-x64-msvc": "16.3.4", - "sharp": "^0.35.4" + "@next/swc-darwin-arm64": "16.4.5", + "@next/swc-darwin-x64": "16.4.5", + "@next/swc-linux-arm64-gnu": "16.4.5", + "@next/swc-linux-arm64-musl": "16.4.5", + "@next/swc-linux-x64-gnu": "16.4.5", + "@next/swc-linux-x64-musl": "16.4.5", + "@next/swc-win32-arm64-msvc": "16.4.5", + "@next/swc-win32-x64-msvc": "16.4.5", + "sharp": "^0.35.5" }, "peerDependencies": { "@opentelemetry/api": "^1.1.0", @@ -4580,9 +3886,9 @@ } }, "node_modules/node-releases": { - "version": "2.0.54", - "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.54.tgz", - "integrity": "sha512-YHs7BmmcsdAI5Ozuf8JZo6PT0mv2GIWC9vMfvUC3dp65M8hn7Ux8CPL+2oBI7juNuj9d0ndhTcznq2ODBps9cQ==", + "version": "2.0.63", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.63.tgz", + "integrity": "sha512-6li2QanLsAXpIH2R6j77MzRyvOyrvdTqlKtO4CgBdHGNvN99ZZfyxszHc5o5+IZBWD5u6Hzwg83JoGXmqZNG9Q==", "license": "MIT", "engines": { "node": ">=18" @@ -4616,27 +3922,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/object-assign": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", - "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/object-inspect": { - "version": "1.13.4", - "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz", - "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==", - "license": "MIT", - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, "node_modules/object-treeify": { "version": "1.1.33", "resolved": "https://registry.npmjs.org/object-treeify/-/object-treeify-1.1.33.tgz", @@ -4646,27 +3931,6 @@ "node": ">= 10" } }, - "node_modules/on-finished": { - "version": "2.4.1", - "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", - "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==", - "license": "MIT", - "dependencies": { - "ee-first": "1.1.1" - }, - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/once": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", - "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", - "license": "ISC", - "dependencies": { - "wrappy": "1" - } - }, "node_modules/onetime": { "version": "5.1.2", "resolved": "https://registry.npmjs.org/onetime/-/onetime-5.1.2.tgz", @@ -4692,16 +3956,16 @@ } }, "node_modules/open": { - "version": "11.0.1", - "resolved": "https://registry.npmjs.org/open/-/open-11.0.1.tgz", - "integrity": "sha512-NzwMUB6C1D0+Kd+9iMS/H4k+Ck3cTX6Ckyfr/gAGlmvSE1LUQZnEZvWBi4PYmMwH/S5SMeTXnE+9uAz8uF+pWw==", + "version": "11.0.4", + "resolved": "https://registry.npmjs.org/open/-/open-11.0.4.tgz", + "integrity": "sha512-++Zlftm0kVLPmzC06t6epuWmcRMDbI4z5P3NNX979WA/k23+NtSOynEGzsVfZwguKw2mi5umVgnBlJQMwRz4Pg==", "license": "MIT", "dependencies": { - "default-browser": "^5.4.0", + "default-browser": "^5.5.1", "define-lazy-prop": "^3.0.0", "is-in-ssh": "^1.0.0", "is-inside-container": "^1.0.0", - "powershell-utils": "^0.2.0", + "powershell-utils": "^0.2.1", "wsl-utils": "^1.0.0" }, "engines": { @@ -4797,36 +4061,6 @@ "node": ">=6" } }, - "node_modules/parent-module": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", - "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", - "license": "MIT", - "dependencies": { - "callsites": "^3.0.0" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/parse-json": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-5.2.0.tgz", - "integrity": "sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==", - "license": "MIT", - "dependencies": { - "@babel/code-frame": "^7.0.0", - "error-ex": "^1.3.1", - "json-parse-even-better-errors": "^2.3.0", - "lines-and-columns": "^1.1.6" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/parse-ms": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/parse-ms/-/parse-ms-4.0.0.tgz", @@ -4839,15 +4073,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/parseurl": { - "version": "1.3.3", - "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", - "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==", - "license": "MIT", - "engines": { - "node": ">= 0.8" - } - }, "node_modules/path-browserify": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/path-browserify/-/path-browserify-1.0.1.tgz", @@ -4872,16 +4097,6 @@ "node": ">=8" } }, - "node_modules/path-to-regexp": { - "version": "8.4.2", - "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-8.4.2.tgz", - "integrity": "sha512-qRcuIdP69NPm4qbACK+aDogI5CBDMi1jKe0ry5rSQJz8JVLsC7jV8XpiJjGRLLol3N+R5ihGYcrPLTno6pAdBA==", - "license": "MIT", - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/express" - } - }, "node_modules/pg": { "version": "8.23.0", "resolved": "https://registry.npmjs.org/pg/-/pg-8.23.0.tgz", @@ -4988,15 +4203,6 @@ "url": "https://github.com/sponsors/jonschlinkert" } }, - "node_modules/pkce-challenge": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/pkce-challenge/-/pkce-challenge-5.0.1.tgz", - "integrity": "sha512-wQ0b/W4Fr01qtpHlqSqspcj3EhBvimsdh0KlHhH8HRZnMsEa0ea2fTULOXOS9ccQr3om+GcGRk4e+isrZWV8qQ==", - "license": "MIT", - "engines": { - "node": ">=16.20.0" - } - }, "node_modules/pkg-up": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/pkg-up/-/pkg-up-3.1.0.tgz", @@ -5038,9 +4244,9 @@ } }, "node_modules/postcss-selector-parser": { - "version": "7.1.5", - "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-7.1.5.tgz", - "integrity": "sha512-KvvtD7SrlBP7dlgkBghEE3r84CABm5SmV2aNcG4oCA+qDnJ/tvKonFVvwWAyyWUEwxuNawdfEAZKP9zM3oZ2Uw==", + "version": "7.1.7", + "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-7.1.7.tgz", + "integrity": "sha512-wWBro/7hJ40ye6TPiXkUQoXKCw23xSsjriUvqd7Uls9Sso3oLiON1flc18cCdnjgCRXw4fBVqUJJeOdDTh3P3g==", "license": "MIT", "dependencies": { "cssesc": "^3.0.0", @@ -5090,9 +4296,9 @@ } }, "node_modules/powershell-utils": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/powershell-utils/-/powershell-utils-0.2.0.tgz", - "integrity": "sha512-ZlsFlG7MtSFCoc5xreOvBAozCJ6Pf06opgJjh9ONEv418xpZSAzNjstD36C6+JwOnfSqOW/9uDkqKjezTdxZhw==", + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/powershell-utils/-/powershell-utils-0.2.1.tgz", + "integrity": "sha512-C+y9x90UElAddDZmV4qOx9W53B61PO7cIqWz2dQsWlwswuq4mr8NEwytdGKboYbQlGZ3awrkTeNvcZiZNHnQ8A==", "license": "MIT", "engines": { "node": ">=20" @@ -5138,35 +4344,6 @@ "node": ">=6" } }, - "node_modules/proxy-addr": { - "version": "2.0.7", - "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", - "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==", - "license": "MIT", - "dependencies": { - "forwarded": "0.2.0", - "ipaddr.js": "1.9.1" - }, - "engines": { - "node": ">= 0.10" - } - }, - "node_modules/qs": { - "version": "6.15.3", - "resolved": "https://registry.npmjs.org/qs/-/qs-6.15.3.tgz", - "integrity": "sha512-O9gl3zCl5h5blw1KGUzQKhA5oUXSl8rwUIM5o0S3nCXMliSvy5Dzx7/DJcI+SwgICv+IneSZwhBh1oSyEHA71A==", - "license": "BSD-3-Clause", - "dependencies": { - "es-define-property": "^1.0.1", - "side-channel": "^1.1.1" - }, - "engines": { - "node": ">=0.6" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, "node_modules/queue-microtask": { "version": "1.2.3", "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", @@ -5187,34 +4364,6 @@ ], "license": "MIT" }, - "node_modules/range-parser": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.3.0.tgz", - "integrity": "sha512-hek2mFQpPuI4E1BBKrSto+BU3e3x4xuarsbiwr3+lf7p44juvFMV0XFWQAP3xUyqXA4RrXLIoaSUGbSt056ZMw==", - "license": "MIT", - "engines": { - "node": ">= 0.6" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/express" - } - }, - "node_modules/raw-body": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-3.0.2.tgz", - "integrity": "sha512-K5zQjDllxWkf7Z5xJdV0/B0WTNqx6vxG70zJE4N0kBs4LovmEYWJzQGxC9bS9RAKu3bgM40lrd5zoLJ12MQ5BA==", - "license": "MIT", - "dependencies": { - "bytes": "~3.1.2", - "http-errors": "~2.0.1", - "iconv-lite": "~0.7.0", - "unpipe": "~1.0.0" - }, - "engines": { - "node": ">= 0.10" - } - }, "node_modules/react": { "version": "19.2.8", "resolved": "https://registry.npmjs.org/react/-/react-19.2.8.tgz", @@ -5265,15 +4414,6 @@ "integrity": "sha512-XGoLeRAVzUTcJ1qkxPQhDJyIZ5d6zzZD9nT7AEZOaaU9UbWclhycElmhO+VD5bFeLuzhPBaOV2oXC8uG35ZSpg==", "license": "MIT" }, - "node_modules/resolve-from": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", - "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", - "license": "MIT", - "engines": { - "node": ">=4" - } - }, "node_modules/restore-cursor": { "version": "5.1.0", "resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-5.1.0.tgz", @@ -5321,22 +4461,6 @@ "integrity": "sha512-3SOzvaAg8rkHrXtRjpCvCvbyO5to9oOO27Z/XqHEYXfMRVSw/qMIVdmaOk9W2lcRLtR6dlqTjo9hDeJk70QBYQ==", "license": "MIT" }, - "node_modules/router": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/router/-/router-2.2.0.tgz", - "integrity": "sha512-nLTrUKm2UyiL7rlhapu/Zl45FwNgkZGaCpZbIHajDYgwlJCOzLSk+cIPAnsEqV955GjILJnKbdQC1nVPz+gAYQ==", - "license": "MIT", - "dependencies": { - "debug": "^4.4.0", - "depd": "^2.0.0", - "is-promise": "^4.0.0", - "parseurl": "^1.3.3", - "path-to-regexp": "^8.0.0" - }, - "engines": { - "node": ">= 18" - } - }, "node_modules/run-applescript": { "version": "7.1.0", "resolved": "https://registry.npmjs.org/run-applescript/-/run-applescript-7.1.0.tgz", @@ -5372,12 +4496,6 @@ "queue-microtask": "^1.2.2" } }, - "node_modules/safer-buffer": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", - "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", - "license": "MIT" - }, "node_modules/scheduler": { "version": "0.27.0", "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.27.0.tgz", @@ -5394,67 +4512,16 @@ "node": ">=10" } }, - "node_modules/send": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/send/-/send-1.2.1.tgz", - "integrity": "sha512-1gnZf7DFcoIcajTjTwjwuDjzuz4PPcY2StKPlsGAQ1+YH20IRVrBaXSWmdjowTJ6u8Rc01PoYOGHXfP1mYcZNQ==", - "license": "MIT", - "dependencies": { - "debug": "^4.4.3", - "encodeurl": "^2.0.0", - "escape-html": "^1.0.3", - "etag": "^1.8.1", - "fresh": "^2.0.0", - "http-errors": "^2.0.1", - "mime-types": "^3.0.2", - "ms": "^2.1.3", - "on-finished": "^2.4.1", - "range-parser": "^1.2.1", - "statuses": "^2.0.2" - }, - "engines": { - "node": ">= 18" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/express" - } - }, - "node_modules/serve-static": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-2.2.1.tgz", - "integrity": "sha512-xRXBn0pPqQTVQiC8wyQrKs2MOlX24zQ0POGaj0kultvoOCstBQM5yvOhAVSUwOMjQtTvsPWoNCHfPGwaaQJhTw==", - "license": "MIT", - "dependencies": { - "encodeurl": "^2.0.0", - "escape-html": "^1.0.3", - "parseurl": "^1.3.3", - "send": "^1.2.0" - }, - "engines": { - "node": ">= 18" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/express" - } - }, "node_modules/set-cookie-parser": { "version": "3.1.2", "resolved": "https://registry.npmjs.org/set-cookie-parser/-/set-cookie-parser-3.1.2.tgz", "integrity": "sha512-5/r/lTwbJ3zQ+qwdUFZYeRNqda7P5HD8zQKqlSjdGt1/S0cjLAphHusj4Y58ahDtWn/g32xrIS58/ikOvwl0Lw==", "license": "MIT" }, - "node_modules/setprototypeof": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", - "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==", - "license": "ISC" - }, "node_modules/shadcn": { - "version": "4.19.0", - "resolved": "https://registry.npmjs.org/shadcn/-/shadcn-4.19.0.tgz", - "integrity": "sha512-EQF6R+CUXTsEP2BpyhxrUEAFesrtFD1POvVOf5jM+wkgtA4kG1EW1+1Wlmi9LqiprSL681JJXBcS0u8WkVVVyQ==", + "version": "4.23.0", + "resolved": "https://registry.npmjs.org/shadcn/-/shadcn-4.23.0.tgz", + "integrity": "sha512-lKgq7QPFgkV/6EOrMCwvEn7ZAw/YnM/grhxzJmK4cZdw3E4F3Ut2EmWvrn2E+vzJp1y/76mxU185OSkXo9Ff+Q==", "license": "MIT", "dependencies": { "@babel/core": "^7.28.0", @@ -5462,11 +4529,12 @@ "@babel/plugin-transform-typescript": "^7.28.0", "@babel/preset-typescript": "^7.27.1", "@dotenvx/dotenvx": "^1.48.4", - "@modelcontextprotocol/sdk": "^1.26.0", + "@modelcontextprotocol/server": "^2.3.1", + "@shadcn/registry": "0.2.1", "@types/validate-npm-package-name": "^4.0.2", - "browserslist": "^4.26.2", + "browserslist": "^4.29.3", + "cn": "^0.2.4", "commander": "^14.0.0", - "cosmiconfig": "^9.0.0", "dedent": "^1.6.0", "deepmerge": "^4.3.1", "diff": "^8.0.2", @@ -5475,21 +4543,19 @@ "fs-extra": "^11.3.1", "fuzzysort": "^3.1.0", "kleur": "^4.1.5", - "open": "^11.0.0", + "open": "^11.0.4", "ora": "^8.2.0", - "postcss": "^8.5.6", - "postcss-selector-parser": "^7.1.0", + "postcss": "^8.5.23", + "postcss-selector-parser": "^7.1.6", "prompts": "^2.4.2", "recast": "^0.23.11", "socks": "^2.8.8", "stringify-object": "^5.0.0", - "tailwind-merge": "^3.0.1", "ts-morph": "^26.0.0", "tsconfig-paths": "^4.2.0", - "undici": "^7.27.2", + "undici": "^7.30.1", "validate-npm-package-name": "^7.0.1", - "zod": "^3.24.1", - "zod-to-json-schema": "^3.24.6" + "zod": "^4.2.0" }, "bin": { "shadcn": "dist/index.js" @@ -5498,19 +4564,10 @@ "node": ">=20.18.1" } }, - "node_modules/shadcn/node_modules/zod": { - "version": "3.25.76", - "resolved": "https://registry.npmjs.org/zod/-/zod-3.25.76.tgz", - "integrity": "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==", - "license": "MIT", - "funding": { - "url": "https://github.com/sponsors/colinhacks" - } - }, "node_modules/sharp": { - "version": "0.35.4", - "resolved": "https://registry.npmjs.org/sharp/-/sharp-0.35.4.tgz", - "integrity": "sha512-n++8XWcj+jCOr2IOl7h8LbKnGBDY4aPbmprMONBNFdn0ImXqpGVv5zliDs0V9HbmbCQLpbuo2ej9rAoOQTvMDA==", + "version": "0.35.5", + "resolved": "https://registry.npmjs.org/sharp/-/sharp-0.35.5.tgz", + "integrity": "sha512-Ywn4OnzGukp7CDMrp08RQ50YKmuwG47brZgIVPTvBaaAfQlRlygrRqSrxdCiL9M+LlzLBiJ68IR1QqvzHyjC7g==", "license": "Apache-2.0", "optional": true, "dependencies": { @@ -5525,31 +4582,31 @@ "url": "https://opencollective.com/libvips" }, "optionalDependencies": { - "@img/sharp-darwin-arm64": "0.35.4", - "@img/sharp-darwin-x64": "0.35.4", - "@img/sharp-freebsd-wasm32": "0.35.4", - "@img/sharp-libvips-darwin-arm64": "1.3.3", - "@img/sharp-libvips-darwin-x64": "1.3.3", - "@img/sharp-libvips-linux-arm": "1.3.3", - "@img/sharp-libvips-linux-arm64": "1.3.3", - "@img/sharp-libvips-linux-ppc64": "1.3.3", - "@img/sharp-libvips-linux-riscv64": "1.3.3", - "@img/sharp-libvips-linux-s390x": "1.3.3", - "@img/sharp-libvips-linux-x64": "1.3.3", - "@img/sharp-libvips-linuxmusl-arm64": "1.3.3", - "@img/sharp-libvips-linuxmusl-x64": "1.3.3", - "@img/sharp-linux-arm": "0.35.4", - "@img/sharp-linux-arm64": "0.35.4", - "@img/sharp-linux-ppc64": "0.35.4", - "@img/sharp-linux-riscv64": "0.35.4", - "@img/sharp-linux-s390x": "0.35.4", - "@img/sharp-linux-x64": "0.35.4", - "@img/sharp-linuxmusl-arm64": "0.35.4", - "@img/sharp-linuxmusl-x64": "0.35.4", - "@img/sharp-webcontainers-wasm32": "0.35.4", - "@img/sharp-win32-arm64": "0.35.4", - "@img/sharp-win32-ia32": "0.35.4", - "@img/sharp-win32-x64": "0.35.4" + "@img/sharp-darwin-arm64": "0.35.5", + "@img/sharp-darwin-x64": "0.35.5", + "@img/sharp-freebsd-wasm32": "0.35.5", + "@img/sharp-libvips-darwin-arm64": "1.3.4", + "@img/sharp-libvips-darwin-x64": "1.3.4", + "@img/sharp-libvips-linux-arm": "1.3.4", + "@img/sharp-libvips-linux-arm64": "1.3.4", + "@img/sharp-libvips-linux-ppc64": "1.3.4", + "@img/sharp-libvips-linux-riscv64": "1.3.4", + "@img/sharp-libvips-linux-s390x": "1.3.4", + "@img/sharp-libvips-linux-x64": "1.3.4", + "@img/sharp-libvips-linuxmusl-arm64": "1.3.4", + "@img/sharp-libvips-linuxmusl-x64": "1.3.4", + "@img/sharp-linux-arm": "0.35.5", + "@img/sharp-linux-arm64": "0.35.5", + "@img/sharp-linux-ppc64": "0.35.5", + "@img/sharp-linux-riscv64": "0.35.5", + "@img/sharp-linux-s390x": "0.35.5", + "@img/sharp-linux-x64": "0.35.5", + "@img/sharp-linuxmusl-arm64": "0.35.5", + "@img/sharp-linuxmusl-x64": "0.35.5", + "@img/sharp-webcontainers-wasm32": "0.35.5", + "@img/sharp-win32-arm64": "0.35.5", + "@img/sharp-win32-ia32": "0.35.5", + "@img/sharp-win32-x64": "0.35.5" }, "peerDependenciesMeta": { "@types/node": { @@ -5578,78 +4635,6 @@ "node": ">=8" } }, - "node_modules/side-channel": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.1.tgz", - "integrity": "sha512-6x6dK6zJdpTzF4sQeNYxwtvBzf6Eg4GtlesS94HOvTudUeyK2WXAaIfmDgsyslYrRBeFIlsi54AYsFGUuhmvrQ==", - "license": "MIT", - "dependencies": { - "es-errors": "^1.3.0", - "object-inspect": "^1.13.4", - "side-channel-list": "^1.0.1", - "side-channel-map": "^1.0.1", - "side-channel-weakmap": "^1.0.2" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/side-channel-list": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.1.tgz", - "integrity": "sha512-mjn/0bi/oUURjc5Xl7IaWi/OJJJumuoJFQJfDDyO46+hBWsfaVM65TBHq2eoZBhzl9EchxOijpkbRC8SVBQU0w==", - "license": "MIT", - "dependencies": { - "es-errors": "^1.3.0", - "object-inspect": "^1.13.4" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/side-channel-map": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz", - "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==", - "license": "MIT", - "dependencies": { - "call-bound": "^1.0.2", - "es-errors": "^1.3.0", - "get-intrinsic": "^1.2.5", - "object-inspect": "^1.13.3" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/side-channel-weakmap": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz", - "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==", - "license": "MIT", - "dependencies": { - "call-bound": "^1.0.2", - "es-errors": "^1.3.0", - "get-intrinsic": "^1.2.5", - "object-inspect": "^1.13.3", - "side-channel-map": "^1.0.1" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, "node_modules/signal-exit": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", @@ -5702,9 +4687,10 @@ } }, "node_modules/source-map-js": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", - "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.2.tgz", + "integrity": "sha512-KGj/8Y43x35aZVDtt+J4mK1hoLGHULMYfSkODJNQjNDC3oW1PqPoxMwo0pLUsWM/UEGzON/NxeHywEfNXNP3Vw==", + "license": "BSD-3-Clause", "engines": { "node": ">=0.10.0" } @@ -5718,15 +4704,6 @@ "node": ">= 10.x" } }, - "node_modules/statuses": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.2.tgz", - "integrity": "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==", - "license": "MIT", - "engines": { - "node": ">= 0.8" - } - }, "node_modules/stdin-discarder": { "version": "0.2.2", "resolved": "https://registry.npmjs.org/stdin-discarder/-/stdin-discarder-0.2.2.tgz", @@ -5868,9 +4845,9 @@ } }, "node_modules/systeminformation": { - "version": "5.33.5", - "resolved": "https://registry.npmjs.org/systeminformation/-/systeminformation-5.33.5.tgz", - "integrity": "sha512-0v8l1CwFOAjfkv6ynpMrv3YGjH0M7PWCpZwusr8J1TEoQFPK7WXO6gbeAiandaWoh7vbMdnFtDqVotJVnLJtIg==", + "version": "5.33.19", + "resolved": "https://registry.npmjs.org/systeminformation/-/systeminformation-5.33.19.tgz", + "integrity": "sha512-Zm7wGeCG7PqfvzegMJIzrxICopWqYCeLATVmvxOOci1isLcNVKgY4Fr0hhYQzvc7cgtK3377jQVChTFAnznOiA==", "license": "MIT", "os": [ "darwin", @@ -5940,15 +4917,6 @@ "node": ">=8.0" } }, - "node_modules/toidentifier": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", - "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==", - "license": "MIT", - "engines": { - "node": ">=0.6" - } - }, "node_modules/ts-morph": { "version": "26.0.0", "resolved": "https://registry.npmjs.org/ts-morph/-/ts-morph-26.0.0.tgz", @@ -5987,42 +4955,23 @@ "url": "https://github.com/sponsors/Wombosvideo" } }, - "node_modules/type-is": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/type-is/-/type-is-2.1.0.tgz", - "integrity": "sha512-faYHw0anBbc/kWF3zFTEnxSFOAGUX9GFbOBthvDdLsIlEoWOFOtS0zgCiQYwIskL9iGXZL3kAXD8OoZ4GmMATA==", - "license": "MIT", - "dependencies": { - "content-type": "^2.0.0", - "media-typer": "^1.1.0", - "mime-types": "^3.0.0" - }, - "engines": { - "node": ">= 18" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/express" - } - }, - "node_modules/type-is/node_modules/content-type": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/content-type/-/content-type-2.1.0.tgz", - "integrity": "sha512-mj7UPXE0jaqaOsukNZRUEfEi2AcL7C/vwmwcHV0O97eO1E1pxBZuyjlZrx5seTaNBg1U6+o35wpa35Qfcc+7ag==", - "license": "MIT", + "node_modules/type-fest": { + "version": "4.41.0", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-4.41.0.tgz", + "integrity": "sha512-TeTSQ6H5YHvpqVwBRcnLDCBnDOHWYu7IvGbHT6N8AOymcr9PJGjc1GTtiWZTYg0NCgYwvnYWEkVChQAr9bjfwA==", + "license": "(MIT OR CC0-1.0)", "engines": { - "node": ">=18" + "node": ">=16" }, "funding": { - "type": "opencollective", - "url": "https://opencollective.com/express" + "url": "https://github.com/sponsors/sindresorhus" } }, "node_modules/typescript": { "version": "5.9.3", "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", - "devOptional": true, + "dev": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" @@ -6032,9 +4981,9 @@ } }, "node_modules/undici": { - "version": "7.29.0", - "resolved": "https://registry.npmjs.org/undici/-/undici-7.29.0.tgz", - "integrity": "sha512-IDxfleLmmbSskfWSUATiN1nfn2rDuvnMOqb5CWR92iIfojA0Ud+ulOAAEQ57LPr9rWmsreUyf5lwyao+7GNNVw==", + "version": "7.30.2", + "resolved": "https://registry.npmjs.org/undici/-/undici-7.30.2.tgz", + "integrity": "sha512-mLyPQnHwUxaM/rideoK/+KL8DaAJLRl4aBCfmX444aCSLnIaomqPLtNRe2jdCYcsk5wZzXrDIQC8ZeYNRAYyIA==", "license": "MIT", "engines": { "node": ">=20.18.1" @@ -6067,19 +5016,10 @@ "node": ">= 10.0.0" } }, - "node_modules/unpipe": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", - "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==", - "license": "MIT", - "engines": { - "node": ">= 0.8" - } - }, "node_modules/update-browserslist-db": { - "version": "1.3.2", - "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.3.2.tgz", - "integrity": "sha512-UQ+MSxlhRm1bzjhU+DcuXfjFO1FzNtqhK5+9Yvlp90ItDLk5vT932A0rFu619nf7RVS+Y/VeaUW1jaRDqZ8VJw==", + "version": "1.3.4", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.3.4.tgz", + "integrity": "sha512-t8wonIOdQyOjtPd2VktlBu6QjTVTks6v8bSi7kd0r2Vll8TK1JYDZtRRdbmCHOAMphkZgrzK+L+vXK4pyZ8t4A==", "funding": [ { "type": "opencollective", @@ -6130,15 +5070,6 @@ "node": "^20.17.0 || >=22.9.0" } }, - "node_modules/vary": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", - "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==", - "license": "MIT", - "engines": { - "node": ">= 0.8" - } - }, "node_modules/which": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/which/-/which-4.0.0.tgz", @@ -6154,20 +5085,14 @@ "node": "^16.13.0 || >=18.0.0" } }, - "node_modules/wrappy": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", - "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", - "license": "ISC" - }, "node_modules/wsl-utils": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/wsl-utils/-/wsl-utils-1.0.0.tgz", - "integrity": "sha512-Hl0ZOAs672vg+06kfujwRhoS6/jehvULrlFkuF2dRu6pHgA8U06h3xqNIqNNU1LTXPcedxByAR4GS6pwQK0mgA==", + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/wsl-utils/-/wsl-utils-1.0.1.tgz", + "integrity": "sha512-c16PKLIRxBYpR3jSWsdS7SFp0COm5SOQdiFjY818dYFHJbMGuLBszpF07LBWmOS1uzrHa9VGdynsEgoeKy0oOQ==", "license": "MIT", "dependencies": { "is-wsl": "^3.1.0", - "powershell-utils": "^0.1.0" + "powershell-utils": "^0.2.0" }, "engines": { "node": ">=20" @@ -6176,18 +5101,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/wsl-utils/node_modules/powershell-utils": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/powershell-utils/-/powershell-utils-0.1.0.tgz", - "integrity": "sha512-dM0jVuXJPsDN6DvRpea484tCUaMiXWjuCn++HGTqUWzGDjv5tZkEZldAJ/UMlqRYGFrD/etByo4/xOuC/snX2A==", - "license": "MIT", - "engines": { - "node": ">=20" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/xtend": { "version": "4.0.2", "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz", @@ -6231,22 +5144,13 @@ } }, "node_modules/zod": { - "version": "4.4.3", - "resolved": "https://registry.npmjs.org/zod/-/zod-4.4.3.tgz", - "integrity": "sha512-ytENFjIJFl2UwYglde2jchW2Hwm4GJFLDiSXWdTrJQBIN9Fcyp7n4DhxJEiWNAJMV1/BqWfW/kkg71UDcHJyTQ==", + "version": "4.6.7", + "resolved": "https://registry.npmjs.org/zod/-/zod-4.6.7.tgz", + "integrity": "sha512-g8W2D2kU6a8untc053fX1uVBKzM7yqAxW/3zwk83+6RyF2+SKOuV+c0wBDhUwzV1PaGapmZH+rmcUzPf+3CGwg==", "license": "MIT", "funding": { "url": "https://github.com/sponsors/colinhacks" } - }, - "node_modules/zod-to-json-schema": { - "version": "3.25.2", - "resolved": "https://registry.npmjs.org/zod-to-json-schema/-/zod-to-json-schema-3.25.2.tgz", - "integrity": "sha512-O/PgfnpT1xKSDeQYSCfRI5Gy3hPf91mKVDuYLUHZJMiDFptvP41MSnWofm8dnCm0256ZNfZIM7DSzuSMAFnjHA==", - "license": "ISC", - "peerDependencies": { - "zod": "^3.25.28 || ^4" - } } } } diff --git a/apps/dashboard/package.json b/apps/dashboard/package.json index 6a67b08..c9d13a1 100644 --- a/apps/dashboard/package.json +++ b/apps/dashboard/package.json @@ -17,7 +17,7 @@ "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", "lucide-react": "^1.34.0", - "next": "^16.3.4", + "next": "^16.4.5", "next-themes": "^0.4.6", "pg": "8.23.0", "pg-connection-string": "2.14.0", diff --git a/apps/docs/next/admin/managed-packages.mdx b/apps/docs/next/admin/managed-packages.mdx index b520dfd..d33885b 100644 --- a/apps/docs/next/admin/managed-packages.mdx +++ b/apps/docs/next/admin/managed-packages.mdx @@ -163,7 +163,7 @@ A deployment operator must first configure a GitHub or Bitbucket connection and | OpenCode plugin module | `toolkit/plugins/index.js` | | Helper | `toolkit/bin/review` | - A skill path can point directly to a skill folder containing `SKILL.md`, regardless of where it lives in the repository. A folder containing several skill folders remains supported for compatibility. Codex and Claude subagents and hooks still require a native plugin mapping; OpenCode hooks are plugin modules. For a helper, also enter its command name and platform key. + A skill path can point directly to a skill folder containing `SKILL.md`, regardless of where it lives in the repository. A folder containing several skill folders remains supported for compatibility. Codex and Claude subagents and hooks still require a native plugin mapping; OpenCode hooks are plugin modules. OpenCode 2.x only loads modules written for its new plugin API, so map 1.x and 2.x modules with a version variant. For a helper, also enter its command name and platform key. Select **Add to pending changes**, review the inventory on the Extensions page, and then select **Publish extension changes**. Publishing validates the full configuration and creates a new immutable governance revision for clients to reconcile. @@ -330,7 +330,7 @@ Each package declares an immutable `source_ref`, a SHA-256 digest, and adapters | `skills_dir` | Skills loaded for the target harness. | | `agents_dir` | Managed subagent definitions. | | `hooks_file` | Hook configuration supported by that harness. | -| `plugins` | OpenCode plugin modules. | +| `plugins` | OpenCode plugin modules. OpenCode 1.x and 2.x use different plugin APIs; use a `variants` entry with `introduced: 2.0.0` for 2.x modules. | | `helpers` | Platform-specific executables added to the governed launch `PATH`. | See [Governance configuration](/next/reference/governance-config#managed-packages) for the complete YAML schema and adapter example. Maintainers can use [Local development with Docker Compose](/next/development/local-compose) to test package-catalog mounting. diff --git a/apps/docs/next/admin/session-capture.mdx b/apps/docs/next/admin/session-capture.mdx index 9ef749c..88a7fcc 100644 --- a/apps/docs/next/admin/session-capture.mdx +++ b/apps/docs/next/admin/session-capture.mdx @@ -110,7 +110,7 @@ Bundles contain the native data needed by each harness: - Codex: rollout JSONL and native index-derived display metadata. - Claude: primary JSONL, session-owned subagent transcripts, metadata, and externalized tool results. - Kimi: sanitized `state.json`, agent wire histories, plans, and attachments. Credentials, approval grants, processes, cron jobs, queued goals, and logs are excluded. -- OpenCode: its native export-shaped session and messages JSON, restored through `opencode import`. +- OpenCode: its native export-shaped session and messages JSON, restored through `opencode import` (1.x) or `opencode session import` (2.x). ## How titles and previews are generated @@ -174,7 +174,7 @@ and governance eligibility checks for the destination machine. | Codex | Portable capture begins with the profile whose native lifecycle supports `SessionEnd`. The bundle requires exactly one rollout under `.codex/sessions`; earlier captures stay downloadable but non-resumable. Display metadata is matched by native session ID before the rollout fallback is scanned. Restore places the rollout at its native path and invokes the explicit `codex resume ` path; it does not overwrite Codex database state. | | Claude | Only profiles with a uniform portable capture contract are resumable. Blue captures the primary project JSONL and session-owned companion subtree, accepts the supported native index container shapes, and merges a new entry without replacing unrelated sessions. Older pre-hook and hooks-only profiles remain non-resumable. | | Kimi | The adapter accepts the hook path or searches only the documented managed/native session roots for the exact session ID. It captures all agent histories, plans, and attachments, sanitizes `state.json`, and recognizes existing index IDs written as `session_id`, `sessionId`, or `id`. | -| OpenCode | Blue writes the native export shape `{info, messages}` instead of parsing OpenCode's private database. Restore delegates format interpretation to `opencode import`, reads the imported ID reported by the CLI, and resumes that ID. | +| OpenCode | Blue writes the native export shape `{info, messages}` instead of parsing OpenCode's private database; on 2.x that is the output of `opencode session export`, refreshed after each turn and again after OpenCode exits. Restore delegates format interpretation to `opencode import` (1.x) or `opencode session import` (2.x), reads the imported ID reported by the CLI, and resumes that ID. OpenCode 2.x cannot import 1.x exports, so a session resumes only on the release line that recorded it; Blue refuses the other before running OpenCode. | Compatibility profiles protect known storage layouts; they do not make an diff --git a/apps/docs/next/concepts/harness-version-compatibility.mdx b/apps/docs/next/concepts/harness-version-compatibility.mdx index a790049..f291c9e 100644 --- a/apps/docs/next/concepts/harness-version-compatibility.mdx +++ b/apps/docs/next/concepts/harness-version-compatibility.mdx @@ -73,12 +73,12 @@ unsupported, or if an installer fails. | Installation | Recognition evidence | Repair | | --- | --- | --- | -| npm-compatible global layout (all four harnesses, Unix) | `/bin/` resolves to the expected package under `/lib/node_modules`; package name and `bin` target match the resolved executable, and the package directory equals its canonical path (no linked or redirected package directories) | `npm install -g --prefix @` | +| npm-compatible global layout (all four harnesses, Unix) | `/bin/` resolves to one of the harness's packages under `/lib/node_modules` (OpenCode: `opencode-ai` for 1.x, `@opencode/cli` for 2.x); package name and `bin` target match the resolved executable, and the package directory equals its canonical path (no linked or redirected package directories) | `npm install -g --prefix @`, only when the policy-compatible release is published under the detected package | | Claude native (Unix) | Native binary in `~/.local/share/claude/versions/`, reached through `~/.local/bin/claude`; relative symlink chains are resolved | Detected executable: `install ` | | Homebrew Claude/Codex/OpenCode | Resolved target under the matching `Caskroom` or `Cellar` package, including custom prefixes | Refuse automatic arbitrary-version pinning; manually migrate to npm or use a compatible vendor/Homebrew installation | | Kimi standalone (Unix) | Regular native binary at the documented default `~/.kimi-code/bin/kimi`, without redirected parent directories | Official installer with exact `--version`, `KIMI_INSTALL_DIR`, and `KIMI_NO_MODIFY_PATH=1`; the child PATH excludes the installation's bin so the installer skips legacy migration | | Kimi legacy uv tool | Target belongs to a Python environment with `uv-receipt.toml` identifying `kimi-cli` and `pyvenv.cfg` | Refuse: legacy Python versions are not the current Kimi CLI; manually migrate to npm | -| OpenCode standalone (Unix) | Regular native binary at the default `~/.opencode/bin/opencode`, without redirected parent directories | Detected executable: `upgrade --method curl` | +| OpenCode standalone (Unix) | Regular native binary at the default `~/.opencode/bin/opencode`, without redirected parent directories | Detected executable: `upgrade --method curl`, only within the installed major release line | | Codex standalone/release download, custom Kimi/OpenCode roots, pnpm/bun wrappers, Windows wrappers/native layouts, other unrecognized layouts | No supported installation layout | Refuse automatic replacement; manually install a policy-compatible release or migrate to npm | A manually copied package matching the npm-compatible layout is also eligible @@ -181,6 +181,57 @@ adopt a changed vendor mechanism without altering older intervals. Daemon runs, redirected/non-interactive commands, and `blue apply --yes` never install native CLIs. They fail closed and print installation-aware manual guidance for an operator. +## OpenCode 1.x and 2.x + +OpenCode 2 is a separate release line with its own npm package, and the two +cannot be installed side by side. Blue supports both: + +| Installed | Profile | npm package | +| --- | --- | --- | +| `>=0.0.0, <2.0.0` | `opencode-v0_0_0` | `opencode-ai` | +| `>=2.0.0` | `opencode-v2_0_0` | `@opencode/cli` | + +Blue repairs an installation only inside its own release line. An OpenCode +1.x release above Blue's certified 1.x ceiling is repaired to a certified 1.x +release, never upgraded to 2.x. Blue never switches between `opencode-ai` and +`@opencode/cli`: when the policy admits only the other line, Blue prints the +commands to run instead, for example: + +```text +opencode 1.18.25 does not match the required version range >=2. +Installation: npm global — /usr/local/bin/opencode +The supported release is published under a different package, and Blue does not switch packages automatically. + +To switch manually (POSIX shell or PowerShell): + npm uninstall -g --prefix "/usr/local" opencode-ai + npm install -g --prefix "/usr/local" '@opencode/cli@>=2 >=2.0.0 <2.2.6-0' +Then retry Blue. +``` + +To keep a fleet on 1.x, set `version_requirement: "<2"` for `opencode`; fresh +installs then use `opencode-ai`. Without a requirement, fresh installs use the +newest generation, so an existing open range such as `>=1.18` now also admits +2.x releases. + +Before moving users to 2.x: + +- **Packages.** OpenCode 2 does not load 1.x plugin modules. Give each package + that ships an OpenCode plugin a `variants` entry with `introduced: 2.0.0` that + points at a plugin written for the 2.x plugin API, or bound the 1.x adapter + with `before: 2.0.0`. See [Versioned package layouts](#versioned-package-layouts). +- **Sessions.** A session captured on 1.x resumes only with OpenCode 1.x, and a + 2.x session only with 2.x, because 2.x cannot import 1.x exports. Blue refuses + a cross-release restore before it runs OpenCode and names the required range. +- **Launch.** Blue starts OpenCode 2 with `--standalone`, so each governed + session runs its own private server instead of OpenCode's shared background + service, which would not receive Blue's managed configuration or gateway + credentials. `--server` is rejected by OpenCode in that mode. +- **Provider restriction.** In gateway mode Blue restricts providers with + `provider.use` policies. OpenCode 2 ranks policies from a user's own + configuration files above launch-supplied ones, so, as with every managed + overlay setting, a user who adds their own `provider.use` allow statement can + re-enable another provider. + ## Breaking-version profiles Each harness has an ordered registry of compiled profiles. Every profile has an inclusive `introduced` bound, an optional exclusive breaking-generation `before` bound, and a mandatory exclusive `verified_before` release ceiling. Dispatch may remain open-ended, but certified support never is: a parseable release at or beyond `verified_before` is blocked unless the organization uses the explicit unverified opt-in above. diff --git a/apps/docs/next/concepts/managed-configuration.mdx b/apps/docs/next/concepts/managed-configuration.mdx index 2447fb0..a85fe25 100644 --- a/apps/docs/next/concepts/managed-configuration.mdx +++ b/apps/docs/next/concepts/managed-configuration.mdx @@ -13,7 +13,7 @@ Every supported agent ends up with two configurations: the one you already own, | Codex | `~/.codex/config.toml` | `~/.codex/blue.config.toml` | Blue launches Codex with `--profile blue`. | | Claude Code | `~/.claude/settings.json` and `~/.claude.json` | `runtime/claude/settings.json` and `runtime/claude/mcp.json` | Blue passes `--settings` and `--mcp-config` to the governed process. | | Kimi Code | `~/.kimi-code/config.toml` and `~/.kimi-code/mcp.json` | `runtime/kimi/` | Blue points `KIMI_CODE_HOME` at the generated directory. | -| OpenCode | `~/.config/opencode/opencode.json` and `~/.local/share/opencode/auth.json` | `runtime/opencode/opencode.json` and its sibling package directories | Blue exports `OPENCODE_CONFIG_CONTENT` and `OPENCODE_CONFIG_DIR`. | +| OpenCode | `~/.config/opencode/opencode.json` and `~/.local/share/opencode/auth.json` | `runtime/opencode/opencode.json` and its sibling package directories | Blue exports `OPENCODE_CONFIG_CONTENT`. For OpenCode 1.x it also exports `OPENCODE_CONFIG_DIR`; OpenCode 2.x gets `--standalone` instead, and the generated configuration references the package directories by path. | Paths shown as `runtime/…` are relative to Blue's managed data directory, which is `~/.config/blue` on macOS and Linux and `%LOCALAPPDATA%\Blue\Data` on Windows. See [Windows paths and migration](/next/cli/windows-paths) for the full Windows layout. @@ -27,7 +27,7 @@ For Codex, the governed launch selects the separate `blue` profile, so the organ For Claude Code, the generated settings and MCP files are passed as extra launch arguments, so Claude merges them with `~/.claude/settings.json` and `~/.claude.json` at startup. -For OpenCode, the generated configuration arrives through environment variables and lands in OpenCode's own layered configuration, above your global `~/.config/opencode/opencode.json`. +For OpenCode, the generated configuration arrives through environment variables and lands in OpenCode's own layered configuration, above your global `~/.config/opencode/opencode.json`. OpenCode 2.x treats `OPENCODE_CONFIG_DIR` as a replacement for `~/.config/opencode`, so Blue does not set it there and your global configuration, plugins, skills, and agents keep loading. Blue renders OpenCode 2.x configuration in its native shape (`permissions`, `providers`, `mcp.servers`, and `policies`). Kimi Code works differently. Blue builds an isolated `KIMI_CODE_HOME` and seeds it from your native `~/.kimi-code/` files when it reconciles, rather than layering at launch. An edit to your own Kimi files therefore reaches a governed session only after the next `blue apply`. diff --git a/apps/docs/next/development/harness-adapter-architecture.mdx b/apps/docs/next/development/harness-adapter-architecture.mdx index d8ad80c..8368ef7 100644 --- a/apps/docs/next/development/harness-adapter-architecture.mdx +++ b/apps/docs/next/development/harness-adapter-architecture.mdx @@ -98,6 +98,17 @@ Harnesses with standalone installers may resolve the selector to a concrete published version and update that installation in place; they must not install a second binary that remains shadowed on `PATH`. +The install plan, and therefore the npm package, belongs to each generation's +operation set rather than to the harness. OpenCode 1.x installs `opencode-ai` +and 2.x installs `@opencode/cli`; both publish the same `opencode` binary. +Installation-layout detection recognizes every package a harness has used. +Repair prefers the generation that already owns the installed version, so an +out-of-range 1.x release is repaired within 1.x. When the policy only admits a +generation published under another package, Blue never switches packages +itself: it returns manual guidance to uninstall the current package and install +the other, as the vendor requires. A fresh install with no detected binary uses +the newest generation the policy admits. + Wrapped-launch update suppression is also generation-owned. Each named `Operations` set declares how its harness version disables native update checks or automatic installation. The same operation decorates both a newly reconciled @@ -228,8 +239,22 @@ adapters: throughout that interval: `SessionEnd` arrived at `1.0.85`, so it is not supported uniformly. These releases are recorded in the [upstream changelog](https://github.com/anthropics/claude-code/blob/main/CHANGELOG.md). -- Kimi and OpenCode retain one production interval each. Candidate transitions - and the evidence still needed are recorded in +- OpenCode: `2.0.0` ships under a new npm package (`@opencode/cli`) and is + incompatible with the 1.x output in four ways, so `opencode-v2_0_0` has its + own operation set. 1.x plugins do not load, so the session plugin uses the + v2 `{ id, setup(ctx) }` API and uploads on `session.execution.*`. The CLI + attaches to a shared per-user background service that never sees Blue's launch + environment, so every launch and restore command passes `--standalone`. + `OPENCODE_CONFIG_DIR` replaces the user's global configuration directory + instead of layering over it. Blue therefore renders native v2 configuration + in `OPENCODE_CONFIG_CONTENT` and references managed plugins and skills by + absolute path; package agents are inlined, because no configuration key + names an agent directory. Restoration uses `session import`, and 1.x exports + cannot be imported by 2.x, so restoring a bundle on the other generation is + refused before OpenCode runs. See the upstream + [V1 migration guide](https://github.com/anomalyco/opencode/blob/dev/services/www/src/docs/content/migrate-v1.mdx). +- Kimi retains one production interval. Candidate transitions and the evidence + still needed are recorded in `crates/gh-config/tests/fixtures/future-boundaries.json`; they are not production boundaries. diff --git a/crates/gh-cli/src/commands.rs b/crates/gh-cli/src/commands.rs index 5c2a86f..1f879fb 100644 --- a/crates/gh-cli/src/commands.rs +++ b/crates/gh-cli/src/commands.rs @@ -2745,12 +2745,7 @@ fn run_prepared( ); tracing::debug!(elapsed_ms = startup_started.elapsed().as_millis(), harness = %harness, "coding agent ready to launch"); - let launch_args = write - .launch_args - .iter() - .cloned() - .chain(args.iter().cloned()) - .collect::>(); + let launch_args = merge_launch_args(&write.launch_args, args); // Mint this blue run's session identity and inject it into the agent process // env only. It must NEVER enter the gh-config plan env (goldens/fingerprints // stay deterministic); the managed session-start hook reads it back to map @@ -3756,10 +3751,13 @@ fn finalize_blue_session_inner(blue_session_id: &str) -> Result<()> { // Synthesize a hook-style payload from the metadata so each harness routes // through its native capture path (kimi via its filesystem resolver, // opencode via its stable plugin export). + // `reason: exit` tells a capture operation the native process is gone, so + // it may re-read native state the in-session hook could not finish writing. let payload = serde_json::json!({ "session_id": session_id, "transcript_path": metadata.transcript_path, "cwd": metadata.cwd, + "reason": "exit", }); // Capture and spool the session bundle, then upload ONLY the just-spooled // record. A full spool drain would bound exit latency by unrelated stale @@ -4239,13 +4237,23 @@ fn restore_verified_bundle( // stopped. Recompute it immediately before publication to retain sessions // another local process may have added in the meantime. prepare_native_session_index(harness, bundle, &home)?; - if harness == Harness::Opencode - && opencode_session_exists(destination, &bundle.manifest.native_session_id)? - { - bail!( - "native session collision for {}; the local OpenCode session was not changed", - bundle.manifest.native_session_id - ); + let native_import = registration.implementation.session_import(); + if native_import.is_some() { + ensure_installed_generation(harness, registration)?; + } + if let Some(import) = native_import { + if native_session_exists( + harness, + import, + destination, + &bundle.manifest.native_session_id, + )? { + bail!( + "native session collision for {}; the local {} session was not changed", + bundle.manifest.native_session_id, + harness.metadata().label + ); + } } if !preflight { gh_config::session_bundle::restore_home_files_with( @@ -4272,39 +4280,55 @@ fn restore_verified_bundle( } } let mut native_session_id = bundle.manifest.native_session_id.clone(); - if harness == Harness::Opencode && !preflight { + if let Some(import) = native_import.filter(|_| !preflight) { + let label = harness.metadata().label; let export = bundle .manifest .files .iter() .find(|file| file.role == "portable_export") .and_then(|file| bundle.files.get(&file.path)) - .ok_or_else(|| anyhow!("OpenCode bundle has no portable export"))?; - let import_path = - session_spool_dir()?.join(format!("opencode-import-{}.json", std::process::id())); + .ok_or_else(|| anyhow!("{label} bundle has no portable export"))?; + let import_path = session_spool_dir()?.join(format!( + "{}-import-{}.json", + harness.key(), + std::process::id() + )); gh_common::write_atomic(&import_path, export)?; - let mut command = std::process::Command::new( - gh_common::which("opencode").unwrap_or_else(|| PathBuf::from("opencode")), - ); - command.arg("import").arg(&import_path); + let mut command = native_command(harness); + command.args((import.import_args)(&import_path, destination)); if let Some(destination) = destination { command.current_dir(destination); } - let output = command.output().context("running opencode import")?; + let output = command + .output() + .with_context(|| format!("running {label} session import")); let _ = std::fs::remove_file(&import_path); + let output = output?; + let stderr = String::from_utf8_lossy(&output.stderr); if !output.status.success() { - bail!( - "opencode import failed: {}", - String::from_utf8_lossy(&output.stderr).trim() - ); + bail!("{label} session import failed: {}", stderr.trim()); } - native_session_id = String::from_utf8_lossy(&output.stdout) + native_session_id = match String::from_utf8_lossy(&output.stdout) .lines() - .find_map(|line| line.trim().strip_prefix("Imported session:")) + .find_map(|line| line.trim().strip_prefix(import.imported_prefix)) .map(str::trim) .filter(|value| !value.is_empty()) - .map(str::to_owned) - .ok_or_else(|| anyhow!("opencode import did not report an imported session ID"))?; + { + Some(id) => id.to_owned(), + // The pre-import probe races with other local processes; a vendor + // that reports the conflict and exits 0 still left nothing changed. + None if import + .collision_marker + .is_some_and(|marker| stderr.contains(marker)) => + { + bail!( + "native session collision for {}; the local {label} session was not changed", + bundle.manifest.native_session_id + ) + } + None => bail!("{label} session import did not report an imported session ID"), + }; } let args = match harness { Harness::Codex => vec!["resume", native_session_id.as_str()], @@ -4325,32 +4349,97 @@ fn restore_verified_bundle( Ok(result) } -fn opencode_session_exists(destination: Option<&Path>, session_id: &str) -> Result { - let mut command = std::process::Command::new( - gh_common::which("opencode").unwrap_or_else(|| PathBuf::from("opencode")), - ); - command.args([ - "session", - "list", - "--max-count", - "2147483647", - "--format", - "json", - ]); +/// Managed launch arguments precede the user's argv. A managed boolean flag the +/// user already passed (OpenCode 2.x `--standalone`) is not repeated; flags +/// followed by a managed value are always kept with their value. +fn merge_launch_args(managed: &[String], user: &[String]) -> Vec { + let boolean = |index: usize| { + managed[index].starts_with("--") + && !managed[index].contains('=') + && managed + .get(index + 1) + .is_none_or(|next| next.starts_with('-')) + }; + managed + .iter() + .enumerate() + .filter(|(index, arg)| !(boolean(*index) && user.contains(arg))) + .map(|(_, arg)| arg) + .chain(user) + .cloned() + .collect() +} + +fn native_command(harness: Harness) -> std::process::Command { + let program = harness.binary_names()[0]; + std::process::Command::new(gh_common::which(program).unwrap_or_else(|| PathBuf::from(program))) +} + +/// Native import formats are generation-specific: a bundle recorded under one +/// generation is refused before the installed CLI of another is invoked. +fn ensure_installed_generation( + harness: Harness, + recorded: &gh_config::implementations::ImplementationRegistration, +) -> Result<()> { + match gh_harness::detect(harness).and_then(|installed| installed.version) { + Some(version) => generation_mismatch(harness, recorded, &version).map_or(Ok(()), Err), + None => Ok(()), + } +} + +fn generation_mismatch( + harness: Harness, + recorded: &gh_config::implementations::ImplementationRegistration, + installed: &semver::Version, +) -> Option { + let current = gh_config::implementations::definition(harness).select(installed)?; + if current.interval.profile == recorded.interval.profile { + return None; + } + let interval = recorded.interval; + let range = match interval.before { + Some(before) => format!(">={}, <{before}", interval.introduced), + None => format!(">={}", interval.introduced), + }; + Some(anyhow!( + "session was recorded with {label} profile `{}`; resuming requires {label} {range}, but the installed {label} {installed} uses `{}`", + interval.profile, + current.interval.profile, + label = harness.metadata().label, + )) +} + +fn native_session_exists( + harness: Harness, + import: &gh_config::implementations::NativeSessionImport, + destination: Option<&Path>, + session_id: &str, +) -> Result { + let label = harness.metadata().label; + let mut command = native_command(harness); + command.args(import.list_args); if let Some(destination) = destination { command.current_dir(destination); } let output = command .output() - .context("checking for a local OpenCode session collision")?; + .with_context(|| format!("checking for a local {label} session collision"))?; if !output.status.success() { bail!( - "could not safely check OpenCode session collisions: {}", + "could not safely check {label} session collisions: {}", String::from_utf8_lossy(&output.stderr).trim() ); } - let sessions: serde_json::Value = serde_json::from_slice(&output.stdout) - .context("decoding `opencode session list --format json`")?; + session_list_contains(&output.stdout, session_id, label) +} + +/// OpenCode 1.x prints nothing, not `[]`, when no sessions exist. +fn session_list_contains(stdout: &[u8], session_id: &str, label: &str) -> Result { + if stdout.trim_ascii().is_empty() { + return Ok(false); + } + let sessions: serde_json::Value = serde_json::from_slice(stdout) + .with_context(|| format!("decoding the {label} session list"))?; Ok(sessions.as_array().is_some_and(|sessions| { sessions.iter().any(|session| { session.get("id").and_then(serde_json::Value::as_str) == Some(session_id) @@ -5110,6 +5199,73 @@ fn set_executable(_path: &std::path::Path) {} #[cfg(test)] mod tests { + #[test] + fn opencode_sessions_resume_only_on_their_recording_generation() { + let recorded = |profile| { + gh_config::implementations::implementation_for_profile(Harness::Opencode, profile) + .unwrap() + }; + let version = |raw| semver::Version::parse(raw).unwrap(); + let error = generation_mismatch( + Harness::Opencode, + recorded("opencode-v0_0_0"), + &version("2.2.5"), + ) + .unwrap() + .to_string(); + assert!(error.contains("`opencode-v0_0_0`"), "{error}"); + assert!(error.contains(">=0.0.0, <2.0.0"), "{error}"); + assert!(error.contains("2.2.5 uses `opencode-v2_0_0`"), "{error}"); + let error = generation_mismatch( + Harness::Opencode, + recorded("opencode-v2_0_0"), + &version("1.18.25"), + ) + .unwrap() + .to_string(); + assert!(error.contains("requires OpenCode >=2.0.0"), "{error}"); + assert!(generation_mismatch( + Harness::Opencode, + recorded("opencode-v1"), + &version("2.0.0") + ) + .is_none()); + assert!(generation_mismatch( + Harness::Opencode, + recorded("opencode-v0_0_0"), + &version("1.19.5") + ) + .is_none()); + } + + #[test] + fn managed_boolean_flags_are_not_repeated_but_valued_flags_are_kept() { + let owned = |args: &[&str]| args.iter().map(|arg| arg.to_string()).collect::>(); + assert_eq!( + merge_launch_args( + &owned(&["--standalone"]), + &owned(&["--standalone", "-s", "id"]) + ), + owned(&["--standalone", "-s", "id"]) + ); + assert_eq!( + merge_launch_args(&owned(&["--standalone"]), &owned(&["run", "hi"])), + owned(&["--standalone", "run", "hi"]) + ); + assert_eq!( + merge_launch_args( + &owned(&["--config", "check_for_update_on_startup=false"]), + &owned(&["--config", "model=x"]) + ), + owned(&[ + "--config", + "check_for_update_on_startup=false", + "--config", + "model=x" + ]) + ); + } + use super::*; use gh_config::resolve_compatibility; @@ -6671,4 +6827,23 @@ mod tests { assert!(prepared(Some("git@example.com:other/repo.git")).repository_mismatch()); assert!(prepared(None).repository_mismatch()); } + + #[test] + fn session_list_contains_treats_empty_output_as_no_sessions() { + for stdout in ["", "\n", "[]"] { + assert!(!session_list_contains(stdout.as_bytes(), "ses_1", "OpenCode").unwrap()); + } + } + + #[test] + fn session_list_contains_matches_session_ids() { + let stdout = br#"[{"id":"ses_1"},{"id":"ses_2"}]"#; + assert!(session_list_contains(stdout, "ses_2", "OpenCode").unwrap()); + assert!(!session_list_contains(stdout, "ses_3", "OpenCode").unwrap()); + } + + #[test] + fn session_list_contains_rejects_non_json_output() { + assert!(session_list_contains(b"not json", "ses_1", "OpenCode").is_err()); + } } diff --git a/crates/gh-cli/src/repair.rs b/crates/gh-cli/src/repair.rs index 4cf0dee..84e9623 100644 --- a/crates/gh-cli/src/repair.rs +++ b/crates/gh-cli/src/repair.rs @@ -1,7 +1,10 @@ //! Installation-aware repair. Plans contain argv, never executable shell strings. use anyhow::{anyhow, bail, Context, Result}; use gh_common::{Harness, InstallInvocation}; -use gh_config::{resolve_compatibility, supported_install, CompatibilityFailure, HarnessContext}; +use gh_config::{ + resolve_compatibility, supported_install, supported_repair_install, CompatibilityFailure, + HarnessContext, +}; use gh_harness::{Detected, InstallMethod, Installation}; use gh_service::HarnessPolicy; use semver::Version; @@ -160,6 +163,52 @@ fn guidance(installation: &Installation) -> String { ) } +/// A repair whose policy-compatible release is published under a different +/// package than the active installation (OpenCode 1.x `opencode-ai` versus +/// 2.x `@opencode/cli`). Upstream requires removing one before installing the +/// other, so Blue never performs the switch itself. +#[derive(Debug)] +struct PackageSwitch { + current: String, + target: InstallInvocation, +} + +fn package_switch( + harness: Harness, + installation: &Installation, + installed: Option<&Version>, + policy: &HarnessPolicy, +) -> Option { + let target = supported_repair_install(harness, policy, installed).ok()?; + let target_package = gh_config::install_package(&target)?; + let current = match &installation.method { + InstallMethod::NpmGlobal { package, .. } => package.clone(), + InstallMethod::OpenCodeStandalone => gh_config::generation_package(harness, installed?)?, + _ => return None, + }; + (current != target_package).then_some(PackageSwitch { current, target }) +} + +fn switch_remedy(harness: Harness, installation: &Installation, switch: &PackageSwitch) -> String { + let selector = switch + .target + .args + .last() + .map(String::as_str) + .unwrap_or_default(); + match &installation.method { + InstallMethod::NpmGlobal { prefix, .. } => format!( + "To switch manually (POSIX shell or PowerShell):\n npm uninstall -g --prefix {prefix:?} {}\n npm install -g --prefix {prefix:?} '{}'\nThen retry Blue.", + switch.current, + selector.replace('\'', "'\\''"), + ), + _ => format!( + "To switch manually, remove this {harness} installation using its installer, then run:\n {}\nEnsure npm's executable directory is on PATH, then retry Blue.", + switch.target.display + ), + } +} + /// Keeps operational details available without flattening them into the terminal message. #[derive(Debug)] pub(crate) struct ManualRepairRequired { @@ -225,7 +274,11 @@ impl ManualRepairRequired { harness: Harness, policy: &HarnessPolicy, ) -> Self { + let switch = package_switch(harness, &installation, failure.installed(), policy); let reason = match &installation.method { + _ if switch.is_some() => { + "The supported release is published under a different package, and Blue does not switch packages automatically." + } InstallMethod::Homebrew { .. } => { "Blue cannot automatically install a specific version with Homebrew." } @@ -237,7 +290,9 @@ impl ManualRepairRequired { } _ => "Automatic repair requires an interactive terminal.", }; - let remedy = if refusal(&installation.method).is_none() { + let remedy = if let Some(switch) = &switch { + switch_remedy(harness, &installation, switch) + } else if refusal(&installation.method).is_none() { format!( "To repair manually:\n {}\nThen retry Blue.", manual_action(&installation) @@ -491,11 +546,15 @@ pub(crate) fn ensure_compatible_version( Err(error) => error, }; let installation = runtime.installation(&detected); - if !interactive || refusal(&installation.method).is_some() { + if !interactive + || refusal(&installation.method).is_some() + || package_switch(harness, &installation, detected.version.as_ref(), policy).is_some() + { return Err(ManualRepairRequired::new(error, installation, harness, policy).into()); } let manual = guidance(&installation); - let invocation = supported_install(harness, policy)?; + // Stay within the installed generation's release line when the policy allows it. + let invocation = supported_repair_install(harness, policy, detected.version.as_ref())?; let version = runtime.lookup(harness, &invocation)?; // A registry response is not authority to bypass the original policy. resolve_compatibility(harness, Some(&version), Some(&version.to_string()), policy) @@ -729,7 +788,7 @@ mod tests { (Harness::Codex, "0.145.0"), (Harness::Claude, "2.1.252"), (Harness::Kimi, "0.0.0"), - (Harness::Opencode, "1.0.0"), + (Harness::Opencode, "2.2.5"), ] { let mut winner = detected("/new/bin/agent", version); winner.harness = harness; @@ -751,7 +810,13 @@ mod tests { args: vec![ "install".into(), "-g".into(), - format!("{}@{version}", gh_harness::install::npm_package(harness)) + format!( + "{}@{version}", + gh_config::install_package( + &supported_install(harness, &HarnessPolicy::default()).unwrap() + ) + .unwrap() + ) ] }] ); @@ -854,6 +919,108 @@ mod tests { assert!(fake.calls.contains(&"ownership")); } + fn opencode( + fake: &mut Fake, + version: &str, + requirement: Option<&str>, + ) -> Result<(Detected, HarnessContext, bool)> { + let mut installed = detected("/prefix/bin/opencode", version); + installed.harness = Harness::Opencode; + let policy = HarnessPolicy { + version_requirement: requirement.map(str::to_owned), + ..HarnessPolicy::default() + }; + ensure_compatible_version(Harness::Opencode, installed, &policy, true, fake) + } + + fn opencode_npm(package: &str) -> InstallMethod { + InstallMethod::NpmGlobal { + prefix: "/prefix".into(), + package: package.into(), + } + } + + #[test] + fn opencode_repairs_stay_inside_the_installed_release_line() { + for (package, installed, release, profile) in [ + ("opencode-ai", "1.99.0", "1.18.25", "opencode-v0_0_0"), + ("@opencode/cli", "2.99.0", "2.2.5", "opencode-v2_0_0"), + ] { + let mut fake = Fake { + method: opencode_npm(package), + release: Ok(Version::parse(release).unwrap()), + winner: Some(Detected { + harness: Harness::Opencode, + ..detected("/prefix/bin/opencode", release) + }), + ..Fake::default() + }; + let (_, context, changed) = opencode(&mut fake, installed, None).unwrap(); + assert!(changed); + assert_eq!(context.profile.id, profile); + assert_eq!( + fake.executed, + vec![RepairPlan::Command { + program: "npm".into(), + args: vec![ + "install".into(), + "-g".into(), + "--prefix".into(), + "/prefix".into(), + format!("{package}@{release}") + ] + }] + ); + } + } + + #[test] + fn opencode_never_switches_packages_automatically() { + for (method, installed, requirement, current, target) in [ + ( + opencode_npm("opencode-ai"), + "1.18.25", + ">=2", + "opencode-ai", + "@opencode/cli", + ), + ( + opencode_npm("@opencode/cli"), + "2.2.5", + "<2", + "@opencode/cli", + "opencode-ai", + ), + ( + InstallMethod::OpenCodeStandalone, + "1.18.25", + ">=2", + "opencode-ai", + "@opencode/cli", + ), + ] { + let mut fake = Fake { + method: method.clone(), + ..Fake::default() + }; + let error = opencode(&mut fake, installed, Some(requirement)).unwrap_err(); + let message = error + .downcast_ref::() + .expect("manual repair") + .to_string(); + assert!(message.contains("different package"), "{message}"); + assert!(message.contains(target), "{message}"); + if matches!(method, InstallMethod::NpmGlobal { .. }) { + assert!( + message.contains(&format!("npm uninstall -g --prefix \"/prefix\" {current}")), + "{message}" + ); + } + assert_eq!(fake.calls, vec!["ownership"]); + assert!(fake.executed.is_empty()); + } + } + #[test] fn plans_preserve_exact_argv_and_installation_owner() { let version = Version::new(2, 1, 252); @@ -864,8 +1031,10 @@ mod tests { args: vec!["install".into(), version.to_string()] } ); - for harness in Harness::ALL { - let package = gh_harness::install::npm_package(harness); + for package in Harness::ALL + .into_iter() + .flat_map(|harness| gh_harness::npm_packages(harness).iter().copied()) + { let prefix = PathBuf::from("/custom npm prefix"); assert_eq!( plan( diff --git a/crates/gh-common/src/harness.rs b/crates/gh-common/src/harness.rs index 5a725af..5cb063c 100644 --- a/crates/gh-common/src/harness.rs +++ b/crates/gh-common/src/harness.rs @@ -73,9 +73,11 @@ macro_rules! harness_catalog { label: "OpenCode", description: "OpenCode agent settings", binary_names: &["opencode"], - install_command_template: "npm install -g 'opencode-ai@{version}'", + // The newest generation's package. OpenCode 1.x installs from + // `opencode-ai`; each generation's adapter owns its own plan. + install_command_template: "npm install -g '@opencode/cli@{version}'", install_program: "npm", - install_args: &["install", "-g", "opencode-ai@{version}"], + install_args: &["install", "-g", "@opencode/cli@{version}"], } } }; diff --git a/crates/gh-config/Cargo.toml b/crates/gh-config/Cargo.toml index 93c84f9..a6d67ff 100644 --- a/crates/gh-config/Cargo.toml +++ b/crates/gh-config/Cargo.toml @@ -21,6 +21,7 @@ flate2 = "1" tar = "0.4" semver = { workspace = true } rand = { workspace = true } +serde_yaml = { workspace = true } [target.'cfg(unix)'.dependencies] libc = "0.2" diff --git a/crates/gh-config/src/adapters/mod.rs b/crates/gh-config/src/adapters/mod.rs index d7384b0..3bfc73a 100644 --- a/crates/gh-config/src/adapters/mod.rs +++ b/crates/gh-config/src/adapters/mod.rs @@ -182,6 +182,21 @@ pub(crate) fn apply_session_upload_disposition( } } +/// Restoration through the harness's own CLI rather than restored files. The +/// commands are versioned with the implementation because vendors move them +/// (OpenCode 1.x `import `, 2.x `session import `). +#[derive(Debug)] +pub struct NativeSessionImport { + /// Arguments printing every local session as a JSON array of `{ "id" }` objects. + pub list_args: &'static [&'static str], + /// Arguments importing a portable export into `directory`. + pub import_args: fn(&Path, Option<&Path>) -> Vec, + /// Stdout line prefix that precedes the imported native session ID. + pub imported_prefix: &'static str, + /// Stderr text reported when the session already exists locally. + pub collision_marker: Option<&'static str>, +} + /// Filesystem authority for a single compatibility implementation. #[derive(Debug, Default)] pub struct ImplementationPaths { @@ -274,6 +289,10 @@ pub trait HarnessImplementation: std::fmt::Debug + Sync { "this compatibility profile does not support native session restoration", ) } + /// Native CLI import used to restore a portable session export. + fn session_import(&self) -> Option<&'static NativeSessionImport> { + None + } fn gateway_wiring(&self, gateway: &GatewayConfig) -> Result; fn validate_components(&self, adapter: &PackageAdapter) -> Result<(), GhError>; fn validate_staged_components( @@ -847,6 +866,18 @@ mod tests { assert!(format!("{kimi_start:?}").contains("session-start kimi")); assert!(format!("{kimi_start:?}").contains("--profile 'kimi-v1'")); + // The 2.x plugin uses the v2 API and the finished-turn events 2.2 emits, + // and owns its own profile. + let opencode_v2 = opencode::v2_0_0::session_upload_plugin_v2("opencode-v2_0_0").unwrap(); + assert!(opencode_v2.contains("export default {")); + assert!(opencode_v2.contains("ctx.event.subscribe")); + assert!(opencode_v2.contains("session.execution.succeeded")); + assert!(opencode_v2.contains("\"session\", \"export\"")); + assert!(opencode_v2.contains("\"--profile\", \"opencode-v2_0_0\"")); + assert!(opencode_v2.contains("\"session-start\", \"opencode\"")); + assert!(!opencode_v2.contains("Bun.")); + assert!(!opencode.contains("ctx.event")); + // The opencode plugin carries session-start inline (no separate renderer). assert!(opencode.contains("session-start")); assert!(opencode.contains("\"session-start\", \"opencode\"")); @@ -895,6 +926,7 @@ mod tests { claude::v2_0_12::SPEC.session_resume, kimi::v0_0_0::SPEC.session_resume, opencode::v0_0_0::SPEC.session_resume, + opencode::v2_0_0::SPEC.session_resume, ] { assert!(matches!(capability, Feature::Supported(()))); } @@ -933,6 +965,23 @@ mod tests { opencode::v0_0_0::SPEC.session_start, Feature::Supported(()) )); + assert!(matches!( + opencode::v2_0_0::SPEC.session_start, + Feature::Supported(()) + )); + assert!(matches!( + opencode::v2_0_0::SPEC.session_upload, + Feature::Supported(_) + )); + // 1.x keeps its frozen operation set; 2.x selects its own. + assert!(std::ptr::eq( + opencode::v0_0_0::SPEC.operations, + &opencode::V1_OPERATIONS + )); + assert!(std::ptr::eq( + opencode::v2_0_0::SPEC.operations, + &opencode::V2_OPERATIONS + )); assert!(!std::ptr::eq( claude::v0_0_0::SPEC.operations, @@ -1017,6 +1066,8 @@ mod tests { (Harness::Claude, "2.1.247 (Claude Code)", "2.1.247"), (Harness::Kimi, "kimi v0.37.1", "0.37.1"), (Harness::Opencode, "opencode 1.18.7", "1.18.7"), + (Harness::Opencode, "1.18.25", "1.18.25"), + (Harness::Opencode, "opencode v2.2.5", "2.2.5"), (Harness::Claude, "claude v2.1.0-beta.2", "2.1.0-beta.2"), ] { assert_eq!( diff --git a/crates/gh-config/src/adapters/opencode/inspection.rs b/crates/gh-config/src/adapters/opencode/inspection.rs index e7ab67b..ffcde77 100644 --- a/crates/gh-config/src/adapters/opencode/inspection.rs +++ b/crates/gh-config/src/adapters/opencode/inspection.rs @@ -65,6 +65,50 @@ pub(crate) fn opencode_current(_: &HarnessPolicy, files: &[PathBuf]) -> BTreeMap values } +/// OpenCode 2.x: the managed overlay uses the native v2 shape, while a user's +/// global file may still be in either shape. +pub(crate) fn opencode_current_v2( + _: &HarnessPolicy, + files: &[PathBuf], +) -> BTreeMap { + let mut values = BTreeMap::new(); + if let Some(config) = find(files, "runtime/opencode/opencode.json") + .or_else(|| find(files, ".config/opencode/opencode.json")) + .and_then(json) + { + if let Some(value) = config.get("model").and_then(serde_json::Value::as_str) { + values.insert("model".into(), value.into()); + } + let mcp = config.get("mcp").and_then(serde_json::Value::as_object); + let servers = mcp + .and_then(|mcp| mcp.get("servers")) + .and_then(serde_json::Value::as_object); + values.insert("mcp".into(), names(servers.or(mcp))); + if let Some(value) = config + .get("providers") + .and_then(|providers| providers.get("governed")) + .and_then(|governed| governed.get("settings")) + .or_else(|| { + config + .get("provider") + .and_then(|provider| provider.get("governed")) + .and_then(|governed| governed.get("options")) + }) + .and_then(|settings| settings.get("baseURL")) + .and_then(serde_json::Value::as_str) + { + values.insert("gateway".into(), value.into()); + } + } + if files.iter().any(|path| { + path.ends_with("blue-session-upload/index.js") + && std::fs::read_to_string(path).is_ok_and(|body| body.contains("session-upload")) + }) { + values.insert("session_upload_hook".into(), "enabled".into()); + } + values +} + pub(crate) fn opencode_proposed( mut values: BTreeMap, policy: &HarnessPolicy, diff --git a/crates/gh-config/src/adapters/opencode/mod.rs b/crates/gh-config/src/adapters/opencode/mod.rs index 3e6f3b7..3c279e7 100644 --- a/crates/gh-config/src/adapters/opencode/mod.rs +++ b/crates/gh-config/src/adapters/opencode/mod.rs @@ -2,7 +2,9 @@ use super::*; mod inspection; mod writer; +mod writer_v2; pub mod v0_0_0; +pub mod v2_0_0; pub const VERSION_PROBES: &[VersionProbe] = DEFAULT_VERSION_PROBES; const DISABLE_AUTOUPDATE_ENV: &str = "OPENCODE_DISABLE_AUTOUPDATE"; @@ -23,6 +25,7 @@ pub struct Operations { inspect: InspectOperation, proposed_values: ProposedValuesOperation, transcript_path: TranscriptOperation, + session_import: &'static NativeSessionImport, } #[derive(Debug)] pub struct VersionSpec { pub operations: &'static Operations, pub session_upload: Feature, pub session_start: Feature<()>, pub session_resume: Feature<()>, pub support: &'static GenerationSupport } @@ -30,11 +33,40 @@ pub struct VersionSpec { pub operations: &'static Operations, pub session_upload pub struct Implementation { spec: &'static VersionSpec } impl Implementation { pub const fn new(spec: &'static VersionSpec) -> Self { Self { spec } } } -pub static V1_OPERATIONS: Operations = Operations { plan: plan_v1, launch, launch_controls: disable_auto_updates, paths, native_migration_needs_review, gateway_wiring, validate_components, validate_staged_components: accept_staged_components, package_components: collect_package_components, install_plan, inspect, proposed_values, transcript_path: payload_transcript_path }; -pub static IMPLEMENTATIONS: &[ImplementationRegistration] = &[ImplementationRegistration { - interval: VersionInterval { profile: "opencode-v0_0_0", aliases: &["opencode-v1"], introduced: "0.0.0", before: None, verified_before: "1.18.26-0", lifecycle: ImplementationLifecycle::Supported }, - implementation: &v0_0_0::IMPLEMENTATION, -}]; +pub static V1_OPERATIONS: Operations = Operations { plan: plan_v1, launch, launch_controls: disable_auto_updates, paths, native_migration_needs_review, gateway_wiring, validate_components, validate_staged_components: accept_staged_components, package_components: collect_package_components, install_plan, inspect, proposed_values, transcript_path: payload_transcript_path, session_import: &V1_SESSION_IMPORT }; +/// OpenCode 2.x: a new package, a private `--standalone` server per launch, +/// native v2 configuration without `OPENCODE_CONFIG_DIR`, the v2 plugin API, +/// and `session import`. +pub static V2_OPERATIONS: Operations = Operations { plan: plan_v2, launch: launch_v2, launch_controls: disable_updates_v2, paths, native_migration_needs_review, gateway_wiring, validate_components, validate_staged_components: accept_staged_components, package_components: collect_package_components, install_plan: install_plan_v2, inspect: inspect_v2, proposed_values, transcript_path: exported_transcript_path, session_import: &V2_SESSION_IMPORT }; +pub static IMPLEMENTATIONS: &[ImplementationRegistration] = &[ + ImplementationRegistration { + interval: VersionInterval { profile: "opencode-v0_0_0", aliases: &[], introduced: "0.0.0", before: Some("2.0.0"), verified_before: "1.18.26-0", lifecycle: ImplementationLifecycle::Supported }, + implementation: &v0_0_0::IMPLEMENTATION, + }, + ImplementationRegistration { + interval: VersionInterval { profile: "opencode-v2_0_0", aliases: &["opencode-v1"], introduced: "2.0.0", before: None, verified_before: "2.2.6-0", lifecycle: ImplementationLifecycle::Supported }, + implementation: &v2_0_0::IMPLEMENTATION, + }, +]; + +const LIST_SESSIONS: &[&str] = &["session", "list", "--max-count", "2147483647", "--format", "json"]; +static V1_SESSION_IMPORT: NativeSessionImport = NativeSessionImport { list_args: LIST_SESSIONS, import_args: import_args_v1, imported_prefix: "Imported session:", collision_marker: None }; +// The restore commands run a private server too: without `--standalone` they +// would start the shared per-user background service as a side effect. +static V2_SESSION_IMPORT: NativeSessionImport = NativeSessionImport { + list_args: &[writer_v2::STANDALONE, "session", "list", "--max-count", "2147483647", "--format", "json"], + import_args: import_args_v2, + imported_prefix: "Imported session:", + // 2.x answers an existing session ID with HTTP 409, which the CLI reports + // on stderr while still exiting 0. + collision_marker: Some("Session already exists"), +}; +fn import_args_v1(export: &Path, _: Option<&Path>) -> Vec { vec!["import".into(), export.into()] } +fn import_args_v2(export: &Path, directory: Option<&Path>) -> Vec { + let mut args: Vec = vec![writer_v2::STANDALONE.into(), "session".into(), "import".into(), export.into()]; + if let Some(directory) = directory { args.extend(["--directory".into(), directory.into()]); } + args +} pub(crate) fn session_upload_plugin(profile: &str) -> Result { let exe = serde_json::to_string(&crate::util::harness_executable()?.to_string_lossy().as_ref()).map_err(|error| GhError::Serde(error.to_string()))?; @@ -119,6 +151,133 @@ export const BlueSessionUpload = async ({ client, directory }) => { } "#; +pub(crate) fn session_upload_plugin_v2(profile: &str) -> Result { + let exe = serde_json::to_string(&crate::util::harness_executable()?.to_string_lossy().as_ref()).map_err(|error| GhError::Serde(error.to_string()))?; + let profile = serde_json::to_string(profile).map_err(|error| GhError::Serde(error.to_string()))?; + Ok(SESSION_PLUGIN_TEMPLATE_V2 + .replace("__BLUE_EXE__", &exe) + .replace("__BLUE_PROFILE__", &profile)) +} + +// The OpenCode 2.x plugin API: a default-exported `{ id, setup(ctx) }` whose +// event stream replaces the v1 `event` hook and whose cleanup function aborts +// the subscription. 2.2 declares but never publishes `session.status` and +// `session.idle`; a finished turn is `session.execution.{succeeded,failed, +// interrupted}`, so the plugin uploads on those (and on an idle status, should +// a later release start emitting it). Plugins are instantiated per location +// and every instance sees every location's events, so only the instance whose +// location owns the session acts on it. The transcript is the CLI's own +// `session export` output, so restoration feeds `session import` exactly what +// it expects; that export runs a private server, which must not re-run this +// plugin. +const SESSION_PLUGIN_TEMPLATE_V2: &str = r#"// Managed by Blue. Changes will be overwritten. +import { spawn } from "node:child_process" +import { mkdir, rm, writeFile } from "node:fs/promises" +import { dirname } from "node:path" + +const BLUE_SESSION_ID = process.env.BLUE_SESSION_ID +const BLUE_DATA_DIR = process.env.BLUE_DATA_DIR +const EXPORTING = "BLUE_OPENCODE_SESSION_EXPORT" +const blueTranscriptPath = (sessionID) => + BLUE_SESSION_ID && BLUE_DATA_DIR + ? `${BLUE_DATA_DIR}/sessions/${BLUE_SESSION_ID}/opencode-transcript-${sessionID}.json` + : `${process.env.TMPDIR || "/tmp"}/blue-opencode-${sessionID}.json` + +const run = (command, args, input, env = process.env) => + new Promise((resolve, reject) => { + const child = spawn(command, args, { env, stdio: ["pipe", "pipe", "inherit"] }) + let stdout = "" + child.stdout.setEncoding("utf8") + child.stdout.on("data", (chunk) => { stdout += chunk }) + child.on("error", reject) + child.on("close", (code) => + code === 0 ? resolve(stdout) : reject(new Error(`${command} ${args[0]} exited with ${code}`))) + child.stdin.end(input ?? "") + }) + +export default { + id: "blue.session-upload", + setup(ctx) { + if (process.env[EXPORTING]) return + const directory = ctx.location.directory + const controller = new AbortController() + const uploaded = new Set() + const started = new Set() + const topLevel = async (sessionID) => { + const response = await ctx.session.get({ sessionID }) + const session = response?.data ?? response + const owner = session?.location?.directory + return !session?.parentID && (!owner || owner === directory) + } + const ensureStarted = async (sessionID) => { + if (!sessionID || started.has(sessionID)) return + started.add(sessionID) + try { + if (!(await topLevel(sessionID))) return + await run(__BLUE_EXE__, ["session-start", "opencode", "--profile", __BLUE_PROFILE__], JSON.stringify({ + session_id: sessionID, source: "startup", cwd: directory, + hook_event_name: "SessionStart", transcript_path: blueTranscriptPath(sessionID), + })) + } catch (error) { + started.delete(sessionID) + console.error("Blue session start failed", error) + } + } + const exportTranscript = async (sessionID) => { + // Exporting reads the shared database only; the managed overlay would + // just start its MCP servers and plugins again. + const { OPENCODE_CONFIG_CONTENT: _, ...env } = process.env + const exported = await run(process.execPath, ["--standalone", "session", "export", sessionID], "", + { ...env, [EXPORTING]: "1" }) + const transcriptPath = blueTranscriptPath(sessionID) + await mkdir(dirname(transcriptPath), { recursive: true }) + await writeFile(transcriptPath, exported) + return transcriptPath + } + const upload = async (sessionID) => { + if (!sessionID) return + if (uploaded.has(sessionID)) { + // Keep the stable transcript current for `blue run`'s post-exit upload. + if (BLUE_SESSION_ID && (await topLevel(sessionID).catch(() => false))) { + await exportTranscript(sessionID).catch((error) => console.error("Blue session export failed", error)) + } + return + } + uploaded.add(sessionID) + let transcriptPath + try { + if (!(await topLevel(sessionID))) return + transcriptPath = await exportTranscript(sessionID) + await run(__BLUE_EXE__, ["session-upload", "opencode", "--profile", __BLUE_PROFILE__], JSON.stringify({ + session_id: sessionID, transcript_path: transcriptPath, cwd: directory, + hook_event_name: "SessionEnd", reason: "idle", + })) + } catch (error) { + uploaded.delete(sessionID) + console.error("Blue session upload failed", error) + } finally { + if (transcriptPath && !BLUE_SESSION_ID) await rm(transcriptPath, { force: true }).catch(() => {}) + } + } + const finished = new Set([ + "session.execution.succeeded", "session.execution.failed", "session.execution.interrupted", + ]) + void (async () => { + for await (const event of ctx.event.subscribe({ signal: controller.signal })) { + const sessionID = event.data?.sessionID + await ensureStarted(sessionID) + const idle = finished.has(event.type) || + (event.type === "session.status" && event.data?.status?.type === "idle") + if (idle) await upload(sessionID) + } + })().catch((error) => { + if (!controller.signal.aborted) console.error("Blue session events failed", error) + }) + return () => controller.abort() + }, +} +"#; + impl HarnessImplementation for Implementation { fn support(&self) -> &'static GenerationSupport { self.spec.support } fn launch(&self, home: &Path, wiring: Option<&GatewayWiring>, policy: &HarnessPolicy, spec: crate::HarnessLaunchSpec) -> Result { @@ -131,6 +290,7 @@ impl HarnessImplementation for Implementation { match self.spec.session_upload { Feature::Supported(_) => SessionUploadDisposition::Installed, Feature::Unsupported(reason) => SessionUploadDisposition::Unsupported { reason: reason.into() } } } fn session_resume_capability(&self) -> Feature<()> { self.spec.session_resume } + fn session_import(&self) -> Option<&'static NativeSessionImport> { Some(self.spec.operations.session_import) } fn native_migration_needs_review(&self, path: &Path, policy: &HarnessPolicy) -> bool { (self.spec.operations.native_migration_needs_review)(path, policy) } fn paths(&self, home: &Path) -> ImplementationPaths { (self.spec.operations.paths)(home) } fn plan(&self, input: &ReconcileInput<'_>, packages: &ResolvedPackages) -> Result { @@ -158,6 +318,72 @@ fn plan_v1(implementation: &Implementation, input: &ReconcileInput<'_>, packages plan.files = report.files; plan.env = report.env; plan.launch_args = report.launch_args; plan.warnings = report.warnings; plan.normalize(); Ok(plan) } +fn plan_v2(implementation: &Implementation, input: &ReconcileInput<'_>, packages: &ResolvedPackages) -> Result { + let paths = implementation.paths(input.home); + for path in paths.read_only_sources.iter().chain(&paths.owned_outputs).chain(&paths.native_migrations) { crate::validate_home_path(input.home, path, false)?; } + let mut plan = ReconcilePlan { owned_paths: paths.owned_outputs, ..Default::default() }; + let components = implementation.package_components(packages); + let disposition = implementation.session_upload_disposition(input); + let plugin = match implementation.spec.session_upload { Feature::Supported(render) if input.options.session_upload_enabled => Some(render(input.interval.profile)?), _ => None }; + let mut report = writer_v2::write(&mut plan, input.home, input.policy, input.gateway, plugin, &components)?; + apply_session_upload_disposition(disposition, &mut report); + let mut launch = crate::HarnessLaunchSpec { env: report.env, launch_args: report.launch_args }; + (implementation.spec.operations.launch_controls)(input.home, input.policy, &mut launch)?; + plan.files = report.files; plan.env = launch.env; plan.launch_args = launch.launch_args; plan.warnings = report.warnings; plan.normalize(); Ok(plan) +} + +fn launch_v2(home: &Path, wiring: Option<&GatewayWiring>, mut spec: crate::HarnessLaunchSpec) -> Result { + let config = crate::managed_runtime_dir(home).join("opencode").join("opencode.json"); + let contents = std::fs::read_to_string(&config).map_err(|source| GhError::Io { path: config, source })?; + spec.env.insert("OPENCODE_CONFIG_CONTENT".into(), contents); + if let Some(wiring) = wiring { spec.env.insert("BLUE_OPENCODE_GATEWAY_TOKEN".into(), wiring.token.clone()); } + if !spec.launch_args.iter().any(|arg| arg == writer_v2::STANDALONE) { spec.launch_args.insert(0, writer_v2::STANDALONE.into()); } + Ok(spec) +} + +// 2.x still reads OPENCODE_DISABLE_AUTOUPDATE from the process environment; the +// native `update: "disable"` key additionally governs the in-session notice. +fn disable_updates_v2(_: &Path, policy: &HarnessPolicy, spec: &mut crate::HarnessLaunchSpec) -> Result<(), GhError> { + if !crate::compat::policy_requires_update_suppression(policy)? { + return Ok(()); + } + let contents = spec + .env + .get("OPENCODE_CONFIG_CONTENT") + .ok_or_else(|| GhError::config("managed OpenCode launch config is missing"))?; + let contents = writer_v2::disable_update(contents)?; + spec.env.insert("OPENCODE_CONFIG_CONTENT".into(), contents); + spec.env.insert(DISABLE_AUTOUPDATE_ENV.into(), "1".into()); + Ok(()) +} +// The plugin exports on every finished turn, but the private server exits with +// the client and can cut the last export short. `blue run`'s post-exit upload +// therefore re-exports natively once nothing else is running; a failed export +// keeps whatever the plugin last wrote. +fn exported_transcript_path(definition: &HarnessDefinition, home: &Path, session_id: &str, payload: &serde_json::Value) -> Result { + let path = payload_transcript_path(definition, home, session_id, payload)?; + if payload.get("reason").and_then(serde_json::Value::as_str) == Some("exit") { + match native_export(session_id, payload.get("cwd").and_then(serde_json::Value::as_str)) { + Some(export) => { + if let Some(parent) = path.parent() { std::fs::create_dir_all(parent).map_err(|source| GhError::Io { path: parent.to_path_buf(), source })?; } + gh_common::write_atomic(&path, export)?; + } + None => tracing::debug!(session = %session_id, "native OpenCode export unavailable; using the plugin transcript"), + } + } + Ok(path) +} +fn native_export(session_id: &str, cwd: Option<&str>) -> Option> { + let mut command = std::process::Command::new(gh_common::which("opencode")?); + command.args([writer_v2::STANDALONE, "session", "export", session_id]).env(DISABLE_AUTOUPDATE_ENV, "1").stdin(std::process::Stdio::null()); + if let Some(cwd) = cwd.map(Path::new).filter(|cwd| cwd.is_dir()) { command.current_dir(cwd); } + let output = command.output().ok().filter(|output| output.status.success())?; + let export: serde_json::Value = serde_json::from_slice(&output.stdout).ok()?; + (export.pointer("/info/id").and_then(serde_json::Value::as_str) == Some(session_id)).then_some(output.stdout) +} +fn install_plan_v2(requirement: Option<&str>) -> InstallInvocation { npm_install_plan("@opencode/cli", requirement) } +fn inspect_v2(policy: &HarnessPolicy, files: &[PathBuf]) -> std::collections::BTreeMap { inspection::opencode_current_v2(policy, files) } + fn launch(home: &Path, wiring: Option<&GatewayWiring>, mut spec: crate::HarnessLaunchSpec) -> Result { let runtime = crate::managed_runtime_dir(home).join("opencode"); let config = runtime.join("opencode.json"); let contents = std::fs::read_to_string(&config).map_err(|source| GhError::Io { path: config, source })?; diff --git a/crates/gh-config/src/adapters/opencode/v2_0_0/mod.rs b/crates/gh-config/src/adapters/opencode/v2_0_0/mod.rs new file mode 100644 index 0000000..d0dd1e5 --- /dev/null +++ b/crates/gh-config/src/adapters/opencode/v2_0_0/mod.rs @@ -0,0 +1,5 @@ +use super::*; +static SUPPORT: GenerationSupport = GenerationSupport { capabilities: &["mcp", "packages", "skills", "agents", "plugins", "helpers", "gateway", "session_upload", "session_start", "session_resume"], component_rules: ComponentRules { agents_require_plugin: false, hooks_require_plugin: false, hooks_as_plugin_modules: true } }; +pub static SPEC: VersionSpec = VersionSpec { operations: &V2_OPERATIONS, session_upload: Feature::Supported(session_upload_plugin_v2), session_start: Feature::Supported(()), session_resume: Feature::Supported(()), support: &SUPPORT }; +pub static IMPLEMENTATION: Implementation = Implementation::new(&SPEC); +pub(crate) use super::session_upload_plugin_v2; diff --git a/crates/gh-config/src/adapters/opencode/writer.rs b/crates/gh-config/src/adapters/opencode/writer.rs index 9f5ecdd..c129c31 100644 --- a/crates/gh-config/src/adapters/opencode/writer.rs +++ b/crates/gh-config/src/adapters/opencode/writer.rs @@ -11,8 +11,8 @@ use serde_json::{json, Map, Value}; use crate::util::json_pretty; use crate::HarnessWrite; -const GOVERNED_PROVIDER: &str = "governed"; -const GATEWAY_TOKEN_ENV: &str = "BLUE_OPENCODE_GATEWAY_TOKEN"; +pub(super) const GOVERNED_PROVIDER: &str = "governed"; +pub(super) const GATEWAY_TOKEN_ENV: &str = "BLUE_OPENCODE_GATEWAY_TOKEN"; const ADDITIONAL_GOVERNED_MODELS: &[&str] = &["openai/gpt-5.6-sol"]; const SCHEMA: &str = "https://opencode.ai/config.json"; @@ -145,7 +145,7 @@ pub fn write( /// is the only thing that knows what it actually serves), else the built-in /// default. Strip one optional `governed/` prefix and ignore empty IDs. /// Never empty, so the provider block is always renderable. -fn governed_catalog(policy: &HarnessPolicy) -> Vec { +pub(super) fn governed_catalog(policy: &HarnessPolicy) -> Vec { let configured = policy .managed_config .extra @@ -173,7 +173,7 @@ fn governed_catalog(policy: &HarnessPolicy) -> Vec { /// Both the bare name and the `/`-prefixed spelling are accepted, so /// a native `governed/openai/gpt-x` or `openai/gpt-x` resolves to the same /// catalog entry. Anything else is a provider `enabled_providers` disables. -fn native_governed_model(native: &Map, catalog: &[String]) -> Option { +pub(super) fn native_governed_model(native: &Map, catalog: &[String]) -> Option { let model = native.get("model").and_then(Value::as_str)?; let stripped = model.split_once('/').map(|(_, rest)| rest); [Some(model), stripped] @@ -229,7 +229,7 @@ pub fn apply_packages( Ok(()) } -fn migrate_legacy_global_config( +pub(super) fn migrate_legacy_global_config( plan: &mut crate::adapters::ReconcilePlan, home: &Path, path: &Path, @@ -332,7 +332,7 @@ fn merge_mcp_block( root.insert("mcp".into(), Value::Object(mcp)); } -fn plan_skills( +pub(super) fn plan_skills( plan: &mut crate::adapters::ReconcilePlan, sources: &[std::path::PathBuf], target: &Path, diff --git a/crates/gh-config/src/adapters/opencode/writer_v2.rs b/crates/gh-config/src/adapters/opencode/writer_v2.rs new file mode 100644 index 0000000..f6c266d --- /dev/null +++ b/crates/gh-config/src/adapters/opencode/writer_v2.rs @@ -0,0 +1,572 @@ +//! OpenCode 2.x launch overlay. Native v2 configuration travels in +//! `OPENCODE_CONFIG_CONTENT`, and managed components are referenced by absolute +//! path: in v2 `OPENCODE_CONFIG_DIR` replaces the user's global configuration +//! directory instead of layering over it, so Blue no longer sets it. + +use std::path::{Path, PathBuf}; + +use gh_common::GhError; +use gh_gateway::GatewayWiring; +use gh_service::HarnessPolicy; +use serde_json::{json, Map, Value}; + +use super::writer::{ + governed_catalog, migrate_legacy_global_config, native_governed_model, plan_skills, + GATEWAY_TOKEN_ENV, GOVERNED_PROVIDER, +}; +use crate::adapters::PackageComponents; +use crate::util::json_pretty; +use crate::HarnessWrite; + +const SCHEMA: &str = "https://opencode.ai/config.json"; +/// Session plugin directory name; v2 loads local plugins from directories. +pub(super) const SESSION_PLUGIN_DIR: &str = "blue-session-upload"; +/// Every v2 launch runs a private server. The default background service is +/// shared per user and never sees Blue's launch environment, so the managed +/// configuration, gateway token, and session identity would not reach it. +pub(super) const STANDALONE: &str = "--standalone"; + +pub fn write( + plan: &mut crate::adapters::ReconcilePlan, + home: &Path, + policy: &HarnessPolicy, + wiring: Option<&GatewayWiring>, + session_upload_plugin: Option, + components: &PackageComponents, +) -> Result { + let native_path = home.join(".config").join("opencode").join("opencode.json"); + migrate_legacy_global_config(plan, home, &native_path, policy)?; + let native = plan.read_json_object(&native_path)?; + let runtime = crate::managed_runtime_dir(home).join("opencode"); + let cfg_path = runtime.join("opencode.json"); + let mut root = Map::new(); + root.insert("$schema".into(), json!(SCHEMA)); + + let catalog = governed_catalog(policy); + let mut warnings = Vec::new(); + // Gateway mode always pins the model; see the v1 writer for the rationale. + let effective_model = match (&policy.managed_config.model, wiring) { + (Some(model), _) => Some(model.clone()), + (None, None) => None, + (None, Some(_)) => { + let model = native_governed_model(&native, &catalog) + .unwrap_or_else(|| catalog[0].clone()); + warnings.push(format!( + "`harnesses.opencode.managed_config.model` is unset; defaulting to `{model}`. \ + Set it in your governance config to pin the model your gateway serves." + )); + Some(model) + } + }; + if let Some(model) = &effective_model { + let model_value = if wiring.is_some() { + format!("{GOVERNED_PROVIDER}/{model}") + } else { + model.clone() + }; + root.insert("model".into(), json!(model_value)); + } + + root.insert("permissions".into(), permission_rules(policy)); + root.insert("mcp".into(), mcp_block(&native, policy)); + + let mut files = Vec::new(); + let mut env = std::collections::BTreeMap::new(); + if let Some(w) = wiring { + let mut models = Map::new(); + if let Some(model) = &effective_model { + models.insert(model.clone(), json!({ "name": model })); + } + for available in &catalog { + models + .entry(available.clone()) + .or_insert_with(|| json!({ "name": available })); + } + root.insert( + "providers".into(), + json!({ + GOVERNED_PROVIDER: { + // Built in, so the provider needs no runtime package download. + "package": "@opencode/ai/providers/openai-compatible", + "name": "Governed (Blue)", + "settings": { + "baseURL": w.base_url, + "apiKey": format!("{{env:{GATEWAY_TOKEN_ENV}}}") + }, + "models": models + } + }), + ); + // The v2 replacement for `enabled_providers`: the last matching + // statement wins, so deny everything and then allow the gateway. + // Policies from the launch overlay rank below the user's own config + // files, the same trust boundary as every other overlay setting. + root.insert( + "policies".into(), + json!([ + { "action": "provider.use", "resource": "*", "effect": "deny" }, + { "action": "provider.use", "resource": GOVERNED_PROVIDER, "effect": "allow" } + ]), + ); + env.insert(GATEWAY_TOKEN_ENV.into(), w.token.clone()); + } + + // Rebuild the plugin tree on every reconcile. This also removes the flat + // v1 `plugins/blue-session-upload.js`, which v2 would load as a broken + // plugin if it were ever discovered. + let plugins_root = runtime.join("plugins"); + plan.remove(&plugins_root)?; + let mut plugin_dirs = Vec::new(); + if let Some(plugin) = session_upload_plugin { + let dir = plugins_root.join(SESSION_PLUGIN_DIR); + let entry = dir.join("index.js"); + plan.write(&entry, plugin)?; + files.push(entry); + plugin_dirs.push(dir); + } + for module in &components.plugin_modules { + let dir = plugins_root.join(plugin_dir_name(module)?); + if plugin_dirs.contains(&dir) { + return Err(GhError::config(format!( + "OpenCode plugin module collision at {}", + dir.display() + ))); + } + let extension = module + .extension() + .and_then(|extension| extension.to_str()) + .unwrap_or("js"); + plan.write(&dir.join(format!("index.{extension}")), plan.read(module)?)?; + files.push(dir.clone()); + plugin_dirs.push(dir); + } + if !plugin_dirs.is_empty() { + root.insert("plugins".into(), json!(paths(&plugin_dirs))); + } + + let skills = runtime.join("skills"); + plan_skills(plan, &components.skills_dirs, &skills, &mut files)?; + if !components.skills_dirs.is_empty() { + root.insert("skills".into(), json!(paths(std::slice::from_ref(&skills)))); + } + plan.component_dirs(&components.agents_dirs, &runtime.join("agents"), &mut files)?; + root.extend(agent_definitions(plan, &runtime.join("agents"))?); + // Package hooks are plugin modules for OpenCode; nothing else is emitted. + plan.remove(&runtime.join("hooks"))?; + + let mut config = serde_json::to_string_pretty(&Value::Object(root)) + .map_err(|error| GhError::Serde(error.to_string()))?; + config.push('\n'); + plan.write(&cfg_path, config.as_bytes())?; + files.insert(0, cfg_path); + env.insert("OPENCODE_CONFIG_CONTENT".into(), config); + env.extend(components.env.clone()); + crate::util::prepend_helper_paths(&mut env, &components.helpers); + + let mut launch_args = vec![STANDALONE.to_owned()]; + launch_args.extend(components.launch_args.iter().cloned()); + Ok(HarnessWrite { + files, + env, + launch_args, + package_errors: Vec::new(), + warnings, + }) +} + +/// The native spelling of v1's `autoupdate: false`. As in 1.x, the updater +/// itself only reads the global config files, so `OPENCODE_DISABLE_AUTOUPDATE` +/// remains the effective suppression and this key is kept for parity. +pub(super) fn disable_update(config: &str) -> Result { + let mut root: Value = serde_json::from_str(config) + .map_err(|error| GhError::Serde(format!("parsing managed OpenCode config: {error}")))?; + let object = root + .as_object_mut() + .ok_or_else(|| GhError::config("managed OpenCode config must be a JSON object"))?; + object.insert("update".into(), json!("disable")); + String::from_utf8(json_pretty(&root)?) + .map_err(|error| GhError::Serde(format!("encoding managed OpenCode config: {error}"))) +} + +fn paths(dirs: &[PathBuf]) -> Vec { + dirs.iter().map(|dir| dir.display().to_string()).collect() +} + +fn plugin_dir_name(module: &Path) -> Result { + let stem = module + .file_stem() + .and_then(|stem| stem.to_str()) + .filter(|stem| !stem.is_empty()) + .ok_or_else(|| GhError::config("OpenCode plugin module has no usable file name"))?; + if stem == SESSION_PLUGIN_DIR { + return Err(GhError::config(format!( + "OpenCode plugin module name `{stem}` is reserved by Blue" + ))); + } + Ok(stem.to_owned()) +} + +/// The managed permission posture: allow everything except `question`, and +/// gate mutating tools behind `ask` when auto-approve is disabled. Global rules +/// are appended after each built-in agent's own, so they also override the +/// `build` agent's `question` allow. The v1 `fetch`, `todowrite`, and `todoread` +/// keys map to `webfetch` and `todowrite`; `list` and `mcp` have no v2 action. +fn permission_rules(policy: &HarnessPolicy) -> Value { + let action = if policy.managed_config.auto_approve == Some(false) { + "ask" + } else { + "allow" + }; + Value::Array( + [ + ("read", "allow"), + ("glob", "allow"), + ("grep", "allow"), + ("edit", action), + ("shell", action), + ("external_directory", action), + ("webfetch", "allow"), + ("subagent", "allow"), + ("skill", "allow"), + ("todowrite", "allow"), + ("question", "deny"), + ] + .into_iter() + .map(|(name, effect)| json!({ "action": name, "resource": "*", "effect": effect })) + .collect(), + ) +} + +/// Native servers win by name in either the v1 (`mcp.`) or v2 +/// (`mcp.servers.`) shape; the v2 normalizer accepts both side by side. +fn mcp_block(native: &Map, policy: &HarnessPolicy) -> Value { + let local = native.get("mcp").and_then(Value::as_object); + let native_v2 = local + .and_then(|mcp| mcp.get("servers")) + .and_then(Value::as_object); + let mut servers = Map::new(); + for s in &policy.mcp { + if s.disabled + || local.is_some_and(|mcp| mcp.contains_key(&s.name)) + || native_v2.is_some_and(|mcp| mcp.contains_key(&s.name)) + { + continue; + } + let mut entry = Map::new(); + if let Some(cmd) = &s.command { + let mut command = vec![cmd.clone()]; + command.extend(s.args.iter().cloned()); + entry.insert("type".into(), json!("local")); + entry.insert("command".into(), json!(command)); + if !s.env.is_empty() { + entry.insert("environment".into(), json!(s.env)); + } + } else if let Some(url) = &s.url { + entry.insert("type".into(), json!("remote")); + entry.insert("url".into(), json!(url)); + } + entry.insert("disabled".into(), json!(false)); + servers.insert(s.name.clone(), Value::Object(entry)); + } + json!({ "servers": servers }) +} + +/// Fields only the v2 agent schema understands. OpenCode reads a definition +/// that uses any of them as v2; otherwise unknown fields mark it as v1. +const NATIVE_AGENT_KEYS: &[&str] = &["permissions", "request", "system", "disabled"]; +/// Keys of the v2 agent schema (plus the v1 `variant`, which v2 joins into `model`). +const AGENT_KEYS: &[&str] = &[ + "description", + "mode", + "model", + "variant", + "system", + "permissions", + "request", + "disabled", + "hidden", + "color", + "steps", +]; + +/// v2 has no configuration key for an extra agent directory and only loads +/// agent files from its own config roots, which Blue does not own. Package +/// agents are therefore inlined with the same v1/v2 split OpenCode applies to +/// agent files: native definitions go under `agents` with the body as +/// `system`, and legacy ones under the v1 `agent` map with the body as +/// `prompt`, which OpenCode migrates in memory. +fn agent_definitions( + plan: &crate::adapters::ReconcilePlan, + root: &Path, +) -> Result, GhError> { + let mut sources = plan + .writes + .iter() + .filter(|write| { + write.path.starts_with(root) + && write.path.extension().is_some_and(|extension| extension == "md") + }) + .collect::>(); + sources.sort_by(|left, right| left.path.cmp(&right.path)); + let mut native = Map::new(); + let mut legacy = Map::new(); + for source in sources { + let name = source + .path + .strip_prefix(root) + .expect("filtered by prefix") + .with_extension("") + .to_string_lossy() + .replace('\\', "/"); + let text = String::from_utf8(source.body.clone()).map_err(|_| { + GhError::config(format!("agent {} is not UTF-8", source.path.display())) + })?; + let (mut data, body) = frontmatter(&text) + .map_err(|error| GhError::config(format!("agent {name}: {error}")))?; + let is_legacy = data.keys().any(|key| !AGENT_KEYS.contains(&key.as_str())) + && !data + .keys() + .any(|key| NATIVE_AGENT_KEYS.contains(&key.as_str())); + if is_legacy { + // The ID comes from the path, as for files; a v1 JSON `name` only + // produces an "unsupported field" warning in 2.x. + data.remove("name"); + data.insert("prompt".into(), json!(body)); + legacy.insert(name, Value::Object(data)); + continue; + } + if let (Some(Value::String(model)), Some(Value::String(variant))) = + (data.get("model"), data.get("variant")) + { + if !model.contains('#') && !variant.contains('#') { + let joined = format!("{model}#{variant}"); + data.insert("model".into(), json!(joined)); + } + } + data.remove("variant"); + data.insert("system".into(), json!(body)); + native.insert(name, Value::Object(data)); + } + let mut definitions = Map::new(); + if !native.is_empty() { + definitions.insert("agents".into(), Value::Object(native)); + } + if !legacy.is_empty() { + definitions.insert("agent".into(), Value::Object(legacy)); + } + Ok(definitions) +} + +/// Split optional `---` YAML frontmatter from a Markdown body. +fn frontmatter(text: &str) -> Result<(Map, String), String> { + let text = text.strip_prefix('\u{feff}').unwrap_or(text); + let Some(rest) = text + .strip_prefix("---\n") + .or_else(|| text.strip_prefix("---\r\n")) + else { + return Ok((Map::new(), text.trim().to_owned())); + }; + let mut offset = 0; + for line in rest.split_inclusive('\n') { + if line.trim_end() == "---" { + let data = if rest[..offset].trim().is_empty() { + Value::Object(Map::new()) + } else { + serde_yaml::from_str::(&rest[..offset]) + .map_err(|error| format!("invalid frontmatter: {error}"))? + }; + let Value::Object(data) = data else { + return Err("frontmatter must be a mapping".into()); + }; + return Ok((data, rest[offset + line.len()..].trim().to_owned())); + } + offset += line.len(); + } + Err("unterminated frontmatter".into()) +} + +#[cfg(test)] +mod tests { + use super::*; + + fn home(label: &str) -> PathBuf { + let home = std::env::temp_dir().join(format!( + "gh-opencode-v2-{label}-{}-{}", + std::process::id(), + std::time::SystemTime::now() + .duration_since(std::time::UNIX_EPOCH) + .unwrap() + .as_nanos() + )); + std::fs::create_dir_all(&home).unwrap(); + home + } + + fn render( + home: &Path, + policy: Value, + wiring: Option<&GatewayWiring>, + components: &PackageComponents, + ) -> (HarnessWrite, Value, crate::adapters::ReconcilePlan) { + let policy: HarnessPolicy = serde_json::from_value(policy).unwrap(); + let mut plan = crate::adapters::ReconcilePlan::default(); + let report = write(&mut plan, home, &policy, wiring, None, components).unwrap(); + let config = serde_json::from_str(&report.env["OPENCODE_CONFIG_CONTENT"]).unwrap(); + (report, config, plan) + } + + #[test] + fn governance_renders_native_v2_shape_without_replacing_the_config_dir() { + let home = home("governance"); + let (report, config, _) = render( + &home, + json!({ "managed_config": { "model": "org/model" } }), + None, + &PackageComponents::default(), + ); + assert_eq!(report.launch_args, [STANDALONE]); + assert!(!report.env.contains_key("OPENCODE_CONFIG_DIR")); + assert_eq!(config["model"], "org/model"); + assert!(config.get("permission").is_none()); + assert!(config.get("providers").is_none()); + assert!(config.get("policies").is_none()); + assert!(config.get("plugins").is_none()); + let rules = config["permissions"].as_array().unwrap(); + assert_eq!( + rules.last().unwrap(), + &json!({ "action": "question", "resource": "*", "effect": "deny" }) + ); + assert!(rules + .iter() + .all(|rule| !["bash", "task", "write", "list", "mcp", "todoread"] + .contains(&rule["action"].as_str().unwrap()))); + let _ = std::fs::remove_dir_all(home); + } + + #[test] + fn gateway_uses_a_native_provider_and_a_provider_policy() { + let home = home("gateway"); + let wiring = GatewayWiring { + base_url: "https://gateway.example/v1".into(), + token: "jwt".into(), + wire_api: None, + auth: gh_gateway::AuthPlacement::InFile, + }; + let (report, config, _) = render( + &home, + json!({ "managed_config": { "model": "org/model" } }), + Some(&wiring), + &PackageComponents::default(), + ); + let governed = &config["providers"][GOVERNED_PROVIDER]; + assert_eq!(governed["package"], "@opencode/ai/providers/openai-compatible"); + assert_eq!(governed["settings"]["baseURL"], "https://gateway.example/v1"); + assert_eq!(governed["settings"]["apiKey"], "{env:BLUE_OPENCODE_GATEWAY_TOKEN}"); + assert_eq!(governed["models"]["org/model"]["name"], "org/model"); + assert_eq!(config["model"], "governed/org/model"); + assert_eq!( + config["policies"], + json!([ + { "action": "provider.use", "resource": "*", "effect": "deny" }, + { "action": "provider.use", "resource": "governed", "effect": "allow" } + ]) + ); + assert!(config.get("enabled_providers").is_none()); + assert_eq!(report.env[GATEWAY_TOKEN_ENV], "jwt"); + let _ = std::fs::remove_dir_all(home); + } + + #[test] + fn native_servers_win_in_either_config_shape() { + let home = home("mcp"); + let native = home.join(".config/opencode/opencode.json"); + std::fs::create_dir_all(native.parent().unwrap()).unwrap(); + std::fs::write( + &native, + json!({ "mcp": { "v1-native": { "type": "local" }, "servers": { "v2-native": {} } } }) + .to_string(), + ) + .unwrap(); + let (_, config, _) = render( + &home, + json!({ "mcp": [ + { "name": "v1-native", "command": "x" }, + { "name": "v2-native", "command": "x" }, + { "name": "off", "command": "x", "disabled": true }, + { "name": "local", "command": "run", "args": ["--stdio"], "env": { "K": "V" } }, + { "name": "remote", "url": "https://mcp.example" } + ] }), + None, + &PackageComponents::default(), + ); + assert_eq!( + config["mcp"], + json!({ "servers": { + "local": { "type": "local", "command": ["run", "--stdio"], "environment": { "K": "V" }, "disabled": false }, + "remote": { "type": "remote", "url": "https://mcp.example", "disabled": false } + } }) + ); + let _ = std::fs::remove_dir_all(home); + } + + #[test] + fn package_components_are_referenced_by_absolute_directory() { + let home = home("components"); + let source = home.join("package"); + std::fs::create_dir_all(source.join("skills/review")).unwrap(); + std::fs::create_dir_all(source.join("agents/team")).unwrap(); + std::fs::write(source.join("skills/review/SKILL.md"), "# Review\n").unwrap(); + std::fs::write(source.join("tool.js"), "export default { id: 'tool', setup() {} }\n") + .unwrap(); + std::fs::write( + source.join("agents/legacy.md"), + "---\nname: legacy\ndescription: Old\ntools:\n write: false\n---\nBe careful.\n", + ) + .unwrap(); + std::fs::write( + source.join("agents/team/native.md"), + "---\ndescription: New\nmodel: org/model\nvariant: high\npermissions: []\n---\nBe brief.\n", + ) + .unwrap(); + std::fs::write(source.join("agents/plain.md"), "Just a prompt.\n").unwrap(); + let components = PackageComponents { + plugin_modules: vec![source.join("tool.js")], + skills_dirs: vec![source.join("skills/review")], + agents_dirs: vec![source.join("agents")], + ..Default::default() + }; + let (_, config, plan) = render(&home, json!({}), None, &components); + let runtime = crate::managed_runtime_dir(&home).join("opencode"); + assert_eq!( + config["plugins"], + json!([runtime.join("plugins").join("tool").display().to_string()]) + ); + assert!(plan + .writes + .iter() + .any(|write| write.path == runtime.join("plugins").join("tool").join("index.js"))); + assert_eq!( + config["skills"], + json!([runtime.join("skills").display().to_string()]) + ); + assert_eq!( + config["agent"]["legacy"], + json!({ "description": "Old", "tools": { "write": false }, "prompt": "Be careful." }) + ); + assert_eq!( + config["agents"]["team/native"], + json!({ "description": "New", "model": "org/model#high", "permissions": [], "system": "Be brief." }) + ); + assert_eq!(config["agents"]["plain"], json!({ "system": "Just a prompt." })); + let _ = std::fs::remove_dir_all(home); + } + + #[test] + fn the_session_plugin_name_is_reserved_and_frontmatter_must_close() { + assert!(plugin_dir_name(Path::new("/p/blue-session-upload.js")).is_err()); + assert_eq!(plugin_dir_name(Path::new("/p/tool.ts")).unwrap(), "tool"); + assert!(frontmatter("---\ndescription: x\n").is_err()); + assert!(frontmatter("---\n- list\n---\nbody").is_err()); + assert_eq!(frontmatter("---\n---\nbody").unwrap().1, "body"); + } +} diff --git a/crates/gh-config/src/compat.rs b/crates/gh-config/src/compat.rs index bae1d42..a974db0 100644 --- a/crates/gh-config/src/compat.rs +++ b/crates/gh-config/src/compat.rs @@ -113,9 +113,22 @@ impl CompatibilityFailure { } } + /// The installed version the failure was raised for, when it was readable. + pub fn installed(&self) -> Option<&Version> { + match self { + Self::PolicyMismatch { installed, .. } + | Self::UnsupportedGeneration { installed, .. } + | Self::UnverifiedGeneration { installed, .. } => Some(installed), + Self::UnparseableVersion { .. } | Self::InvalidPolicy { .. } => None, + } + } + pub fn with_install_hint(&self, policy: &HarnessPolicy) -> String { if self.is_installable() { - format!("{self}; {}", install_hint(self.harness(), policy)) + format!( + "{self}; {}", + install_hint(self.harness(), policy, self.installed()) + ) } else { self.to_string() } @@ -155,6 +168,51 @@ fn select_profile(harness: Harness, version: &Version) -> Option Result { + install_for(harness, harness_policy, None) +} + +/// The install plan for repairing an existing installation. Prefer the +/// generation that already owns `installed` so a repair stays inside its +/// release line (and npm package); fall back to the newest generation the +/// policy admits, which callers must compare against [`generation_package`] +/// before replacing anything. +pub fn supported_repair_install( + harness: Harness, + harness_policy: &HarnessPolicy, + installed: Option<&Version>, +) -> Result { + install_for(harness, harness_policy, installed) +} + +/// The npm package that installs the generation owning `version`. +pub fn generation_package(harness: Harness, version: &Version) -> Option { + let registration = adapters::definition(harness).select(version)?; + let invocation = registration + .implementation + .install_plan(®istration.interval, None); + install_package(&invocation).map(str::to_owned) +} + +/// The package named by an `npm install -g @` plan. +pub fn install_package(invocation: &InstallInvocation) -> Option<&str> { + match invocation.args.as_slice() { + [install, global, selector] + if invocation.program == "npm" && install == "install" && global == "-g" => + { + selector + .rsplit_once('@') + .map(|(package, _)| package) + .filter(|package| !package.is_empty()) + } + _ => None, + } +} + +fn install_for( + harness: Harness, + harness_policy: &HarnessPolicy, + installed: Option<&Version>, ) -> Result { if harness_policy.allow_unverified_versions && harness_policy @@ -176,17 +234,18 @@ pub fn supported_install( .map_err(|error| { GhError::config(format!("invalid {harness} install requirement: {error}")) })?; - let registration = adapters::definition(harness) - .implementations - .iter() - .rev() - .find(|registration| { - interval_intersects( - ®istration.interval, - policy.as_ref(), - harness_policy.allow_unverified_versions, - ) - }) + let admitted = |registration: &&adapters::ImplementationRegistration| { + interval_intersects( + ®istration.interval, + policy.as_ref(), + harness_policy.allow_unverified_versions, + ) + }; + let definition = adapters::definition(harness); + let registration = installed + .and_then(|version| definition.select(version)) + .filter(admitted) + .or_else(|| definition.implementations.iter().rev().find(admitted)) .ok_or_else(|| { GhError::config(format!( "no supported {harness} implementation intersects the install requirement" @@ -286,9 +345,18 @@ fn boundary_candidates(endpoint: &Version) -> Vec { candidates } -fn install_hint(harness: Harness, policy: &HarnessPolicy) -> String { - match supported_install(harness, policy) { - Ok(invocation) => format!("install a supported version with `{}`", invocation.display), +fn install_hint(harness: Harness, policy: &HarnessPolicy, installed: Option<&Version>) -> String { + match supported_repair_install(harness, policy, installed) { + Ok(invocation) => { + let current = installed.and_then(|version| generation_package(harness, version)); + match (current, install_package(&invocation)) { + (Some(current), Some(target)) if current != target => format!( + "uninstall `{current}`, then install a supported version with `{}`", + invocation.display + ), + _ => format!("install a supported version with `{}`", invocation.display), + } + } Err(error) => error.to_string(), } } diff --git a/crates/gh-config/src/contract_tests.rs b/crates/gh-config/src/contract_tests.rs index 5345cff..3c7b05e 100644 --- a/crates/gh-config/src/contract_tests.rs +++ b/crates/gh-config/src/contract_tests.rs @@ -204,7 +204,18 @@ fn assert_wrapped_update_controls( Harness::Opencode => { let config: serde_json::Value = serde_json::from_str(env.get("OPENCODE_CONFIG_CONTENT").unwrap()).unwrap(); - assert_eq!(config.get("autoupdate").is_some(), expected); + // 1.x spells the managed key `autoupdate`; 2.x uses native `update`. + let key = if config.get("permissions").is_some() { + "update" + } else { + "autoupdate" + }; + assert_eq!(config.get(key).is_some(), expected); + // 2.x launches always use a private server that inherits the env. + assert_eq!( + config.get("permissions").is_some(), + args.iter().any(|arg| arg == "--standalone") + ); // The config key alone does not reach OpenCode's updater; only the // env var does. assert_eq!(env.get("OPENCODE_DISABLE_AUTOUPDATE").is_some(), expected); @@ -556,9 +567,59 @@ fn boundaries_aliases_and_unsupported_packages_are_explicit() { (Harness::Claude, "1.0.38", "claude-v1_0_38"), (Harness::Claude, "2.0.11", "claude-v1_0_38"), (Harness::Claude, "2.0.12", "claude-v2_0_12"), + (Harness::Opencode, "1.18.25", "opencode-v0_0_0"), + (Harness::Opencode, "2.0.0", "opencode-v2_0_0"), + (Harness::Opencode, "2.2.5", "opencode-v2_0_0"), ] { assert_eq!(context(harness, version).profile.id, expected); } + // Above the 1.x ceiling but below 2.0.0 stays a 1.x generation. + assert_eq!( + adapters::definition(Harness::Opencode) + .select(&semver::Version::parse("1.19.5").unwrap()) + .unwrap() + .interval + .profile, + "opencode-v0_0_0" + ); + // Each generation installs from its own package. + for (requirement, package) in [ + (None, "@opencode/cli@"), + (Some("<2"), "opencode-ai@"), + (Some(">=1.18, <1.19"), "opencode-ai@"), + (Some(">=2"), "@opencode/cli@"), + ] { + let install = supported_install( + Harness::Opencode, + &HarnessPolicy { + version_requirement: requirement.map(str::to_owned), + ..Default::default() + }, + ) + .unwrap(); + assert!( + install.args.last().unwrap().starts_with(package), + "{requirement:?}: {}", + install.display + ); + } + // Repair stays inside the installed generation unless policy excludes it. + for (installed, requirement, package) in [ + ("1.19.5", None, "opencode-ai"), + ("2.99.0", None, "@opencode/cli"), + ("1.18.25", Some(">=2"), "@opencode/cli"), + ] { + let install = crate::compat::supported_repair_install( + Harness::Opencode, + &HarnessPolicy { + version_requirement: requirement.map(str::to_owned), + ..Default::default() + }, + Some(&semver::Version::parse(installed).unwrap()), + ) + .unwrap(); + assert_eq!(crate::compat::install_package(&install), Some(package)); + } for harness in Harness::ALL { let alias = format!("{}-v1", harness.key()); assert_eq!( @@ -973,6 +1034,7 @@ fn production_boundary_transitions_preserve_v4_and_roll_back_activation_failure( Harness::Claude, vec!["1.0.37", "1.0.38", "2.0.12", "2.0.11", "1.0.37"], ), + (Harness::Opencode, vec!["1.18.25", "2.0.0", "1.18.25"]), ] { let home = home("transition"); let policy = HarnessPolicy::default(); @@ -1020,6 +1082,14 @@ fn production_boundary_transitions_preserve_v4_and_roll_back_activation_failure( .collect::>() .join("\n"); assert_eq!(bodies.contains("session-upload"), supported); + if harness == Harness::Opencode { + // Each generation's plugin layout replaces the other's, so 2.x + // never discovers a v1 module and 1.x never sees a v2 directory. + let plugins = home.join(".config/blue/runtime/opencode/plugins"); + let v2 = context.profile.id == "opencode-v2_0_0"; + assert_eq!(plugins.join("blue-session-upload.js").exists(), !v2); + assert_eq!(plugins.join("blue-session-upload/index.js").exists(), v2); + } } std::fs::remove_dir_all(home).unwrap(); } diff --git a/crates/gh-config/src/lib.rs b/crates/gh-config/src/lib.rs index 5c8358d..668d73f 100644 --- a/crates/gh-config/src/lib.rs +++ b/crates/gh-config/src/lib.rs @@ -40,7 +40,8 @@ pub(crate) fn managed_runtime_dir(home: &std::path::Path) -> PathBuf { } pub use compat::{ - resolve as resolve_compatibility, supported_install, validate_package_adapter_for_policy, + generation_package, install_package, resolve as resolve_compatibility, supported_install, + supported_repair_install, validate_package_adapter_for_policy, validate_package_adapters_for_policy, CompatibilityFailure, HarnessContext, ProfileStatus, }; pub use packages::PackageFetcher; diff --git a/crates/gh-config/tests/fixtures/future-boundaries.json b/crates/gh-config/tests/fixtures/future-boundaries.json index 7a8f725..c2aa5bf 100644 --- a/crates/gh-config/tests/fixtures/future-boundaries.json +++ b/crates/gh-config/tests/fixtures/future-boundaries.json @@ -9,10 +9,10 @@ }, { "harness": "opencode", - "candidate": "configuration, skills, and plugin directory transitions", + "candidate": "2.x package, plugin API, background service, and configuration transitions", "paths_to_test": [".config/opencode/opencode.json", ".config/opencode/skills", ".config/opencode/plugins"], "source": "https://github.com/anomalyco/opencode/releases", - "production_boundary": null, - "required_evidence": "Pin schema and discovery fixtures before defining another production interval. Release chronology alone does not establish a Blue output boundary." + "production_boundary": "2.0.0", + "required_evidence": "Established by opencode-v2_0_0: 2.x ships as @opencode/cli, loads only the v2 plugin API, attaches to a shared background service unless --standalone, replaces (rather than layers) OPENCODE_CONFIG_DIR, and imports through `session import`. Later 2.x releases need fresh schema and discovery fixtures before another interval." } ] diff --git a/crates/gh-config/tests/golden/opencode-v2_0_0-gateway-nomodel.json b/crates/gh-config/tests/golden/opencode-v2_0_0-gateway-nomodel.json new file mode 100644 index 0000000..b2dc3e4 --- /dev/null +++ b/crates/gh-config/tests/golden/opencode-v2_0_0-gateway-nomodel.json @@ -0,0 +1,63 @@ +{ + "args": [ + "--standalone" + ], + "env": { + "BLUE_OPENCODE_GATEWAY_TOKEN": "fixture-token", + "HARNESS_PACKAGE_COMPONENTS_SETTINGS": "{\"color\":\"blue\"}", + "OPENCODE_CONFIG_CONTENT": "{\n \"$schema\": \"https://opencode.ai/config.json\",\n \"agent\": {\n \"reviewer\": {\n \"description\": \"Review changes\",\n \"prompt\": \"Review carefully.\"\n }\n },\n \"mcp\": {\n \"servers\": {\n \"fixture\": {\n \"command\": [\n \"fixture-mcp\",\n \"--stdio\"\n ],\n \"disabled\": false,\n \"type\": \"local\"\n }\n }\n },\n \"model\": \"governed/openai/gpt-5.6-sol\",\n \"permissions\": [\n {\n \"action\": \"read\",\n \"effect\": \"allow\",\n \"resource\": \"*\"\n },\n {\n \"action\": \"glob\",\n \"effect\": \"allow\",\n \"resource\": \"*\"\n },\n {\n \"action\": \"grep\",\n \"effect\": \"allow\",\n \"resource\": \"*\"\n },\n {\n \"action\": \"edit\",\n \"effect\": \"ask\",\n \"resource\": \"*\"\n },\n {\n \"action\": \"shell\",\n \"effect\": \"ask\",\n \"resource\": \"*\"\n },\n {\n \"action\": \"external_directory\",\n \"effect\": \"ask\",\n \"resource\": \"*\"\n },\n {\n \"action\": \"webfetch\",\n \"effect\": \"allow\",\n \"resource\": \"*\"\n },\n {\n \"action\": \"subagent\",\n \"effect\": \"allow\",\n \"resource\": \"*\"\n },\n {\n \"action\": \"skill\",\n \"effect\": \"allow\",\n \"resource\": \"*\"\n },\n {\n \"action\": \"todowrite\",\n \"effect\": \"allow\",\n \"resource\": \"*\"\n },\n {\n \"action\": \"question\",\n \"effect\": \"deny\",\n \"resource\": \"*\"\n }\n ],\n \"plugins\": [\n \"$HOME/.config/blue/runtime/opencode/plugins/blue-session-upload\",\n \"$HOME/.config/blue/runtime/opencode/plugins/fixture\"\n ],\n \"policies\": [\n {\n \"action\": \"provider.use\",\n \"effect\": \"deny\",\n \"resource\": \"*\"\n },\n {\n \"action\": \"provider.use\",\n \"effect\": \"allow\",\n \"resource\": \"governed\"\n }\n ],\n \"providers\": {\n \"governed\": {\n \"models\": {\n \"openai/gpt-5.6-sol\": {\n \"name\": \"openai/gpt-5.6-sol\"\n }\n },\n \"name\": \"Governed (Blue)\",\n \"package\": \"@opencode/ai/providers/openai-compatible\",\n \"settings\": {\n \"apiKey\": \"{env:BLUE_OPENCODE_GATEWAY_TOKEN}\",\n \"baseURL\": \"https://gateway.example\"\n }\n }\n },\n \"skills\": [\n \"$HOME/.config/blue/runtime/opencode/skills\"\n ],\n \"update\": \"disable\"\n}\n", + "OPENCODE_DISABLE_AUTOUPDATE": "1", + "PATH": "$HOME/package/bin:$PATH" + }, + "files": [ + "$HOME/.config/blue/runtime/opencode/agents", + "$HOME/.config/blue/runtime/opencode/opencode.json", + "$HOME/.config/blue/runtime/opencode/plugins/blue-session-upload/index.js", + "$HOME/.config/blue/runtime/opencode/plugins/fixture", + "$HOME/.config/blue/runtime/opencode/skills" + ], + "ownership": [ + "$HOME/.config/blue/runtime/opencode/agents", + "$HOME/.config/blue/runtime/opencode/compatibility-state.json", + "$HOME/.config/blue/runtime/opencode/hooks", + "$HOME/.config/blue/runtime/opencode/opencode.json", + "$HOME/.config/blue/runtime/opencode/plugins", + "$HOME/.config/blue/runtime/opencode/skills" + ], + "removals": [ + "$HOME/.config/blue/runtime/opencode/agents", + "$HOME/.config/blue/runtime/opencode/hooks", + "$HOME/.config/blue/runtime/opencode/plugins", + "$HOME/.config/blue/runtime/opencode/skills" + ], + "warnings": [ + "`harnesses.opencode.managed_config.model` is unset; defaulting to `openai/gpt-5.6-sol`. Set it in your governance config to pin the model your gateway serves." + ], + "writes": [ + { + "body": "---\nname: reviewer\ndescription: Review changes\n---\nReview carefully.\n", + "mode": 384, + "path": "$HOME/.config/blue/runtime/opencode/agents/reviewer.md" + }, + { + "body": "{\n \"$schema\": \"https://opencode.ai/config.json\",\n \"agent\": {\n \"reviewer\": {\n \"description\": \"Review changes\",\n \"prompt\": \"Review carefully.\"\n }\n },\n \"mcp\": {\n \"servers\": {\n \"fixture\": {\n \"command\": [\n \"fixture-mcp\",\n \"--stdio\"\n ],\n \"disabled\": false,\n \"type\": \"local\"\n }\n }\n },\n \"model\": \"governed/openai/gpt-5.6-sol\",\n \"permissions\": [\n {\n \"action\": \"read\",\n \"effect\": \"allow\",\n \"resource\": \"*\"\n },\n {\n \"action\": \"glob\",\n \"effect\": \"allow\",\n \"resource\": \"*\"\n },\n {\n \"action\": \"grep\",\n \"effect\": \"allow\",\n \"resource\": \"*\"\n },\n {\n \"action\": \"edit\",\n \"effect\": \"ask\",\n \"resource\": \"*\"\n },\n {\n \"action\": \"shell\",\n \"effect\": \"ask\",\n \"resource\": \"*\"\n },\n {\n \"action\": \"external_directory\",\n \"effect\": \"ask\",\n \"resource\": \"*\"\n },\n {\n \"action\": \"webfetch\",\n \"effect\": \"allow\",\n \"resource\": \"*\"\n },\n {\n \"action\": \"subagent\",\n \"effect\": \"allow\",\n \"resource\": \"*\"\n },\n {\n \"action\": \"skill\",\n \"effect\": \"allow\",\n \"resource\": \"*\"\n },\n {\n \"action\": \"todowrite\",\n \"effect\": \"allow\",\n \"resource\": \"*\"\n },\n {\n \"action\": \"question\",\n \"effect\": \"deny\",\n \"resource\": \"*\"\n }\n ],\n \"plugins\": [\n \"$HOME/.config/blue/runtime/opencode/plugins/blue-session-upload\",\n \"$HOME/.config/blue/runtime/opencode/plugins/fixture\"\n ],\n \"policies\": [\n {\n \"action\": \"provider.use\",\n \"effect\": \"deny\",\n \"resource\": \"*\"\n },\n {\n \"action\": \"provider.use\",\n \"effect\": \"allow\",\n \"resource\": \"governed\"\n }\n ],\n \"providers\": {\n \"governed\": {\n \"models\": {\n \"openai/gpt-5.6-sol\": {\n \"name\": \"openai/gpt-5.6-sol\"\n }\n },\n \"name\": \"Governed (Blue)\",\n \"package\": \"@opencode/ai/providers/openai-compatible\",\n \"settings\": {\n \"apiKey\": \"{env:BLUE_OPENCODE_GATEWAY_TOKEN}\",\n \"baseURL\": \"https://gateway.example\"\n }\n }\n },\n \"skills\": [\n \"$HOME/.config/blue/runtime/opencode/skills\"\n ]\n}\n", + "mode": 384, + "path": "$HOME/.config/blue/runtime/opencode/opencode.json" + }, + { + "body": "// Managed by Blue. Changes will be overwritten.\nimport { spawn } from \"node:child_process\"\nimport { mkdir, rm, writeFile } from \"node:fs/promises\"\nimport { dirname } from \"node:path\"\n\nconst BLUE_SESSION_ID = process.env.BLUE_SESSION_ID\nconst BLUE_DATA_DIR = process.env.BLUE_DATA_DIR\nconst EXPORTING = \"BLUE_OPENCODE_SESSION_EXPORT\"\nconst blueTranscriptPath = (sessionID) =>\n BLUE_SESSION_ID && BLUE_DATA_DIR\n ? `${BLUE_DATA_DIR}/sessions/${BLUE_SESSION_ID}/opencode-transcript-${sessionID}.json`\n : `${process.env.TMPDIR || \"/tmp\"}/blue-opencode-${sessionID}.json`\n\nconst run = (command, args, input, env = process.env) =>\n new Promise((resolve, reject) => {\n const child = spawn(command, args, { env, stdio: [\"pipe\", \"pipe\", \"inherit\"] })\n let stdout = \"\"\n child.stdout.setEncoding(\"utf8\")\n child.stdout.on(\"data\", (chunk) => { stdout += chunk })\n child.on(\"error\", reject)\n child.on(\"close\", (code) =>\n code === 0 ? resolve(stdout) : reject(new Error(`${command} ${args[0]} exited with ${code}`)))\n child.stdin.end(input ?? \"\")\n })\n\nexport default {\n id: \"blue.session-upload\",\n setup(ctx) {\n if (process.env[EXPORTING]) return\n const directory = ctx.location.directory\n const controller = new AbortController()\n const uploaded = new Set()\n const started = new Set()\n const topLevel = async (sessionID) => {\n const response = await ctx.session.get({ sessionID })\n const session = response?.data ?? response\n const owner = session?.location?.directory\n return !session?.parentID && (!owner || owner === directory)\n }\n const ensureStarted = async (sessionID) => {\n if (!sessionID || started.has(sessionID)) return\n started.add(sessionID)\n try {\n if (!(await topLevel(sessionID))) return\n await run(\"$BLUE\", [\"session-start\", \"opencode\", \"--profile\", \"opencode-v2_0_0\"], JSON.stringify({\n session_id: sessionID, source: \"startup\", cwd: directory,\n hook_event_name: \"SessionStart\", transcript_path: blueTranscriptPath(sessionID),\n }))\n } catch (error) {\n started.delete(sessionID)\n console.error(\"Blue session start failed\", error)\n }\n }\n const exportTranscript = async (sessionID) => {\n // Exporting reads the shared database only; the managed overlay would\n // just start its MCP servers and plugins again.\n const { OPENCODE_CONFIG_CONTENT: _, ...env } = process.env\n const exported = await run(process.execPath, [\"--standalone\", \"session\", \"export\", sessionID], \"\",\n { ...env, [EXPORTING]: \"1\" })\n const transcriptPath = blueTranscriptPath(sessionID)\n await mkdir(dirname(transcriptPath), { recursive: true })\n await writeFile(transcriptPath, exported)\n return transcriptPath\n }\n const upload = async (sessionID) => {\n if (!sessionID) return\n if (uploaded.has(sessionID)) {\n // Keep the stable transcript current for `blue run`'s post-exit upload.\n if (BLUE_SESSION_ID && (await topLevel(sessionID).catch(() => false))) {\n await exportTranscript(sessionID).catch((error) => console.error(\"Blue session export failed\", error))\n }\n return\n }\n uploaded.add(sessionID)\n let transcriptPath\n try {\n if (!(await topLevel(sessionID))) return\n transcriptPath = await exportTranscript(sessionID)\n await run(\"$BLUE\", [\"session-upload\", \"opencode\", \"--profile\", \"opencode-v2_0_0\"], JSON.stringify({\n session_id: sessionID, transcript_path: transcriptPath, cwd: directory,\n hook_event_name: \"SessionEnd\", reason: \"idle\",\n }))\n } catch (error) {\n uploaded.delete(sessionID)\n console.error(\"Blue session upload failed\", error)\n } finally {\n if (transcriptPath && !BLUE_SESSION_ID) await rm(transcriptPath, { force: true }).catch(() => {})\n }\n }\n const finished = new Set([\n \"session.execution.succeeded\", \"session.execution.failed\", \"session.execution.interrupted\",\n ])\n void (async () => {\n for await (const event of ctx.event.subscribe({ signal: controller.signal })) {\n const sessionID = event.data?.sessionID\n await ensureStarted(sessionID)\n const idle = finished.has(event.type) ||\n (event.type === \"session.status\" && event.data?.status?.type === \"idle\")\n if (idle) await upload(sessionID)\n }\n })().catch((error) => {\n if (!controller.signal.aborted) console.error(\"Blue session events failed\", error)\n })\n return () => controller.abort()\n },\n}\n", + "mode": 384, + "path": "$HOME/.config/blue/runtime/opencode/plugins/blue-session-upload/index.js" + }, + { + "body": "export const Fixture = async () => ({})\n", + "mode": 384, + "path": "$HOME/.config/blue/runtime/opencode/plugins/fixture/index.js" + }, + { + "body": "# Example\n\nA managed skill.\n", + "mode": 384, + "path": "$HOME/.config/blue/runtime/opencode/skills/example/SKILL.md" + } + ] +} diff --git a/crates/gh-config/tests/golden/opencode-v2_0_0-gateway.json b/crates/gh-config/tests/golden/opencode-v2_0_0-gateway.json new file mode 100644 index 0000000..1ec29b8 --- /dev/null +++ b/crates/gh-config/tests/golden/opencode-v2_0_0-gateway.json @@ -0,0 +1,61 @@ +{ + "args": [ + "--standalone" + ], + "env": { + "BLUE_OPENCODE_GATEWAY_TOKEN": "fixture-token", + "HARNESS_PACKAGE_COMPONENTS_SETTINGS": "{\"color\":\"blue\"}", + "OPENCODE_CONFIG_CONTENT": "{\n \"$schema\": \"https://opencode.ai/config.json\",\n \"agent\": {\n \"reviewer\": {\n \"description\": \"Review changes\",\n \"prompt\": \"Review carefully.\"\n }\n },\n \"mcp\": {\n \"servers\": {\n \"fixture\": {\n \"command\": [\n \"fixture-mcp\",\n \"--stdio\"\n ],\n \"disabled\": false,\n \"type\": \"local\"\n }\n }\n },\n \"model\": \"governed/fixture-model\",\n \"permissions\": [\n {\n \"action\": \"read\",\n \"effect\": \"allow\",\n \"resource\": \"*\"\n },\n {\n \"action\": \"glob\",\n \"effect\": \"allow\",\n \"resource\": \"*\"\n },\n {\n \"action\": \"grep\",\n \"effect\": \"allow\",\n \"resource\": \"*\"\n },\n {\n \"action\": \"edit\",\n \"effect\": \"ask\",\n \"resource\": \"*\"\n },\n {\n \"action\": \"shell\",\n \"effect\": \"ask\",\n \"resource\": \"*\"\n },\n {\n \"action\": \"external_directory\",\n \"effect\": \"ask\",\n \"resource\": \"*\"\n },\n {\n \"action\": \"webfetch\",\n \"effect\": \"allow\",\n \"resource\": \"*\"\n },\n {\n \"action\": \"subagent\",\n \"effect\": \"allow\",\n \"resource\": \"*\"\n },\n {\n \"action\": \"skill\",\n \"effect\": \"allow\",\n \"resource\": \"*\"\n },\n {\n \"action\": \"todowrite\",\n \"effect\": \"allow\",\n \"resource\": \"*\"\n },\n {\n \"action\": \"question\",\n \"effect\": \"deny\",\n \"resource\": \"*\"\n }\n ],\n \"plugins\": [\n \"$HOME/.config/blue/runtime/opencode/plugins/blue-session-upload\",\n \"$HOME/.config/blue/runtime/opencode/plugins/fixture\"\n ],\n \"policies\": [\n {\n \"action\": \"provider.use\",\n \"effect\": \"deny\",\n \"resource\": \"*\"\n },\n {\n \"action\": \"provider.use\",\n \"effect\": \"allow\",\n \"resource\": \"governed\"\n }\n ],\n \"providers\": {\n \"governed\": {\n \"models\": {\n \"fixture-model\": {\n \"name\": \"fixture-model\"\n },\n \"openai/gpt-5.6-sol\": {\n \"name\": \"openai/gpt-5.6-sol\"\n }\n },\n \"name\": \"Governed (Blue)\",\n \"package\": \"@opencode/ai/providers/openai-compatible\",\n \"settings\": {\n \"apiKey\": \"{env:BLUE_OPENCODE_GATEWAY_TOKEN}\",\n \"baseURL\": \"https://gateway.example\"\n }\n }\n },\n \"skills\": [\n \"$HOME/.config/blue/runtime/opencode/skills\"\n ],\n \"update\": \"disable\"\n}\n", + "OPENCODE_DISABLE_AUTOUPDATE": "1", + "PATH": "$HOME/package/bin:$PATH" + }, + "files": [ + "$HOME/.config/blue/runtime/opencode/agents", + "$HOME/.config/blue/runtime/opencode/opencode.json", + "$HOME/.config/blue/runtime/opencode/plugins/blue-session-upload/index.js", + "$HOME/.config/blue/runtime/opencode/plugins/fixture", + "$HOME/.config/blue/runtime/opencode/skills" + ], + "ownership": [ + "$HOME/.config/blue/runtime/opencode/agents", + "$HOME/.config/blue/runtime/opencode/compatibility-state.json", + "$HOME/.config/blue/runtime/opencode/hooks", + "$HOME/.config/blue/runtime/opencode/opencode.json", + "$HOME/.config/blue/runtime/opencode/plugins", + "$HOME/.config/blue/runtime/opencode/skills" + ], + "removals": [ + "$HOME/.config/blue/runtime/opencode/agents", + "$HOME/.config/blue/runtime/opencode/hooks", + "$HOME/.config/blue/runtime/opencode/plugins", + "$HOME/.config/blue/runtime/opencode/skills" + ], + "warnings": [], + "writes": [ + { + "body": "---\nname: reviewer\ndescription: Review changes\n---\nReview carefully.\n", + "mode": 384, + "path": "$HOME/.config/blue/runtime/opencode/agents/reviewer.md" + }, + { + "body": "{\n \"$schema\": \"https://opencode.ai/config.json\",\n \"agent\": {\n \"reviewer\": {\n \"description\": \"Review changes\",\n \"prompt\": \"Review carefully.\"\n }\n },\n \"mcp\": {\n \"servers\": {\n \"fixture\": {\n \"command\": [\n \"fixture-mcp\",\n \"--stdio\"\n ],\n \"disabled\": false,\n \"type\": \"local\"\n }\n }\n },\n \"model\": \"governed/fixture-model\",\n \"permissions\": [\n {\n \"action\": \"read\",\n \"effect\": \"allow\",\n \"resource\": \"*\"\n },\n {\n \"action\": \"glob\",\n \"effect\": \"allow\",\n \"resource\": \"*\"\n },\n {\n \"action\": \"grep\",\n \"effect\": \"allow\",\n \"resource\": \"*\"\n },\n {\n \"action\": \"edit\",\n \"effect\": \"ask\",\n \"resource\": \"*\"\n },\n {\n \"action\": \"shell\",\n \"effect\": \"ask\",\n \"resource\": \"*\"\n },\n {\n \"action\": \"external_directory\",\n \"effect\": \"ask\",\n \"resource\": \"*\"\n },\n {\n \"action\": \"webfetch\",\n \"effect\": \"allow\",\n \"resource\": \"*\"\n },\n {\n \"action\": \"subagent\",\n \"effect\": \"allow\",\n \"resource\": \"*\"\n },\n {\n \"action\": \"skill\",\n \"effect\": \"allow\",\n \"resource\": \"*\"\n },\n {\n \"action\": \"todowrite\",\n \"effect\": \"allow\",\n \"resource\": \"*\"\n },\n {\n \"action\": \"question\",\n \"effect\": \"deny\",\n \"resource\": \"*\"\n }\n ],\n \"plugins\": [\n \"$HOME/.config/blue/runtime/opencode/plugins/blue-session-upload\",\n \"$HOME/.config/blue/runtime/opencode/plugins/fixture\"\n ],\n \"policies\": [\n {\n \"action\": \"provider.use\",\n \"effect\": \"deny\",\n \"resource\": \"*\"\n },\n {\n \"action\": \"provider.use\",\n \"effect\": \"allow\",\n \"resource\": \"governed\"\n }\n ],\n \"providers\": {\n \"governed\": {\n \"models\": {\n \"fixture-model\": {\n \"name\": \"fixture-model\"\n },\n \"openai/gpt-5.6-sol\": {\n \"name\": \"openai/gpt-5.6-sol\"\n }\n },\n \"name\": \"Governed (Blue)\",\n \"package\": \"@opencode/ai/providers/openai-compatible\",\n \"settings\": {\n \"apiKey\": \"{env:BLUE_OPENCODE_GATEWAY_TOKEN}\",\n \"baseURL\": \"https://gateway.example\"\n }\n }\n },\n \"skills\": [\n \"$HOME/.config/blue/runtime/opencode/skills\"\n ]\n}\n", + "mode": 384, + "path": "$HOME/.config/blue/runtime/opencode/opencode.json" + }, + { + "body": "// Managed by Blue. Changes will be overwritten.\nimport { spawn } from \"node:child_process\"\nimport { mkdir, rm, writeFile } from \"node:fs/promises\"\nimport { dirname } from \"node:path\"\n\nconst BLUE_SESSION_ID = process.env.BLUE_SESSION_ID\nconst BLUE_DATA_DIR = process.env.BLUE_DATA_DIR\nconst EXPORTING = \"BLUE_OPENCODE_SESSION_EXPORT\"\nconst blueTranscriptPath = (sessionID) =>\n BLUE_SESSION_ID && BLUE_DATA_DIR\n ? `${BLUE_DATA_DIR}/sessions/${BLUE_SESSION_ID}/opencode-transcript-${sessionID}.json`\n : `${process.env.TMPDIR || \"/tmp\"}/blue-opencode-${sessionID}.json`\n\nconst run = (command, args, input, env = process.env) =>\n new Promise((resolve, reject) => {\n const child = spawn(command, args, { env, stdio: [\"pipe\", \"pipe\", \"inherit\"] })\n let stdout = \"\"\n child.stdout.setEncoding(\"utf8\")\n child.stdout.on(\"data\", (chunk) => { stdout += chunk })\n child.on(\"error\", reject)\n child.on(\"close\", (code) =>\n code === 0 ? resolve(stdout) : reject(new Error(`${command} ${args[0]} exited with ${code}`)))\n child.stdin.end(input ?? \"\")\n })\n\nexport default {\n id: \"blue.session-upload\",\n setup(ctx) {\n if (process.env[EXPORTING]) return\n const directory = ctx.location.directory\n const controller = new AbortController()\n const uploaded = new Set()\n const started = new Set()\n const topLevel = async (sessionID) => {\n const response = await ctx.session.get({ sessionID })\n const session = response?.data ?? response\n const owner = session?.location?.directory\n return !session?.parentID && (!owner || owner === directory)\n }\n const ensureStarted = async (sessionID) => {\n if (!sessionID || started.has(sessionID)) return\n started.add(sessionID)\n try {\n if (!(await topLevel(sessionID))) return\n await run(\"$BLUE\", [\"session-start\", \"opencode\", \"--profile\", \"opencode-v2_0_0\"], JSON.stringify({\n session_id: sessionID, source: \"startup\", cwd: directory,\n hook_event_name: \"SessionStart\", transcript_path: blueTranscriptPath(sessionID),\n }))\n } catch (error) {\n started.delete(sessionID)\n console.error(\"Blue session start failed\", error)\n }\n }\n const exportTranscript = async (sessionID) => {\n // Exporting reads the shared database only; the managed overlay would\n // just start its MCP servers and plugins again.\n const { OPENCODE_CONFIG_CONTENT: _, ...env } = process.env\n const exported = await run(process.execPath, [\"--standalone\", \"session\", \"export\", sessionID], \"\",\n { ...env, [EXPORTING]: \"1\" })\n const transcriptPath = blueTranscriptPath(sessionID)\n await mkdir(dirname(transcriptPath), { recursive: true })\n await writeFile(transcriptPath, exported)\n return transcriptPath\n }\n const upload = async (sessionID) => {\n if (!sessionID) return\n if (uploaded.has(sessionID)) {\n // Keep the stable transcript current for `blue run`'s post-exit upload.\n if (BLUE_SESSION_ID && (await topLevel(sessionID).catch(() => false))) {\n await exportTranscript(sessionID).catch((error) => console.error(\"Blue session export failed\", error))\n }\n return\n }\n uploaded.add(sessionID)\n let transcriptPath\n try {\n if (!(await topLevel(sessionID))) return\n transcriptPath = await exportTranscript(sessionID)\n await run(\"$BLUE\", [\"session-upload\", \"opencode\", \"--profile\", \"opencode-v2_0_0\"], JSON.stringify({\n session_id: sessionID, transcript_path: transcriptPath, cwd: directory,\n hook_event_name: \"SessionEnd\", reason: \"idle\",\n }))\n } catch (error) {\n uploaded.delete(sessionID)\n console.error(\"Blue session upload failed\", error)\n } finally {\n if (transcriptPath && !BLUE_SESSION_ID) await rm(transcriptPath, { force: true }).catch(() => {})\n }\n }\n const finished = new Set([\n \"session.execution.succeeded\", \"session.execution.failed\", \"session.execution.interrupted\",\n ])\n void (async () => {\n for await (const event of ctx.event.subscribe({ signal: controller.signal })) {\n const sessionID = event.data?.sessionID\n await ensureStarted(sessionID)\n const idle = finished.has(event.type) ||\n (event.type === \"session.status\" && event.data?.status?.type === \"idle\")\n if (idle) await upload(sessionID)\n }\n })().catch((error) => {\n if (!controller.signal.aborted) console.error(\"Blue session events failed\", error)\n })\n return () => controller.abort()\n },\n}\n", + "mode": 384, + "path": "$HOME/.config/blue/runtime/opencode/plugins/blue-session-upload/index.js" + }, + { + "body": "export const Fixture = async () => ({})\n", + "mode": 384, + "path": "$HOME/.config/blue/runtime/opencode/plugins/fixture/index.js" + }, + { + "body": "# Example\n\nA managed skill.\n", + "mode": 384, + "path": "$HOME/.config/blue/runtime/opencode/skills/example/SKILL.md" + } + ] +} diff --git a/crates/gh-config/tests/golden/opencode-v2_0_0-governance.json b/crates/gh-config/tests/golden/opencode-v2_0_0-governance.json new file mode 100644 index 0000000..245c9cc --- /dev/null +++ b/crates/gh-config/tests/golden/opencode-v2_0_0-governance.json @@ -0,0 +1,60 @@ +{ + "args": [ + "--standalone" + ], + "env": { + "HARNESS_PACKAGE_COMPONENTS_SETTINGS": "{\"color\":\"blue\"}", + "OPENCODE_CONFIG_CONTENT": "{\n \"$schema\": \"https://opencode.ai/config.json\",\n \"agent\": {\n \"reviewer\": {\n \"description\": \"Review changes\",\n \"prompt\": \"Review carefully.\"\n }\n },\n \"mcp\": {\n \"servers\": {\n \"fixture\": {\n \"command\": [\n \"fixture-mcp\",\n \"--stdio\"\n ],\n \"disabled\": false,\n \"type\": \"local\"\n }\n }\n },\n \"model\": \"fixture-model\",\n \"permissions\": [\n {\n \"action\": \"read\",\n \"effect\": \"allow\",\n \"resource\": \"*\"\n },\n {\n \"action\": \"glob\",\n \"effect\": \"allow\",\n \"resource\": \"*\"\n },\n {\n \"action\": \"grep\",\n \"effect\": \"allow\",\n \"resource\": \"*\"\n },\n {\n \"action\": \"edit\",\n \"effect\": \"ask\",\n \"resource\": \"*\"\n },\n {\n \"action\": \"shell\",\n \"effect\": \"ask\",\n \"resource\": \"*\"\n },\n {\n \"action\": \"external_directory\",\n \"effect\": \"ask\",\n \"resource\": \"*\"\n },\n {\n \"action\": \"webfetch\",\n \"effect\": \"allow\",\n \"resource\": \"*\"\n },\n {\n \"action\": \"subagent\",\n \"effect\": \"allow\",\n \"resource\": \"*\"\n },\n {\n \"action\": \"skill\",\n \"effect\": \"allow\",\n \"resource\": \"*\"\n },\n {\n \"action\": \"todowrite\",\n \"effect\": \"allow\",\n \"resource\": \"*\"\n },\n {\n \"action\": \"question\",\n \"effect\": \"deny\",\n \"resource\": \"*\"\n }\n ],\n \"plugins\": [\n \"$HOME/.config/blue/runtime/opencode/plugins/blue-session-upload\",\n \"$HOME/.config/blue/runtime/opencode/plugins/fixture\"\n ],\n \"skills\": [\n \"$HOME/.config/blue/runtime/opencode/skills\"\n ],\n \"update\": \"disable\"\n}\n", + "OPENCODE_DISABLE_AUTOUPDATE": "1", + "PATH": "$HOME/package/bin:$PATH" + }, + "files": [ + "$HOME/.config/blue/runtime/opencode/agents", + "$HOME/.config/blue/runtime/opencode/opencode.json", + "$HOME/.config/blue/runtime/opencode/plugins/blue-session-upload/index.js", + "$HOME/.config/blue/runtime/opencode/plugins/fixture", + "$HOME/.config/blue/runtime/opencode/skills" + ], + "ownership": [ + "$HOME/.config/blue/runtime/opencode/agents", + "$HOME/.config/blue/runtime/opencode/compatibility-state.json", + "$HOME/.config/blue/runtime/opencode/hooks", + "$HOME/.config/blue/runtime/opencode/opencode.json", + "$HOME/.config/blue/runtime/opencode/plugins", + "$HOME/.config/blue/runtime/opencode/skills" + ], + "removals": [ + "$HOME/.config/blue/runtime/opencode/agents", + "$HOME/.config/blue/runtime/opencode/hooks", + "$HOME/.config/blue/runtime/opencode/plugins", + "$HOME/.config/blue/runtime/opencode/skills" + ], + "warnings": [], + "writes": [ + { + "body": "---\nname: reviewer\ndescription: Review changes\n---\nReview carefully.\n", + "mode": 384, + "path": "$HOME/.config/blue/runtime/opencode/agents/reviewer.md" + }, + { + "body": "{\n \"$schema\": \"https://opencode.ai/config.json\",\n \"agent\": {\n \"reviewer\": {\n \"description\": \"Review changes\",\n \"prompt\": \"Review carefully.\"\n }\n },\n \"mcp\": {\n \"servers\": {\n \"fixture\": {\n \"command\": [\n \"fixture-mcp\",\n \"--stdio\"\n ],\n \"disabled\": false,\n \"type\": \"local\"\n }\n }\n },\n \"model\": \"fixture-model\",\n \"permissions\": [\n {\n \"action\": \"read\",\n \"effect\": \"allow\",\n \"resource\": \"*\"\n },\n {\n \"action\": \"glob\",\n \"effect\": \"allow\",\n \"resource\": \"*\"\n },\n {\n \"action\": \"grep\",\n \"effect\": \"allow\",\n \"resource\": \"*\"\n },\n {\n \"action\": \"edit\",\n \"effect\": \"ask\",\n \"resource\": \"*\"\n },\n {\n \"action\": \"shell\",\n \"effect\": \"ask\",\n \"resource\": \"*\"\n },\n {\n \"action\": \"external_directory\",\n \"effect\": \"ask\",\n \"resource\": \"*\"\n },\n {\n \"action\": \"webfetch\",\n \"effect\": \"allow\",\n \"resource\": \"*\"\n },\n {\n \"action\": \"subagent\",\n \"effect\": \"allow\",\n \"resource\": \"*\"\n },\n {\n \"action\": \"skill\",\n \"effect\": \"allow\",\n \"resource\": \"*\"\n },\n {\n \"action\": \"todowrite\",\n \"effect\": \"allow\",\n \"resource\": \"*\"\n },\n {\n \"action\": \"question\",\n \"effect\": \"deny\",\n \"resource\": \"*\"\n }\n ],\n \"plugins\": [\n \"$HOME/.config/blue/runtime/opencode/plugins/blue-session-upload\",\n \"$HOME/.config/blue/runtime/opencode/plugins/fixture\"\n ],\n \"skills\": [\n \"$HOME/.config/blue/runtime/opencode/skills\"\n ]\n}\n", + "mode": 384, + "path": "$HOME/.config/blue/runtime/opencode/opencode.json" + }, + { + "body": "// Managed by Blue. Changes will be overwritten.\nimport { spawn } from \"node:child_process\"\nimport { mkdir, rm, writeFile } from \"node:fs/promises\"\nimport { dirname } from \"node:path\"\n\nconst BLUE_SESSION_ID = process.env.BLUE_SESSION_ID\nconst BLUE_DATA_DIR = process.env.BLUE_DATA_DIR\nconst EXPORTING = \"BLUE_OPENCODE_SESSION_EXPORT\"\nconst blueTranscriptPath = (sessionID) =>\n BLUE_SESSION_ID && BLUE_DATA_DIR\n ? `${BLUE_DATA_DIR}/sessions/${BLUE_SESSION_ID}/opencode-transcript-${sessionID}.json`\n : `${process.env.TMPDIR || \"/tmp\"}/blue-opencode-${sessionID}.json`\n\nconst run = (command, args, input, env = process.env) =>\n new Promise((resolve, reject) => {\n const child = spawn(command, args, { env, stdio: [\"pipe\", \"pipe\", \"inherit\"] })\n let stdout = \"\"\n child.stdout.setEncoding(\"utf8\")\n child.stdout.on(\"data\", (chunk) => { stdout += chunk })\n child.on(\"error\", reject)\n child.on(\"close\", (code) =>\n code === 0 ? resolve(stdout) : reject(new Error(`${command} ${args[0]} exited with ${code}`)))\n child.stdin.end(input ?? \"\")\n })\n\nexport default {\n id: \"blue.session-upload\",\n setup(ctx) {\n if (process.env[EXPORTING]) return\n const directory = ctx.location.directory\n const controller = new AbortController()\n const uploaded = new Set()\n const started = new Set()\n const topLevel = async (sessionID) => {\n const response = await ctx.session.get({ sessionID })\n const session = response?.data ?? response\n const owner = session?.location?.directory\n return !session?.parentID && (!owner || owner === directory)\n }\n const ensureStarted = async (sessionID) => {\n if (!sessionID || started.has(sessionID)) return\n started.add(sessionID)\n try {\n if (!(await topLevel(sessionID))) return\n await run(\"$BLUE\", [\"session-start\", \"opencode\", \"--profile\", \"opencode-v2_0_0\"], JSON.stringify({\n session_id: sessionID, source: \"startup\", cwd: directory,\n hook_event_name: \"SessionStart\", transcript_path: blueTranscriptPath(sessionID),\n }))\n } catch (error) {\n started.delete(sessionID)\n console.error(\"Blue session start failed\", error)\n }\n }\n const exportTranscript = async (sessionID) => {\n // Exporting reads the shared database only; the managed overlay would\n // just start its MCP servers and plugins again.\n const { OPENCODE_CONFIG_CONTENT: _, ...env } = process.env\n const exported = await run(process.execPath, [\"--standalone\", \"session\", \"export\", sessionID], \"\",\n { ...env, [EXPORTING]: \"1\" })\n const transcriptPath = blueTranscriptPath(sessionID)\n await mkdir(dirname(transcriptPath), { recursive: true })\n await writeFile(transcriptPath, exported)\n return transcriptPath\n }\n const upload = async (sessionID) => {\n if (!sessionID) return\n if (uploaded.has(sessionID)) {\n // Keep the stable transcript current for `blue run`'s post-exit upload.\n if (BLUE_SESSION_ID && (await topLevel(sessionID).catch(() => false))) {\n await exportTranscript(sessionID).catch((error) => console.error(\"Blue session export failed\", error))\n }\n return\n }\n uploaded.add(sessionID)\n let transcriptPath\n try {\n if (!(await topLevel(sessionID))) return\n transcriptPath = await exportTranscript(sessionID)\n await run(\"$BLUE\", [\"session-upload\", \"opencode\", \"--profile\", \"opencode-v2_0_0\"], JSON.stringify({\n session_id: sessionID, transcript_path: transcriptPath, cwd: directory,\n hook_event_name: \"SessionEnd\", reason: \"idle\",\n }))\n } catch (error) {\n uploaded.delete(sessionID)\n console.error(\"Blue session upload failed\", error)\n } finally {\n if (transcriptPath && !BLUE_SESSION_ID) await rm(transcriptPath, { force: true }).catch(() => {})\n }\n }\n const finished = new Set([\n \"session.execution.succeeded\", \"session.execution.failed\", \"session.execution.interrupted\",\n ])\n void (async () => {\n for await (const event of ctx.event.subscribe({ signal: controller.signal })) {\n const sessionID = event.data?.sessionID\n await ensureStarted(sessionID)\n const idle = finished.has(event.type) ||\n (event.type === \"session.status\" && event.data?.status?.type === \"idle\")\n if (idle) await upload(sessionID)\n }\n })().catch((error) => {\n if (!controller.signal.aborted) console.error(\"Blue session events failed\", error)\n })\n return () => controller.abort()\n },\n}\n", + "mode": 384, + "path": "$HOME/.config/blue/runtime/opencode/plugins/blue-session-upload/index.js" + }, + { + "body": "export const Fixture = async () => ({})\n", + "mode": 384, + "path": "$HOME/.config/blue/runtime/opencode/plugins/fixture/index.js" + }, + { + "body": "# Example\n\nA managed skill.\n", + "mode": 384, + "path": "$HOME/.config/blue/runtime/opencode/skills/example/SKILL.md" + } + ] +} diff --git a/crates/gh-harness/src/install.rs b/crates/gh-harness/src/install.rs index 5bd3306..b4698f0 100644 --- a/crates/gh-harness/src/install.rs +++ b/crates/gh-harness/src/install.rs @@ -34,12 +34,14 @@ pub enum InstallMethod { }, } -pub fn npm_package(harness: Harness) -> &'static str { +/// Every npm package that publishes a harness release line. OpenCode 1.x ships +/// as `opencode-ai` and 2.x as `@opencode/cli`; both install an `opencode` bin. +pub fn npm_packages(harness: Harness) -> &'static [&'static str] { match harness { - Harness::Codex => "@openai/codex", - Harness::Claude => "@anthropic-ai/claude-code", - Harness::Kimi => "@moonshot-ai/kimi-code", - Harness::Opencode => "opencode-ai", + Harness::Codex => &["@openai/codex"], + Harness::Claude => &["@anthropic-ai/claude-code"], + Harness::Kimi => &["@moonshot-ai/kimi-code"], + Harness::Opencode => &["opencode-ai", "@opencode/cli"], } } @@ -114,7 +116,6 @@ fn inspect(detected: &Detected, home: Option<&Path>) -> Installation { fn classify(detected: &Detected, canonical: &Path, home: Option<&Path>) -> InstallMethod { let harness = detected.harness; - let package = npm_package(harness); // Recognize npm-compatible layouts, including manually copied packages. // The bin entry and canonical package root must share a prefix; package // symlinks into pnpm's store (or npm link) are unsupported. @@ -125,8 +126,11 @@ fn classify(detected: &Detected, canonical: &Path, home: Option<&Path>) -> Insta { if let Some(prefix) = bin.parent() { let root = prefix.join("lib/node_modules"); - let package_dir = root.join(package); - if canonical.starts_with(&package_dir) { + for &package in npm_packages(harness) { + let package_dir = root.join(package); + if !canonical.starts_with(&package_dir) { + continue; + } let metadata = small_text(&package_dir.join("package.json")) .and_then(|text| serde_json::from_str::(&text).ok()); if let Some(metadata) = metadata { @@ -291,12 +295,17 @@ mod tests { fn npm_compatible_manual_layouts_preserve_prefix_for_every_harness() { let f = Fixture::new(); // Assemble files and symlinks manually: npm is neither run nor required. - for harness in Harness::ALL { + for (harness, package) in Harness::ALL.into_iter().flat_map(|harness| { + npm_packages(harness) + .iter() + .map(move |package| (harness, *package)) + }) { for prefix in ["nvm/versions/node/v24", "custom prefix"] { - let prefix = format!("{prefix}/{}", harness.key()); - let package_dir = format!("{prefix}/lib/node_modules/{}", npm_package(harness)); + let prefix = format!("{prefix}/{}/{}", harness.key(), package.replace('/', "-")); + let package_dir = format!("{prefix}/lib/node_modules/{package}"); let target = f.file(&format!("{package_dir}/cli.js"), "#!/usr/bin/env node"); - let metadata = serde_json::json!({"name": npm_package(harness), "bin": { harness.key(): "cli.js" }}); + let metadata = + serde_json::json!({"name": package, "bin": { harness.key(): "cli.js" }}); let metadata_path = f.file(&format!("{package_dir}/package.json"), metadata.to_string()); let path = f.link(&format!("{prefix}/bin/{}", harness.key()), &target); @@ -311,7 +320,7 @@ mod tests { installation.method, InstallMethod::NpmGlobal { prefix: f.0.join(&prefix), - package: npm_package(harness).into() + package: package.into() } ); for (label, invalid_metadata) in [ @@ -336,6 +345,46 @@ mod tests { } } + #[test] + #[cfg(unix)] + fn opencode_layouts_report_the_package_that_owns_the_binary() { + let f = Fixture::new(); + for (prefix, package) in [("v1", "opencode-ai"), ("v2", "@opencode/cli")] { + let package_dir = format!("{prefix}/lib/node_modules/{package}"); + let target = f.file(&format!("{package_dir}/bin/opencode.exe"), b"\x7fELFbinary"); + f.file( + &format!("{package_dir}/package.json"), + serde_json::json!({ + "name": package, + "bin": { "opencode": "./bin/opencode.exe", "opencode2": "./bin/opencode.exe" } + }) + .to_string(), + ); + let path = f.link(&format!("{prefix}/bin/opencode"), &target); + assert_eq!( + f.inspect(Harness::Opencode, path).method, + InstallMethod::NpmGlobal { + prefix: f.0.join(prefix), + package: package.into() + } + ); + } + // A package directory claiming the other release line's name is not trusted. + let target = f.file( + "mixed/lib/node_modules/@opencode/cli/bin/opencode.exe", + b"\x7fELF", + ); + f.file( + "mixed/lib/node_modules/@opencode/cli/package.json", + r#"{"name":"opencode-ai","bin":{"opencode":"./bin/opencode.exe"}}"#, + ); + let path = f.link("mixed/bin/opencode", &target); + assert!(matches!( + f.inspect(Harness::Opencode, path).method, + InstallMethod::Unknown { .. } + )); + } + #[test] #[cfg(unix)] fn pnpm_store_and_linked_package_layouts_are_unsupported() { diff --git a/crates/gh-harness/src/lib.rs b/crates/gh-harness/src/lib.rs index 9e63f9c..cfcc538 100644 --- a/crates/gh-harness/src/lib.rs +++ b/crates/gh-harness/src/lib.rs @@ -17,4 +17,4 @@ pub use launch::{ launch_inherited, terminal_size, PtyEvent, PtySession, RawGuard, TerminalModeGuard, }; -pub use install::{detect_installation, InstallMethod, Installation}; +pub use install::{detect_installation, npm_packages, InstallMethod, Installation}; diff --git a/tests/e2e-native/helpers.test.mjs b/tests/e2e-native/helpers.test.mjs index 8220502..56f9f6a 100644 --- a/tests/e2e-native/helpers.test.mjs +++ b/tests/e2e-native/helpers.test.mjs @@ -32,6 +32,13 @@ test("canonical lock supplies every unique pin and historical cell without slim grid.length, ); assert.ok(grid.length > 4); + const opencode = grid.filter((c) => c.agent === "opencode"); + assert.equal(opencode.find((c) => c.pin).package, "@opencode/cli"); + assert.ok( + opencode + .filter((c) => c.version.startsWith("1.")) + .every((c) => c.package === "opencode-ai"), + ); }); test("npm prefix selection uses the native layout", () => { assert.equal(binDirectory("C:\\install", "win32"), "C:\\install"); @@ -91,7 +98,7 @@ test("fixture generation preserves legacy bytes and renders native policies as d const legacy = await prepareFixtures(dir, { legacy: true }); assert.equal( legacy.packageSha256, - "f84937b7e4221f795d8603b954cff846c266276fe2bc38e8e98c9812f4c1593f", + "2bea374e0d62af5fd23d73027a4b9865675afc9c5a427246580b9a8c180d55f4", ); const native = await prepareFixtures(dir, { platform: "win32" }); const policy = YAML.parse( diff --git a/tests/e2e-native/install-agents.mjs b/tests/e2e-native/install-agents.mjs index 0f298b3..7729306 100644 --- a/tests/e2e-native/install-agents.mjs +++ b/tests/e2e-native/install-agents.mjs @@ -53,17 +53,17 @@ export async function cells(matrix = true) { const historical = JSON.parse( await readFile(join(repo, "tests/e2e-slim/agents.matrix.json")), ); + // A historical sample may name an older release line's package (OpenCode + // 1.x is `opencode-ai`, 2.x is `@opencode/cli`); it defaults to the pin's. return Object.entries(lock.agents).flatMap(([agent, pin]) => [ - pin.version, - ...(matrix - ? historical.agents[agent].versions.map((v) => v.version) - : []), - ].map((version) => ({ + { version: pin.version, package: pin.package }, + ...(matrix ? historical.agents[agent].versions : []), + ].map((sample) => ({ agent, - version, - package: pin.package, - pin: version === pin.version, + version: sample.version, + package: sample.package ?? pin.package, + pin: sample.version === pin.version, })), ); } diff --git a/tests/e2e-native/prepare-fixtures.mjs b/tests/e2e-native/prepare-fixtures.mjs index 465dc1c..d4b4bb1 100644 --- a/tests/e2e-native/prepare-fixtures.mjs +++ b/tests/e2e-native/prepare-fixtures.mjs @@ -40,19 +40,21 @@ export async function prepareFixtures( `[[hooks]]\nevent = "SessionStart"\ncommand = ${JSON.stringify(marker)}\ntimeout = 5\n`, ); } - const plugin = join(root, "opencode-plugin.js"); - await writeFile( - plugin, - (await readFile(plugin, "utf8")) - .replaceAll( - '"/tmp/blue-e2e/component-markers"', - "process.env.E2E_SLIM_MARKER_DIR", - ) - .replaceAll( - '"/tmp/blue-e2e/component-markers/opencode-package-plugin"', - "`${process.env.E2E_SLIM_MARKER_DIR}/opencode-package-plugin`", - ), - ); + for (const name of ["opencode-plugin.js", "opencode-plugin-v2.js"]) { + const plugin = join(root, name); + await writeFile( + plugin, + (await readFile(plugin, "utf8")) + .replaceAll( + '"/tmp/blue-e2e/component-markers"', + "process.env.E2E_SLIM_MARKER_DIR", + ) + .replaceAll( + '"/tmp/blue-e2e/component-markers/opencode-package-plugin"', + "`${process.env.E2E_SLIM_MARKER_DIR}/opencode-package-plugin`", + ), + ); + } const tar = await import("tar"); await tar.c( { diff --git a/tests/e2e-slim/agents.matrix.json b/tests/e2e-slim/agents.matrix.json index 800dce2..d8be6d1 100644 --- a/tests/e2e-slim/agents.matrix.json +++ b/tests/e2e-slim/agents.matrix.json @@ -11,7 +11,9 @@ "`integrity` mirrors the lock's shape (pulled from npm dist metadata when", "authoring); `profile` documents the adapter VersionInterval each sample maps", "to. `arguments` is the per-cell launch argv (the element mentioning", - "BLUE_CERT_OK is swapped for the tool-forcing prompt at cert time)." + "BLUE_CERT_OK is swapped for the tool-forcing prompt at cert time).", + "`package` is optional and overrides the lock's package for a sample from", + "an older release line (OpenCode 1.x ships as `opencode-ai`)." ], "agents": { "codex": { @@ -82,20 +84,30 @@ }, "opencode": { "versions": [ + { + "version": "1.18.25", + "package": "opencode-ai", + "integrity": "sha512-pS4RKJ9eKwU7Dp5G5pdj1rhMnpG5APixXzfTKNoFqv9aFVI36Rnza2jESvKifxyPZlsA65MQB03WCArY0EK6mg==", + "profile": "opencode-v0_0_0", + "arguments": ["run", "Return BLUE_CERT_OK and do not edit files."] + }, { "version": "1.18.10", + "package": "opencode-ai", "integrity": "sha512-/PoGrZnTrSrmHvvXUg12O4yAEjytuqEQNG8usCydrYg4EtosvbtKtGIHI40DFaIhPws91Wa20niSvFG32xL8ZQ==", "profile": "opencode-v0_0_0", "arguments": ["run", "Return BLUE_CERT_OK and do not edit files."] }, { "version": "1.17.12", + "package": "opencode-ai", "integrity": "sha512-zqtPmMK6BLyAZ0SywKkt8iVwm+gB6RNM7OUbnHS8bRNuC3y6bAbYwOCLuXrE3E/RZ7CyGK2Jj08fheVmGdjKcg==", "profile": "opencode-v0_0_0", "arguments": ["run", "Return BLUE_CERT_OK and do not edit files."] }, { "version": "1.15.13", + "package": "opencode-ai", "integrity": "sha512-9LvgcILITiYPsKrBYSHQYR4ur/WBQYMHN5jEnboa/NVboPyl+WBGgvDHQ2SJasPpBmCAd2YTTorz2uMbcF7Hhw==", "profile": "opencode-v0_0_0", "arguments": ["run", "Return BLUE_CERT_OK and do not edit files."] diff --git a/tests/e2e-slim/fixtures/gateway-slim.yaml b/tests/e2e-slim/fixtures/gateway-slim.yaml index e64353f..483f16e 100644 --- a/tests/e2e-slim/fixtures/gateway-slim.yaml +++ b/tests/e2e-slim/fixtures/gateway-slim.yaml @@ -88,7 +88,7 @@ governance: name: E2E Skill version: "1.0.0" source_ref: file:///tmp/blue-e2e/e2e-package.tar.gz - sha256: f84937b7e4221f795d8603b954cff846c266276fe2bc38e8e98c9812f4c1593f + sha256: 2bea374e0d62af5fd23d73027a4b9865675afc9c5a427246580b9a8c180d55f4 adapters: codex: skills_dir: e2e-package/codex-plugin/skills diff --git a/tests/e2e-slim/fixtures/governance-only-slim.yaml b/tests/e2e-slim/fixtures/governance-only-slim.yaml index 8315ac8..f3f20ec 100644 --- a/tests/e2e-slim/fixtures/governance-only-slim.yaml +++ b/tests/e2e-slim/fixtures/governance-only-slim.yaml @@ -67,7 +67,7 @@ governance: name: E2E Skill version: "1.0.0" source_ref: file:///tmp/blue-e2e/e2e-package.tar.gz - sha256: f84937b7e4221f795d8603b954cff846c266276fe2bc38e8e98c9812f4c1593f + sha256: 2bea374e0d62af5fd23d73027a4b9865675afc9c5a427246580b9a8c180d55f4 adapters: codex: skills_dir: e2e-package/codex-plugin/skills diff --git a/tests/e2e-slim/run.sh b/tests/e2e-slim/run.sh index 4fe4f5b..61eb036 100755 --- a/tests/e2e-slim/run.sh +++ b/tests/e2e-slim/run.sh @@ -136,10 +136,13 @@ if [[ "${E2E_SLIM_INSTALL_AGENTS:-}" == "1" ]]; then count="$(jq -r ".agents.${agent}.versions | length" "$matrix_file")" for ((i = 0; i < count; i++)); do version="$(jq -r ".agents.${agent}.versions[$i].version" "$matrix_file")" + # A sample may come from an older release line's package (OpenCode 1.x + # is `opencode-ai`, 2.x is `@opencode/cli`); default to the lock's. + sample_pkg="$(jq -r --arg pkg "$pkg" ".agents.${agent}.versions[$i].package // \$pkg" "$matrix_file")" prefix="$matrix_root/$agent/$version" mkdir -p "$prefix" - echo " matrix $agent: $pkg@$version" - NPM_CONFIG_PREFIX="$prefix" npm install -g "${pkg}@${version}" + echo " matrix $agent: $sample_pkg@$version" + NPM_CONFIG_PREFIX="$prefix" npm install -g "${sample_pkg}@${version}" manifest_json="$(jq -n --argjson m "$manifest_json" \ --arg a "$agent" --arg v "$version" --arg d "$prefix/bin" \ '$m | .[$a][$v] = $d')" diff --git a/tests/e2e-slim/tests/common/mod.rs b/tests/e2e-slim/tests/common/mod.rs index 96de1c5..07f7d0f 100644 --- a/tests/e2e-slim/tests/common/mod.rs +++ b/tests/e2e-slim/tests/common/mod.rs @@ -92,9 +92,11 @@ pub fn assert_mcp_registered(home: &Home, agent: &str) { } "opencode" => { let config = read_json(&home.data_path().join("runtime/opencode/opencode.json")); - // OpenCode collapses command+args into one array under mcp.. + // OpenCode collapses command+args into one array under mcp. + // (1.x) or mcp.servers. (2.x). let first = config - .pointer("/mcp/e2e-remote/command/0") + .pointer("/mcp/servers/e2e-remote/command/0") + .or_else(|| config.pointer("/mcp/e2e-remote/command/0")) .and_then(serde_json::Value::as_str); assert_eq!( first, diff --git a/tests/e2e/agents.lock.json b/tests/e2e/agents.lock.json index bcda85f..7c60d98 100644 --- a/tests/e2e/agents.lock.json +++ b/tests/e2e/agents.lock.json @@ -26,10 +26,10 @@ "capabilities": ["gateway", "revision", "mcp", "packages", "skills", "agents", "hooks", "session_upload", "session_start", "session_resume"] }, "opencode": { - "package": "opencode-ai", - "version": "1.18.25", - "integrity": "sha512-pS4RKJ9eKwU7Dp5G5pdj1rhMnpG5APixXzfTKNoFqv9aFVI36Rnza2jESvKifxyPZlsA65MQB03WCArY0EK6mg==", - "profile": "opencode-v0_0_0", + "package": "@opencode/cli", + "version": "2.2.5", + "integrity": "sha512-sJAwrwuVa5b+pA3O1UOiSDcC9DgdPVmtkWv6aF6q3+rOiOTCqrE41tkz1kyyE2O8BYPs4eOvBuzkQ6oEm8w5Mw==", + "profile": "opencode-v2_0_0", "arguments": ["run", "Return BLUE_CERT_OK and do not edit files."], "capabilities": ["gateway", "revision", "mcp", "packages", "skills", "agents", "plugins", "hooks", "session_upload", "session_start", "session_resume"] } diff --git a/tests/e2e/fixtures/bin/fake-agent b/tests/e2e/fixtures/bin/fake-agent index 347f5f3..d560f4b 100755 --- a/tests/e2e/fixtures/bin/fake-agent +++ b/tests/e2e/fixtures/bin/fake-agent @@ -14,13 +14,25 @@ fi # OpenCode session restoration uses its native export/import contract. An # unknown export exits non-zero; import returns the new native session ID. -if [[ "$agent" == "opencode" ]] && [[ "${1:-}" == "session" ]] && [[ "${2:-}" == "list" ]]; then - printf '[]\n' - exit 0 -fi -if [[ "$agent" == "opencode" ]] && [[ "${1:-}" == "import" ]]; then - printf 'Imported session: e2e-opencode-imported\n' - exit 0 +# OpenCode 1.x imports with `import `; 2.x runs every restore command on +# a private server, `--standalone session import --directory `. +if [[ "$agent" == "opencode" ]]; then + opencode_args=("$@") + opencode_v2=false + if [[ "${opencode_args[0]:-}" == "--standalone" ]]; then + opencode_v2=true + opencode_args=("${opencode_args[@]:1}") + fi + # Real 1.x prints nothing, not `[]`, when no sessions exist. + if [[ "${opencode_args[0]:-}" == "session" ]] && [[ "${opencode_args[1]:-}" == "list" ]]; then + [[ "$opencode_v2" == true ]] && printf '[]\n' + exit 0 + fi + if [[ "${opencode_args[0]:-}" == "import" ]] || + { [[ "${opencode_args[0]:-}" == "session" ]] && [[ "${opencode_args[1]:-}" == "import" ]]; }; then + printf 'Imported session: e2e-opencode-imported\n' + exit 0 + fi fi # The supervisor's graceful-stop path sends SIGTERM and then waits for the diff --git a/tests/e2e/fixtures/blue.yaml b/tests/e2e/fixtures/blue.yaml index 03e248b..66b57b5 100644 --- a/tests/e2e/fixtures/blue.yaml +++ b/tests/e2e/fixtures/blue.yaml @@ -70,7 +70,7 @@ governance: name: E2E Skill version: "1.0.0" source_ref: file:///fixtures/e2e-package.tar.gz - sha256: f84937b7e4221f795d8603b954cff846c266276fe2bc38e8e98c9812f4c1593f + sha256: 2bea374e0d62af5fd23d73027a4b9865675afc9c5a427246580b9a8c180d55f4 adapters: codex: plugin_dir: e2e-package/codex-plugin @@ -86,6 +86,12 @@ governance: skills_dir: *skills agents_dir: *agents plugins: [e2e-package/opencode-plugin.js] + # OpenCode 2.x cannot load the v1 plugin API. + variants: + - introduced: 2.0.0 + skills_dir: *skills + agents_dir: *agents + plugins: [e2e-package/opencode-plugin-v2.js] session_upload: presign_url: http://127.0.0.1:8080/session-uploads/presign harnesses: diff --git a/tests/e2e/fixtures/governance-only-blue.yaml b/tests/e2e/fixtures/governance-only-blue.yaml index 1439b0c..1e20480 100644 --- a/tests/e2e/fixtures/governance-only-blue.yaml +++ b/tests/e2e/fixtures/governance-only-blue.yaml @@ -47,7 +47,7 @@ governance: name: E2E Skill version: "1.0.0" source_ref: file:///fixtures/e2e-package.tar.gz - sha256: f84937b7e4221f795d8603b954cff846c266276fe2bc38e8e98c9812f4c1593f + sha256: 2bea374e0d62af5fd23d73027a4b9865675afc9c5a427246580b9a8c180d55f4 adapters: codex: plugin_dir: e2e-package/codex-plugin @@ -63,6 +63,12 @@ governance: skills_dir: *skills agents_dir: *agents plugins: [e2e-package/opencode-plugin.js] + # OpenCode 2.x cannot load the v1 plugin API. + variants: + - introduced: 2.0.0 + skills_dir: *skills + agents_dir: *agents + plugins: [e2e-package/opencode-plugin-v2.js] session_upload: presign_url: http://127.0.0.1:8080/session-uploads/presign harnesses: diff --git a/tests/e2e/fixtures/package/e2e-package.tar.gz.b64 b/tests/e2e/fixtures/package/e2e-package.tar.gz.b64 index dec5d86..ab43474 100644 --- a/tests/e2e/fixtures/package/e2e-package.tar.gz.b64 +++ b/tests/e2e/fixtures/package/e2e-package.tar.gz.b64 @@ -1 +1,24 @@ -H4sIAJb2lWoAA+2d3W7byBmGvQssirLH7fFABXYTwKJnhuQMZcBFncS7a8T5aZx0W2wXWkYax1xLpEJSjozA99HznvSwN9Gr6F30pKcdkrJ+KZOUqNms+D2ALYninzh8Z15+nPmotwUVzYHTuXLeib3tgDHmloWSV5a+Ymqmr+l7AxGLmpbBLINihIlJOd1DeEv7M8cwjJxA7krk992w51yvmk/OdnFxz3rSn4Imr78Uvvjdr/Y+39t75nTQi3P0FzQmnrb3a/lH5d97+Rd//kexVR6/fv1q/DZe4u/y7zcLs3w2nf7bjt/XncGgJ/RB4F8Lz/E6Yu+zz/f+86///fef7//dr+BHAqt46Yy+FU5XBAfbqwdy9U/wgv4ZpngPjSrej0xqrn8Do37k9sUR4bZNccvmVKc255yYBtMsjs5OHx2/evzt6Z9P9JETRYGeJdej4z+dHlP+xLDfX45G32HNbKFzudDZX+9baEbj2s99HOrKjOoPtrWNPP3Hello/4kh9W9ta4dmqbn+Z8tfb3f8rhg1B73hO9erbhvl/R9jHIP/UwL4v1ozq/+pF6y2Hijt/yg2CQP/p4Is/2fYpMVsm5ng/3aeWf3Pqr5KM1je/zGLMvB/Kpj3f/5AePFZMD4J9J/CKrYhjwczzUL+j2M5Iyby+pOD/1MC+L9ak+3/qq0HcvU/9X+p/qn8ZIH/U8G8/yOYU1O3LZI0wQb4v51nVv/baf1z9U+ItdT+W8yA9l8Fbn/gBxH6iCLf76FbdBH4fdT44+RUcNyD9GxoaJNZ+1ddN9hHHwI3El+7PTFZzJNLHF6EB1Lj8mCKsKFpBwfoiYhE0Hc9N4zcDuo7njzXuncnnfwcXIkADUPRRW9vkOdE7rVAL+T2H8u1oY4IIvfC7cjJvqdrYpTsQ8f3wgg96g3F49nvXyZ7io6QE954HfTgITr6A/qoIeR8cNwo3e8HjYOoPzh4K5dtypNfXvTIn+UJL2qmexI29uVPDERnGIRyTw5RFAzlL3w4WcvkZ+euaUZQ6Y8dC0tuodHzZUPb/ZvXiFcciGgYeMmeoqQgDsfvEYrX3h4v3k4Pxs1hMs+Du1kQ6oqwE7iD+BAcosardG3Rpbg72NPDme7A/FEdF0Fjf7I+J3gXyl24nZmSHFAxkoclEvK4frzb58ajszcn7Zdnb745fd5+8bSBJgvdPkzfJRNutVuo4j9FFuL/PWc4qf8r20b5+D/HFOp/NcD1X61ZEf+vtB5YI/5vWQSu/1SQFf8nLdqybGZA/H/3mYv/z6q+whsAa8T/GSMQ/1fBvP+T/72ooqv+KaX8H+Gy/C0rLn/wfwoA/1drsv1ftfVAKf+X6J8bmIP/U8Gy/yO6bRLGWzaxwP/tPLP6T1VffTfgUv4vbf9NwsH/qWDe/136/lXl9m8d/2dC/E8R4P9qTbb/q7YeyNU/5ov+j1jg/5RAW8v+r8WwzQixwf7tPrP6T1S/hVFga/g/CvE/Ncz7v/DK7fU+hfgfwxD/UwP4v1qT7f+qrQfWif9R8H9KyIr/ccpMs2W0WmAAd55Z/aeq/zTifwbE/5SQUf56W4ycvtRqVdso7/84NUzwf0oA/1drMvQ/kxKqmnqgtP+jcj4K/k8Fmf5PlkQLMxv6/+0+Gfofq746H1je/3Ec538E/7d97il/vX3+9PTsTO93N9wGLj7+/67+tzAD/6cE8H+15h79T33ghvVArv6X/B9Nrv/A/22fFf7P5IaNIf63+9yj/4pa/3z9G5Qutv+UQf4fJTSbTc1z+uIQjYtdmxtM/iYU6MOl8JDjoRN6sjBw3AmvQnThB3NjzeNR+XcrQ8kppWvxVrTfJ2u4my35Bl24o2gYCE37Lt6I6137V6K7L990esOuGK83SRDwYzLQ/Nnx8+NvTp6kzrT94umPctZkrkCEA98LhQ41SSmW+3/o7Su37+pSD72KtlHc/3ODEpbe/4P8r2oA/19rlvU/tf1V1QPF/f9Y/3KCCflflbCQ/4va3NZJyzZtRjiM/9l9lvVfdetfpP23Ftt/gi1o/1Xw/fdJqf/wgyakLCN0hBrnIgyluz+XxyVqaFK50rJ34y/CS9TsoK+SNFqoOUA56a/Ql1+iyB92LvNmTE65SY6seH++amhxpeQP4x2yoHLYHhnj//R2UlTjSz0RbBwCKB//l/KH+L8awP/Xmgz9Ty8AKqoH1oj/MwzxfyWsGP/PGWcYnv+1+2Tov+LWv4D+DWsx/s9MeP6nEqbx//lin78N8GwctI8D+OHwbXKipCl7I398T+AGpVO7btiRCg9udK0fpwOezJ/eBBgnp52P5st/z18n0fwPM7cBIJSvgNX53/S2XlEaSFw2/yslzAD9qwH8f61Zrf/pZcCm9UCu/pfyvxoGg+f/KiEz/ythLUxaLXj+x+5zT/tfWTrYPP3jxfyvlJgcnv+rhPv8X1WjwMv7P2xQGP+nBvB/taaI/9u0Hijv/yi2YPyfErLz/xuYcgz9v2vAav1Xlw2ivP/DhMH4PyXkln8F2SDWiP9xC/r/qwH8X63J1X8F2SDWiP9ZGJ7/q4Qs/0cJ4fEQbIj/7T65+q8gG8Qa8T9GLPB/Kihc/htkg8Bl+n+m9T81IP+rGsD/1ZrC+t8gG0Su/pf8n8kh/78aVvg/m8TPAAD/t/MU1v8G2SBy9W8sXf9hAuO/laAw/0Nm189pIgfo+vlzULj/h95O3+g/hX7JPmDl/b8pLxhB/0oA/19rCut/6v9L1wPl/b/F4uf/gf/fPtn9PzmmLdMi4P93nsL6X7v1z9c/YWzR/zMG/f+U8FFDqBFfADQOUeMuRUNzzuY39uN5rkUQZ4WIZyM61nE6deZiIf7mbpzY4+TUWbhaGOd6a2i3oPZPhjn9+10xqr777zr9fwmH+L8awP/XmpX6r/BhcLn6X/L/Uv4Q/1dClv835BuDEALP/9h9Vuq/wofB5ekfL/f/xRSe/6aEe/yfPvt5k22U93/EMOH6Tw3g/2pNAf+3cT1Q3v8ZyfOfwP9tn0z/hxmj3OQQ/919Vrf/c5822kZ5/0cohvFfSihY/hvc/V+r/y/DMP5LDeD/ak1B/W9w93+t+/+GAeO/lLDC/3FKKYPxX7tPQf1v0PoXuf9vLeV/g/5/atjS/f/43IHb/78A8uL/FaR/WCf+yyD/lyLA/9eaPP1XkP5hnfivAfm/1JDl/01ZTpzblg3+f+fJ038F6R/Wif+akP9LDUXLf4P0D+vEf+WM4P+UAP6v1hTV/wbpH9bJ/5Bc/4H/2z4r/F8LW6ZNwf/tPEX1v0H6hzXyP1BmQfxXCZD/AQAAoJ78H+sNfsAA6AAA +H4sIAAAAAAACA+1bbU/bSBDOZ/+KkU9qg0Qcx44TKRKnoy1XoVKKjkP3oVfRxd7AEr/Juw5BKP/9 +ZtdOSEJbIDgv6HYksLO212s/s8/Ms7umDm2kxB+QS9qsrchstK7nqS3a4lbttzyn63TbbluWt2zb +a9fAq63Bci5Ihk2p/T+NzuCPf7HgzS3Av+W6nsZ/Q/hfhDlt+DQTrM9oZkVBFfh32u2f4u8g2PP4 +t72OXQNb479yazQaRkwi2oN52I2Acj9jqWBJ3IPPJEbnCODAOQCeXyhHgZxjiUiguOYWitKAcT8Z +0uzWMqIkwHon5xvyVsZfVORZDN/fHZ0dnH/eP97/ePDhHP8d/33+5dN3uLmiMbB4mAxoYBk1bevs +/35I8gB/hvkliysMA8/nf6dja/7fMP5WZe7wfPy9dkvnf9uFf7GxrnkSVx3/W53OAv7dTkvH/7XY +nQFgygTA7IGpUgDpEWUa4BMZ/s1deQ6GdC5/4Gkty7bsonQmS5BHJnnCe+U6MFcN9NkIYz81jbEO +7K+g//MBC8NK1ODz+b/dsjua/7cCfzoiURrSNcd/TAA0/luF/+mnw6Oj5QYDHtf/i/h3um5bx//1 +6v8S6Hnhf8ZpoclJrMT/fEQnfMChn2QgrihEZex/h0nEpDJQTmT9Qvort9LSfzv6fxLQUfXyfxn9 +3/Yczf8bxd+qzhuWiP8tR+d/24T/C9T/U/T/4vh/13VaOv6/Yv0vfUfL/9fc/6uT/0vwv9v1XM3/ +24B/BfJ/qfF/Pf6zXfi/QP4vo/87to7/Wv9rW2v/v0qSwQqWfy2z/qvV7Wr+3wz+AxYxSyRRWCn+ +v+T/9qL+c22p/zX/r96+flWof/tm0KFcvrUH5inlUumd4nsRpuEnETJ7IA/wK2j48DYaBCyDRgpN +EaXNiWbE7CFKkxjraEQkG6BahDdvQCS5f/XYicrlJl7YkO15axqCRTTJZYM8HQzW0/+TlMYyByxT +wMbQsa756vs/qr0H87+2q9d/rMUY9sVMwB2oXr0LNxkT9E+GqdsY+lkSgRnLVZx93kzxF+OUm4bR +bMIX9BU5ygOONYIhyRhB8kj6sOBD6EA9lR1yzDEhoIJmEYsZF8yHovfLyiS/iCQJi9sLmW5eEhZz +oS4dOlBUBvsnh5ZBR6rFAe2TPMSWGwAsKAevrB8PXhF+G/vAMf9M674Y7aiLsPiGMFE8eN18hKPM +XXxHGfXzjLMhZssiwyx3vDNTz/TNPVrXTEcrOa94PryHGSYkoMG/sTlbNbbZku/HEhmJOSbcUb1O +AyaSbAf2fi+fBqAoskgQ1CdFAEVyr97OeXm783LFrno3hc0l/WaZqc9m9VO8SyjmhUDxYDP1sTjN +RQ9fmbhN5e2Ti2vqC3xA9KJUXkrRL+7Gu4CNZbIKEp7MHOmTEIXH+L6+AkE6QgAEre8oLFQb78BP +YvQYvJmphMXJ0dnHw2NUFCZ68LSGEim1wUI9BPkE/q+G/J/A/y3PXcz/PNvR/L9W/pf8MuX8P6au +QFg592May4aKDwusrwhl4nQldRQfE1zcIl0J5Nd7upnjmSn5Y5/H4CBHGt7PHj8puGmvpIv6lB2r +YfqV8fyEyxRHSSB6U07/EW/31DkzHF81e5PssqDnx/n3IeneU26xpwrGmnK3n/8rnPJ56fiP6+nv +PzaFfxVTPi/E37Xl+n+N/0bxf8mUz7PG/1zHWdT/jvz+S+d/q7c1zv/8pmqYnKaOTBaFGMY/M7M/ +u7jjh/IDkqJelSD+bNqIFZlORjkmX5zqmSNt2rRp06ZNmzZt2rRp06btof0Hiz/nKQBQAAA= diff --git a/tests/e2e/fixtures/package/source/e2e-package/opencode-plugin-v2.js b/tests/e2e/fixtures/package/source/e2e-package/opencode-plugin-v2.js new file mode 100644 index 0000000..8db367c --- /dev/null +++ b/tests/e2e/fixtures/package/source/e2e-package/opencode-plugin-v2.js @@ -0,0 +1,19 @@ +import { mkdir, writeFile } from "node:fs/promises" + +// OpenCode 2.x variant of opencode-plugin.js: the same deterministic marker +// and tool, written against the v2 plugin API. +export default { + id: "blue.e2e-certification", + async setup(ctx) { + await mkdir("/tmp/blue-e2e/component-markers", { recursive: true }) + await writeFile("/tmp/blue-e2e/component-markers/opencode-package-plugin", "loaded\n") + await ctx.tool.transform((editor) => { + editor.add({ + name: "blue_package_certify", + description: "Return the managed OpenCode plugin certification marker", + input: { type: "object", properties: {}, additionalProperties: false }, + async execute() { return { content: "BLUE_PLUGIN_OK" } }, + }) + }) + }, +} diff --git a/tests/e2e/specs/journey.spec.ts b/tests/e2e/specs/journey.spec.ts index 1917eba..c368d69 100644 --- a/tests/e2e/specs/journey.spec.ts +++ b/tests/e2e/specs/journey.spec.ts @@ -13,7 +13,7 @@ const canonicalProfiles: Record = { codex: "codex-v0_145_0", claude: "claude-v2_0_12", kimi: "kimi-v0_0_0", - opencode: "opencode-v0_0_0", + opencode: "opencode-v2_0_0", }; const deviceUrlPattern = /http:\/\/127\.0\.0\.1:3000\/device\/[A-Za-z0-9_-]+/;