Compare commits
4
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9a0a1a801e | ||
|
|
5b1085d229 | ||
|
|
1f053c4942 | ||
|
|
c25128a6f5 |
@@ -536,9 +536,9 @@
|
||||
"typescript": "catalog:",
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@opentui/core": ">=0.2.13",
|
||||
"@opentui/keymap": ">=0.2.13",
|
||||
"@opentui/solid": ">=0.2.13",
|
||||
"@opentui/core": ">=0.2.11",
|
||||
"@opentui/keymap": ">=0.2.11",
|
||||
"@opentui/solid": ">=0.2.11",
|
||||
},
|
||||
"optionalPeers": [
|
||||
"@opentui/core",
|
||||
@@ -721,9 +721,9 @@
|
||||
"@npmcli/arborist": "9.4.0",
|
||||
"@octokit/rest": "22.0.0",
|
||||
"@openauthjs/openauth": "0.0.0-20250322224806",
|
||||
"@opentui/core": "0.2.13",
|
||||
"@opentui/keymap": "0.2.13",
|
||||
"@opentui/solid": "0.2.13",
|
||||
"@opentui/core": "0.2.11",
|
||||
"@opentui/keymap": "0.2.11",
|
||||
"@opentui/solid": "0.2.11",
|
||||
"@pierre/diffs": "1.1.0-beta.18",
|
||||
"@playwright/test": "1.59.1",
|
||||
"@sentry/solid": "10.36.0",
|
||||
@@ -735,7 +735,7 @@
|
||||
"@tailwindcss/vite": "4.1.11",
|
||||
"@tsconfig/bun": "1.0.9",
|
||||
"@tsconfig/node22": "22.0.2",
|
||||
"@types/bun": "1.3.13",
|
||||
"@types/bun": "1.3.12",
|
||||
"@types/cross-spawn": "6.0.6",
|
||||
"@types/luxon": "3.7.1",
|
||||
"@types/node": "24.12.2",
|
||||
@@ -1590,23 +1590,23 @@
|
||||
|
||||
"@opentelemetry/semantic-conventions": ["@opentelemetry/semantic-conventions@1.40.0", "", {}, "sha512-cifvXDhcqMwwTlTK04GBNeIe7yyo28Mfby85QXFe1Yk8nmi36Ab/5UQwptOx84SsoGNRg+EVSjwzfSZMy6pmlw=="],
|
||||
|
||||
"@opentui/core": ["@opentui/core@0.2.13", "", { "dependencies": { "bun-ffi-structs": "0.2.2", "diff": "9.0.0", "marked": "17.0.1", "string-width": "7.2.0", "strip-ansi": "7.1.2", "yoga-layout": "3.2.1" }, "optionalDependencies": { "@opentui/core-darwin-arm64": "0.2.13", "@opentui/core-darwin-x64": "0.2.13", "@opentui/core-linux-arm64": "0.2.13", "@opentui/core-linux-x64": "0.2.13", "@opentui/core-win32-arm64": "0.2.13", "@opentui/core-win32-x64": "0.2.13" }, "peerDependencies": { "web-tree-sitter": "0.25.10" } }, "sha512-CFnke/uhuekinVIkcyeVF62VC35I4OTrw5MXUlKU18mMsjb9U1pzB0oBJp3us1oCHKd/KuaeCnsRz4zhEPThKA=="],
|
||||
"@opentui/core": ["@opentui/core@0.2.11", "", { "dependencies": { "bun-ffi-structs": "0.2.2", "diff": "9.0.0", "marked": "17.0.1", "string-width": "7.2.0", "strip-ansi": "7.1.2", "yoga-layout": "3.2.1" }, "optionalDependencies": { "@opentui/core-darwin-arm64": "0.2.11", "@opentui/core-darwin-x64": "0.2.11", "@opentui/core-linux-arm64": "0.2.11", "@opentui/core-linux-x64": "0.2.11", "@opentui/core-win32-arm64": "0.2.11", "@opentui/core-win32-x64": "0.2.11" }, "peerDependencies": { "web-tree-sitter": "0.25.10" } }, "sha512-X0zLmcDEvMrPzWYp769I7VEVb+og38vaete9tGZXu9HnJgu/paPUUplUT+6denBQccr2qx1rBYV6EtgbBpLEyw=="],
|
||||
|
||||
"@opentui/core-darwin-arm64": ["@opentui/core-darwin-arm64@0.2.13", "", { "os": "darwin", "cpu": "arm64" }, "sha512-ZREOhS54UkF2nd7keORI1NwFe2xQdX6NCA2Uft945NqsZ5+cBe4dqoPcn6Qe4WcSfysaZQBcN0eKo623v+hiNA=="],
|
||||
"@opentui/core-darwin-arm64": ["@opentui/core-darwin-arm64@0.2.11", "", { "os": "darwin", "cpu": "arm64" }, "sha512-h2MXtE2Cu3XlKVoQMXthnbhleO68zGXkoh/r1Q5pCoZh6RuXqns5/94D/aZThXBWwzPuEoyarMlxxR9OqrpvHw=="],
|
||||
|
||||
"@opentui/core-darwin-x64": ["@opentui/core-darwin-x64@0.2.13", "", { "os": "darwin", "cpu": "x64" }, "sha512-4q+sjMATKbx/YzEKjuB4LaYe9+vrK0jFzuHaKY/Xg/cLXD8yZ9OpnyHQMCs75ijTBNKZwrsRhCUV174KLsb4eQ=="],
|
||||
"@opentui/core-darwin-x64": ["@opentui/core-darwin-x64@0.2.11", "", { "os": "darwin", "cpu": "x64" }, "sha512-Y0jbPClnOBTPSIy+2THG86MTqIG/jGFlOOKuw4JfCDqEjPBM3pLWIHnJb3WxHRi2LlvfyBxvrUTXWlW6JpI0QQ=="],
|
||||
|
||||
"@opentui/core-linux-arm64": ["@opentui/core-linux-arm64@0.2.13", "", { "os": "linux", "cpu": "arm64" }, "sha512-yPQuEdSLmZFvml4B4KevbcWqWFnSJ5xQPBTUX0Y9lEGgw8xEMkJH7QBPgGSDElTihYPu8/jTZKR0pKxknxYdbw=="],
|
||||
"@opentui/core-linux-arm64": ["@opentui/core-linux-arm64@0.2.11", "", { "os": "linux", "cpu": "arm64" }, "sha512-blQyyuTaW4q/OQ3whs7Kt7GCXhBUR5EQHHDdjOqQAr0HYpohUa6sbHMbiBcX2Ehc9ZWwtiaOoWiyZ5YXy2SAvg=="],
|
||||
|
||||
"@opentui/core-linux-x64": ["@opentui/core-linux-x64@0.2.13", "", { "os": "linux", "cpu": "x64" }, "sha512-8i1dR80/3mz5dOGja8+ui7SHl0FkaPF60YtKJhTYEOrvkhGpQoFVAx0YhjO3YM5cYQ4CkZyeD1+bpK4v65YSyQ=="],
|
||||
"@opentui/core-linux-x64": ["@opentui/core-linux-x64@0.2.11", "", { "os": "linux", "cpu": "x64" }, "sha512-0nEB5+MgzQRYiVcQd1vHXPWNPWGh4JEmQTJKyG3OHnTzPaJ1FVSQ/V71ECyRSl3ymY3F+U0eW9cFgw1hCieK2w=="],
|
||||
|
||||
"@opentui/core-win32-arm64": ["@opentui/core-win32-arm64@0.2.13", "", { "os": "win32", "cpu": "arm64" }, "sha512-5tlUt/sV/fiELecwOqeIIQOgjtdyUmNTe828JGhwRdMWCKcKz/YPMo0gwXx/HFa3lNLThA9cKADdVMnCK6N0Ow=="],
|
||||
"@opentui/core-win32-arm64": ["@opentui/core-win32-arm64@0.2.11", "", { "os": "win32", "cpu": "arm64" }, "sha512-+KKH77fzm0qF8py9G2pU32DzB1bAgDMfBajrs7gKL5NtSEnknrwfh7hIs/tq41aF6j9zvIzgtykByh26tcjFog=="],
|
||||
|
||||
"@opentui/core-win32-x64": ["@opentui/core-win32-x64@0.2.13", "", { "os": "win32", "cpu": "x64" }, "sha512-okqYNxKeNeEr/4IngR+lBrOYjt/cIybLh5AjPzafyNELBTJj6yAFTRSRJxMzuEjRyY3CB3a/aBveEdZ3N8YkcA=="],
|
||||
"@opentui/core-win32-x64": ["@opentui/core-win32-x64@0.2.11", "", { "os": "win32", "cpu": "x64" }, "sha512-dMmb9DX0W0HWadLdgciMbonqIc1xdcKiVmaQSYxw5eGCzFRPZIOrKHByesP+2ipkMuLx85W/MJUFal/lW8XSNg=="],
|
||||
|
||||
"@opentui/keymap": ["@opentui/keymap@0.2.13", "", { "dependencies": { "@opentui/core": "0.2.13" }, "peerDependencies": { "@opentui/react": "0.2.13", "@opentui/solid": "0.2.13", "react": ">=19.2.0", "solid-js": "1.9.12" }, "optionalPeers": ["@opentui/react", "@opentui/solid", "react", "solid-js"] }, "sha512-Y/IVToeiBCm5KEkt8mGP9ZdIuW9NTavOdkxo5r1/lAbo9E4O9aELD97+nRh4BccRfvRALHcVcOoYTYEGCyKHHQ=="],
|
||||
"@opentui/keymap": ["@opentui/keymap@0.2.11", "", { "dependencies": { "@opentui/core": "0.2.11" }, "peerDependencies": { "@opentui/react": "0.2.11", "@opentui/solid": "0.2.11", "react": ">=19.2.0", "solid-js": "1.9.12" }, "optionalPeers": ["@opentui/react", "@opentui/solid", "react", "solid-js"] }, "sha512-pCrJrY3mTuXdDaaRneId1JsJCtGE+7prTtWihzOLZzVJTJYyYtT38gMI7MpyAoloVDfEL5cTe8C+v7wv+IYREw=="],
|
||||
|
||||
"@opentui/solid": ["@opentui/solid@0.2.13", "", { "dependencies": { "@babel/core": "7.28.0", "@babel/preset-typescript": "7.27.1", "@opentui/core": "0.2.13", "babel-plugin-module-resolver": "5.0.2", "babel-preset-solid": "1.9.12", "entities": "7.0.1", "s-js": "^0.4.9" }, "peerDependencies": { "solid-js": "1.9.12" } }, "sha512-NQSuXj0e2Epae0z5nT7HZp7jpHTKgYAl7mwsMM/HJ/6BOtDFago2QaWIoi70gI+SsvO5z2YUREHUaQ7BI/rH3g=="],
|
||||
"@opentui/solid": ["@opentui/solid@0.2.11", "", { "dependencies": { "@babel/core": "7.28.0", "@babel/preset-typescript": "7.27.1", "@opentui/core": "0.2.11", "babel-plugin-module-resolver": "5.0.2", "babel-preset-solid": "1.9.12", "entities": "7.0.1", "s-js": "^0.4.9" }, "peerDependencies": { "solid-js": "1.9.12" } }, "sha512-M3WHxBFORHVE0yqMJYpi9PfjXWlnRTw/LYuBhZaJv0HTo+zTs60P/ukGcwnHDWnMpTGf3BH9x0Yi2dIqjHRY6Q=="],
|
||||
|
||||
"@oslojs/asn1": ["@oslojs/asn1@1.0.0", "", { "dependencies": { "@oslojs/binary": "1.0.0" } }, "sha512-zw/wn0sj0j0QKbIXfIlnEcTviaCzYOY3V5rAyjR6YtOByFtJiT574+8p9Wlach0lZH9fddD4yb9laEAIl4vXQA=="],
|
||||
|
||||
@@ -2282,7 +2282,7 @@
|
||||
|
||||
"@types/braces": ["@types/braces@3.0.5", "", {}, "sha512-SQFof9H+LXeWNz8wDe7oN5zu7ket0qwMu5vZubW4GCJ8Kkeh6nBWUz87+KTz/G3Kqsrp0j/W253XJb3KMEeg3w=="],
|
||||
|
||||
"@types/bun": ["@types/bun@1.3.13", "", { "dependencies": { "bun-types": "1.3.13" } }, "sha512-9fqXWk5YIHGGnUau9TEi+qdlTYDAnOj+xLCmSTwXfAIqXr2x4tytJb43E9uCvt09zJURKXwAtkoH4nLQfzeTXw=="],
|
||||
"@types/bun": ["@types/bun@1.3.12", "", { "dependencies": { "bun-types": "1.3.12" } }, "sha512-DBv81elK+/VSwXHDlnH3Qduw+KxkTIWi7TXkAeh24zpi5l0B2kUg9Ga3tb4nJaPcOFswflgi/yAvMVBPrxMB+A=="],
|
||||
|
||||
"@types/cacache": ["@types/cacache@20.0.1", "", { "dependencies": { "@types/node": "*", "minipass": "*" } }, "sha512-QlKW3AFoFr/hvPHwFHMIVUH/ZCYeetBNou3PCmxu5LaNDvrtBlPJtIA6uhmU9JRt9oxj7IYoqoLcpxtzpPiTcw=="],
|
||||
|
||||
@@ -2694,7 +2694,7 @@
|
||||
|
||||
"bun-pty": ["bun-pty@0.4.8", "", {}, "sha512-rO70Mrbr13+jxHHHu2YBkk2pNqrJE5cJn29WE++PUr+GFA0hq/VgtQPZANJ8dJo6d7XImvBk37Innt8GM7O28w=="],
|
||||
|
||||
"bun-types": ["bun-types@1.3.13", "", { "dependencies": { "@types/node": "*" } }, "sha512-QXKeHLlOLqQX9LgYaHJfzdBaV21T63HhFJnvuRCcjZiaUDpbs5ED1MgxbMra71CsryN/1dAoXuJJJwIv/2drVA=="],
|
||||
"bun-types": ["bun-types@1.3.12", "", { "dependencies": { "@types/node": "*" } }, "sha512-HqOLj5PoFajAQciOMRiIZGNoKxDJSr6qigAttOX40vJuSp6DN/CxWp9s3C1Xwm4oH7ybueITwiaOcWXoYVoRkA=="],
|
||||
|
||||
"bundle-name": ["bundle-name@4.1.0", "", { "dependencies": { "run-applescript": "^7.0.0" } }, "sha512-tjwM5exMg6BGRI+kNmTntNsvdZS1X8BFYS6tnJ2hdH0kVxM6/eVZ2xy+FqStSWvYmtfFMDLIxurorHwDKfDz5Q=="],
|
||||
|
||||
@@ -5834,6 +5834,10 @@
|
||||
|
||||
"openid-client/lru-cache": ["lru-cache@6.0.0", "", { "dependencies": { "yallist": "^4.0.0" } }, "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA=="],
|
||||
|
||||
"opentui-spinner/@opentui/core": ["@opentui/core@0.2.7", "", { "dependencies": { "bun-ffi-structs": "0.2.2", "diff": "9.0.0", "marked": "17.0.1", "string-width": "7.2.0", "strip-ansi": "7.1.2", "yoga-layout": "3.2.1" }, "optionalDependencies": { "@opentui/core-darwin-arm64": "0.2.7", "@opentui/core-darwin-x64": "0.2.7", "@opentui/core-linux-arm64": "0.2.7", "@opentui/core-linux-x64": "0.2.7", "@opentui/core-win32-arm64": "0.2.7", "@opentui/core-win32-x64": "0.2.7" }, "peerDependencies": { "web-tree-sitter": "0.25.10" } }, "sha512-cnN6JcaGC7SeQzobBy/CHzqUAQFtypazuw1CjQBo7WwoOiLMGubt9W5FXeF0zIrSxH2Ed6NLWhPYRg7SD4629Q=="],
|
||||
|
||||
"opentui-spinner/@opentui/solid": ["@opentui/solid@0.2.7", "", { "dependencies": { "@babel/core": "7.28.0", "@babel/preset-typescript": "7.27.1", "@opentui/core": "0.2.7", "babel-plugin-module-resolver": "5.0.2", "babel-preset-solid": "1.9.12", "entities": "7.0.1", "s-js": "^0.4.9" }, "peerDependencies": { "solid-js": "1.9.12" } }, "sha512-nlkx9HvuWaHtc5A8eUEAPNi+5+37LZS3ln73WRmtT5xin8LnQf+yhwopqGgPSnLq1ODLwhkKRdr/9JCDr2j7Bg=="],
|
||||
|
||||
"ora/bl": ["bl@4.1.0", "", { "dependencies": { "buffer": "^5.5.0", "inherits": "^2.0.4", "readable-stream": "^3.4.0" } }, "sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w=="],
|
||||
|
||||
"ora/chalk": ["chalk@4.1.2", "", { "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" } }, "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA=="],
|
||||
@@ -6604,6 +6608,22 @@
|
||||
|
||||
"opencontrol/@modelcontextprotocol/sdk/zod-to-json-schema": ["zod-to-json-schema@3.25.2", "", { "peerDependencies": { "zod": "^3.25.28 || ^4" } }, "sha512-O/PgfnpT1xKSDeQYSCfRI5Gy3hPf91mKVDuYLUHZJMiDFptvP41MSnWofm8dnCm0256ZNfZIM7DSzuSMAFnjHA=="],
|
||||
|
||||
"opentui-spinner/@opentui/core/@opentui/core-darwin-arm64": ["@opentui/core-darwin-arm64@0.2.7", "", { "os": "darwin", "cpu": "arm64" }, "sha512-CAy6cL3byz2Xf6gFiJHBpcnsp/2ADEWLLOUokVypOyPLcy8GY3sPzlA4pkAjVGQMYQhDj+Y3+SXz4uTLt4AETg=="],
|
||||
|
||||
"opentui-spinner/@opentui/core/@opentui/core-darwin-x64": ["@opentui/core-darwin-x64@0.2.7", "", { "os": "darwin", "cpu": "x64" }, "sha512-K06h333rMkC9cyMJr/VvcRK3ik81Admd8ZsES5uf5YXWPdYhXGf75I1T8mKIThhUmoFLb8R5xqfuPmoocsjM7Q=="],
|
||||
|
||||
"opentui-spinner/@opentui/core/@opentui/core-linux-arm64": ["@opentui/core-linux-arm64@0.2.7", "", { "os": "linux", "cpu": "arm64" }, "sha512-iYWGTztbdG9yYSB5Alxuo0dWAmkWQR0+/paNWUyPOocjigmKgMmACDtHgYqa7sxkIcWgmXljt/f8rgXDG4wdMg=="],
|
||||
|
||||
"opentui-spinner/@opentui/core/@opentui/core-linux-x64": ["@opentui/core-linux-x64@0.2.7", "", { "os": "linux", "cpu": "x64" }, "sha512-tymBCfYbsDRfHQNXsolkFfaTEIDhemD4+1ZovUztQd7i+0Ggnu9WbPN1SNCiRz6PjrlaNeQzZE3Wl8FfVdw/cw=="],
|
||||
|
||||
"opentui-spinner/@opentui/core/@opentui/core-win32-arm64": ["@opentui/core-win32-arm64@0.2.7", "", { "os": "win32", "cpu": "arm64" }, "sha512-XLPJWdT8QOukrYDkpIng6+uNUlF66ByXcQlC3qA9JbrUTBetZhgXs8Q2jEjRfc+Ty3uh1iRSA6PgJGbbOK/f4Q=="],
|
||||
|
||||
"opentui-spinner/@opentui/core/@opentui/core-win32-x64": ["@opentui/core-win32-x64@0.2.7", "", { "os": "win32", "cpu": "x64" }, "sha512-CzVGEfqysVk8Hxcj0RDv/DtXIM6iZmbmr23kW7y8CJMPtmV1gmKI4D9abVjynWJnGbaSBnDi43mgZnGMgOdyEg=="],
|
||||
|
||||
"opentui-spinner/@opentui/core/diff": ["diff@9.0.0", "", {}, "sha512-svtcdpS8CgJyqAjEQIXdb3OjhFVVYjzGAPO8WGCmRbrml64SPw/jJD4GoE98aR7r25A0XcgrK3F02yw9R/vhQw=="],
|
||||
|
||||
"opentui-spinner/@opentui/solid/@babel/core": ["@babel/core@7.28.0", "", { "dependencies": { "@ampproject/remapping": "^2.2.0", "@babel/code-frame": "^7.27.1", "@babel/generator": "^7.28.0", "@babel/helper-compilation-targets": "^7.27.2", "@babel/helper-module-transforms": "^7.27.3", "@babel/helpers": "^7.27.6", "@babel/parser": "^7.28.0", "@babel/template": "^7.27.2", "@babel/traverse": "^7.28.0", "@babel/types": "^7.28.0", "convert-source-map": "^2.0.0", "debug": "^4.1.0", "gensync": "^1.0.0-beta.2", "json5": "^2.2.3", "semver": "^6.3.1" } }, "sha512-UlLAnTPrFdNGoFtbSXwcGFQBtQZJCNjaN6hQNP3UPvuNXT1i82N26KL3dZeIpNalWywr9IuQuncaAfUaS1g6sQ=="],
|
||||
|
||||
"ora/bl/buffer": ["buffer@5.7.1", "", { "dependencies": { "base64-js": "^1.3.1", "ieee754": "^1.1.13" } }, "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ=="],
|
||||
|
||||
"ora/bl/readable-stream": ["readable-stream@3.6.2", "", { "dependencies": { "inherits": "^2.0.3", "string_decoder": "^1.1.1", "util-deprecate": "^1.0.1" } }, "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA=="],
|
||||
@@ -6954,6 +6974,8 @@
|
||||
|
||||
"opencontrol/@modelcontextprotocol/sdk/express/type-is": ["type-is@2.0.1", "", { "dependencies": { "content-type": "^1.0.5", "media-typer": "^1.1.0", "mime-types": "^3.0.0" } }, "sha512-OZs6gsjF4vMp32qrCbiVSkrFmXtG/AZhY3t0iAMrMBiAZyV9oALtXO8hsrHbMXF9x6L3grlFuwW2oAz7cav+Gw=="],
|
||||
|
||||
"opentui-spinner/@opentui/solid/@babel/core/semver": ["semver@6.3.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA=="],
|
||||
|
||||
"ora/bl/buffer/ieee754": ["ieee754@1.2.1", "", {}, "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA=="],
|
||||
|
||||
"pkg-dir/find-up/locate-path/p-locate": ["p-locate@4.1.0", "", { "dependencies": { "p-limit": "^2.2.0" } }, "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A=="],
|
||||
|
||||
+4
-4
@@ -1,8 +1,8 @@
|
||||
{
|
||||
"nodeModules": {
|
||||
"x86_64-linux": "sha256-myt0dFX0ebXetjf9Rvpof4XmJ+tV6bKyYZaYMcdpjmk=",
|
||||
"aarch64-linux": "sha256-qLhu0gjCQpvLcOLC3CgaZkimPIa+8BVvm6QkuA5DRoo=",
|
||||
"aarch64-darwin": "sha256-C163D3NlVU2wurIfPmQfBD5byzUEElGymmOVTiIBJ70=",
|
||||
"x86_64-darwin": "sha256-f/E27WEJz4hweXVvkP+Nwl8RW4HIH3qZf4VuZdD6AaY="
|
||||
"x86_64-linux": "sha256-Ucvyzyq+oYvWglkeowSvb0LgDzkAvaSdq0CdA6jgN6U=",
|
||||
"aarch64-linux": "sha256-SERwZvvN6P8/OwNolHmC0KU9H5laVQm+FD/NNKauZA8=",
|
||||
"aarch64-darwin": "sha256-I1ABwMHkTAntlYyg43w0cW8iPYfZa9MT0In2C7plB5g=",
|
||||
"x86_64-darwin": "sha256-degJTL0RG7QQO8/USgIF//ya7oNmwChTmAoJcpXbIp0="
|
||||
}
|
||||
}
|
||||
|
||||
+5
-5
@@ -4,7 +4,7 @@
|
||||
"description": "AI-powered development tool",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"packageManager": "bun@1.3.14",
|
||||
"packageManager": "bun@1.3.13",
|
||||
"scripts": {
|
||||
"dev": "bun run --cwd packages/opencode --conditions=browser src/index.ts",
|
||||
"dev:desktop": "bun --cwd packages/desktop dev",
|
||||
@@ -31,13 +31,13 @@
|
||||
"@effect/opentelemetry": "4.0.0-beta.65",
|
||||
"@effect/platform-node": "4.0.0-beta.65",
|
||||
"@npmcli/arborist": "9.4.0",
|
||||
"@types/bun": "1.3.13",
|
||||
"@types/bun": "1.3.12",
|
||||
"@types/cross-spawn": "6.0.6",
|
||||
"@octokit/rest": "22.0.0",
|
||||
"@hono/zod-validator": "0.4.2",
|
||||
"@opentui/core": "0.2.13",
|
||||
"@opentui/keymap": "0.2.13",
|
||||
"@opentui/solid": "0.2.13",
|
||||
"@opentui/core": "0.2.11",
|
||||
"@opentui/keymap": "0.2.11",
|
||||
"@opentui/solid": "0.2.11",
|
||||
"ulid": "3.0.1",
|
||||
"@kobalte/core": "0.13.11",
|
||||
"@types/luxon": "3.7.1",
|
||||
|
||||
@@ -10,6 +10,7 @@
|
||||
<link rel="apple-touch-icon" sizes="180x180" href="/apple-touch-icon-v3.png" />
|
||||
<link rel="manifest" href="/site.webmanifest" />
|
||||
<meta name="theme-color" content="#F8F7F7" />
|
||||
<meta name="theme-color" content="#131010" media="(prefers-color-scheme: dark)" />
|
||||
<meta property="og:image" content="/social-share.png" />
|
||||
<meta property="twitter:image" content="/social-share.png" />
|
||||
<script id="oc-theme-preload-script" src="/oc-theme-preload.js"></script>
|
||||
|
||||
@@ -16,10 +16,6 @@
|
||||
document.documentElement.dataset.theme = themeId
|
||||
document.documentElement.dataset.colorScheme = mode
|
||||
|
||||
// Update theme-color meta tag to match app color scheme
|
||||
var metas = document.querySelectorAll("meta[name='theme-color']")
|
||||
if (metas.length > 0) metas[0].setAttribute("content", isDark ? "#131010" : "#F8F7F7")
|
||||
|
||||
if (themeId === "oc-2") return
|
||||
|
||||
var css = localStorage.getItem("opencode-theme-css-" + mode)
|
||||
|
||||
@@ -41,7 +41,6 @@ export function trimSessions(
|
||||
.filter((s) => !s.time?.archived)
|
||||
.sort((a, b) => cmp(a.id, b.id))
|
||||
const roots = all.filter((s) => !s.parentID)
|
||||
roots.sort(compareSessionRecent)
|
||||
const children = all.filter((s) => !!s.parentID)
|
||||
const base = roots.slice(0, limit)
|
||||
const recent = takeRecentSessions(roots.slice(limit), SESSION_RECENT_LIMIT, cutoff)
|
||||
|
||||
@@ -4,7 +4,7 @@ FROM ${REGISTRY}/build/base:24.04
|
||||
SHELL ["/bin/bash", "-lc"]
|
||||
|
||||
ARG NODE_VERSION=24.4.0
|
||||
ARG BUN_VERSION=1.3.14
|
||||
ARG BUN_VERSION=1.3.13
|
||||
|
||||
ENV BUN_INSTALL=/opt/bun
|
||||
ENV PATH=/opt/bun/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
|
||||
|
||||
@@ -0,0 +1,242 @@
|
||||
export * as AuthWellKnown from "./auth-well-known"
|
||||
|
||||
import path from "path"
|
||||
import { Context, Effect, Layer, Option, Schema, SynchronizedRef } from "effect"
|
||||
import { FetchHttpClient, HttpClient, HttpClientRequest } from "effect/unstable/http"
|
||||
import { AppFileSystem } from "./filesystem"
|
||||
import { Global } from "./global"
|
||||
import { Substitution } from "./substitution"
|
||||
|
||||
export class Entry extends Schema.Class<Entry>("AuthWellKnown.Entry")({
|
||||
key: Schema.String,
|
||||
token: Schema.String,
|
||||
}) {}
|
||||
|
||||
export class FileWriteError extends Schema.TaggedErrorClass<FileWriteError>()("AuthWellKnown.FileWriteError", {
|
||||
operation: Schema.Union([Schema.Literal("migrate"), Schema.Literal("write")]),
|
||||
cause: Schema.Defect,
|
||||
}) {}
|
||||
|
||||
export class RemoteConfigError extends Schema.TaggedErrorClass<RemoteConfigError>()("AuthWellKnown.RemoteConfigError", {
|
||||
url: Schema.String,
|
||||
status: Schema.Number.pipe(Schema.optional),
|
||||
cause: Schema.Defect.pipe(Schema.optional),
|
||||
}) {}
|
||||
|
||||
export type Error = FileWriteError | RemoteConfigError
|
||||
|
||||
const RemoteConfig = Schema.Struct({
|
||||
url: Schema.String,
|
||||
headers: Schema.Record(Schema.String, Schema.String).pipe(Schema.optional),
|
||||
})
|
||||
|
||||
export class Metadata extends Schema.Class<Metadata>("AuthWellKnown.Metadata")({
|
||||
auth: Schema.Struct({
|
||||
command: Schema.Array(Schema.String),
|
||||
env: Schema.String,
|
||||
}).pipe(Schema.optional),
|
||||
config: Schema.Record(Schema.String, Schema.Unknown).pipe(Schema.optional),
|
||||
remote_config: RemoteConfig.pipe(Schema.optional),
|
||||
}) {}
|
||||
|
||||
export type ConfigDocument = {
|
||||
url: string
|
||||
source: string
|
||||
dir: string
|
||||
content: unknown
|
||||
}
|
||||
|
||||
export interface Interface {
|
||||
readonly all: () => Effect.Effect<Record<string, Entry>, Error>
|
||||
readonly get: (url: string) => Effect.Effect<Entry | undefined, Error>
|
||||
readonly set: (url: string, entry: Entry) => Effect.Effect<void, Error>
|
||||
readonly remove: (url: string) => Effect.Effect<void, Error>
|
||||
readonly metadata: (url: string) => Effect.Effect<Metadata, Error>
|
||||
readonly configs: () => Effect.Effect<ConfigDocument[], Error>
|
||||
}
|
||||
|
||||
export class Service extends Context.Service<Service, Interface>()("@opencode/v2/AuthWellKnown") {}
|
||||
const decodeMetadata = Schema.decodeUnknownEffect(Metadata)
|
||||
const decodeRemoteConfig = Schema.decodeUnknownEffect(RemoteConfig)
|
||||
|
||||
function loadLegacyAuth(input: {
|
||||
fsys: AppFileSystem.Interface
|
||||
dataDir: string
|
||||
write: (data: Record<string, Entry>) => Effect.Effect<void, Error>
|
||||
}) {
|
||||
return Effect.gen(function* () {
|
||||
const decodeLegacy = Schema.decodeUnknownOption(Schema.Record(Schema.String, Schema.Unknown))
|
||||
const decodeLegacyCredential = Schema.decodeUnknownOption(
|
||||
Schema.Struct({
|
||||
type: Schema.Literal("wellknown"),
|
||||
key: Schema.String,
|
||||
token: Schema.String,
|
||||
}),
|
||||
)
|
||||
const legacy = Object.fromEntries(
|
||||
Object.entries(
|
||||
Option.getOrElse(
|
||||
decodeLegacy(
|
||||
yield* input.fsys.readJson(path.join(input.dataDir, "auth.json")).pipe(Effect.orElseSucceed(() => null)),
|
||||
),
|
||||
() => ({}),
|
||||
),
|
||||
).flatMap(([url, value]) => {
|
||||
const decoded = Option.getOrUndefined(decodeLegacyCredential(value))
|
||||
return decoded ? [[url.replace(/\/+$/, ""), new Entry({ key: decoded.key, token: decoded.token })]] : []
|
||||
}),
|
||||
)
|
||||
if (Object.keys(legacy).length > 0) yield* input.write(legacy).pipe(Effect.ignore)
|
||||
return legacy
|
||||
})
|
||||
}
|
||||
|
||||
export const layer = Layer.effect(
|
||||
Service,
|
||||
Effect.gen(function* () {
|
||||
const fsys = yield* AppFileSystem.Service
|
||||
const global = yield* Global.Service
|
||||
const http = yield* HttpClient.HttpClient
|
||||
const substitution = yield* Substitution.Service
|
||||
const file = path.join(global.data, "well-known.json")
|
||||
const decodeEntries = Schema.decodeUnknownOption(Schema.Record(Schema.String, Entry))
|
||||
const normalizeUrl = (url: string) => url.replace(/\/+$/, "")
|
||||
|
||||
const write = (operation: "migrate" | "write", data: Record<string, Entry>) =>
|
||||
fsys.writeJson(file, data, 0o600).pipe(Effect.mapError((cause) => new FileWriteError({ operation, cause })))
|
||||
|
||||
const load: () => Effect.Effect<Record<string, Entry>> = Effect.fnUntraced(function* () {
|
||||
const current = yield* fsys.readJson(file).pipe(Effect.orElseSucceed(() => null))
|
||||
if (current && typeof current === "object")
|
||||
return Option.getOrElse(decodeEntries(current), () => ({}) as Record<string, Entry>)
|
||||
return yield* loadLegacyAuth({ fsys, dataDir: global.data, write: (data) => write("migrate", data) })
|
||||
})
|
||||
|
||||
const state = SynchronizedRef.makeUnsafe<Record<string, Entry>>(yield* load())
|
||||
|
||||
const metadata = Effect.fn("AuthWellKnown.metadata")(function* (url: string) {
|
||||
const normalized = normalizeUrl(url)
|
||||
const source = `${normalized}/.well-known/opencode`
|
||||
const response = yield* HttpClientRequest.get(source).pipe(
|
||||
HttpClientRequest.acceptJson,
|
||||
http.execute,
|
||||
Effect.mapError((cause) => new RemoteConfigError({ url: source, cause })),
|
||||
)
|
||||
if (response.status < 200 || response.status >= 300) {
|
||||
return yield* new RemoteConfigError({ url: source, status: response.status })
|
||||
}
|
||||
const metadata = yield* response.json.pipe(
|
||||
Effect.flatMap(decodeMetadata),
|
||||
Effect.mapError((cause) => new RemoteConfigError({ url: source, cause })),
|
||||
)
|
||||
return { url: normalized, source, dir: path.dirname(source), metadata }
|
||||
})
|
||||
|
||||
const remote = Effect.fn("AuthWellKnown.remote")(function* (input: { url: string; headers?: Record<string, string> }) {
|
||||
const response = yield* HttpClientRequest.get(input.url).pipe(
|
||||
HttpClientRequest.acceptJson,
|
||||
input.headers ? HttpClientRequest.setHeaders(input.headers) : (request) => request,
|
||||
http.execute,
|
||||
Effect.mapError((cause) => new RemoteConfigError({ url: input.url, cause })),
|
||||
)
|
||||
if (response.status < 200 || response.status >= 300) {
|
||||
return yield* new RemoteConfigError({ url: input.url, status: response.status })
|
||||
}
|
||||
return yield* response.json.pipe(Effect.mapError((cause) => new RemoteConfigError({ url: input.url, cause })))
|
||||
})
|
||||
|
||||
return Service.of({
|
||||
all: Effect.fn("AuthWellKnown.all")(function* () {
|
||||
return yield* SynchronizedRef.get(state)
|
||||
}),
|
||||
|
||||
get: Effect.fn("AuthWellKnown.get")(function* (url) {
|
||||
return (yield* SynchronizedRef.get(state))[normalizeUrl(url)]
|
||||
}),
|
||||
|
||||
set: Effect.fn("AuthWellKnown.set")(function* (url, entry) {
|
||||
yield* SynchronizedRef.updateEffect(
|
||||
state,
|
||||
Effect.fnUntraced(function* (data) {
|
||||
const next = { ...data, [normalizeUrl(url)]: entry }
|
||||
yield* write("write", next)
|
||||
return next
|
||||
}),
|
||||
)
|
||||
}),
|
||||
|
||||
remove: Effect.fn("AuthWellKnown.remove")(function* (url) {
|
||||
yield* SynchronizedRef.updateEffect(
|
||||
state,
|
||||
Effect.fnUntraced(function* (data) {
|
||||
const next = { ...data }
|
||||
delete next[url]
|
||||
delete next[normalizeUrl(url)]
|
||||
yield* write("write", next)
|
||||
return next
|
||||
}),
|
||||
)
|
||||
}),
|
||||
|
||||
metadata: Effect.fn("AuthWellKnown.metadata.public")(function* (url) {
|
||||
return (yield* metadata(url)).metadata
|
||||
}),
|
||||
|
||||
configs: Effect.fn("AuthWellKnown.configs")(function* () {
|
||||
const documents = yield* Effect.all(
|
||||
Object.entries(yield* SynchronizedRef.get(state)).map(([url, entry]) =>
|
||||
Effect.gen(function* () {
|
||||
const configs: ConfigDocument[] = []
|
||||
const response = yield* metadata(url)
|
||||
const env = { [entry.key]: entry.token }
|
||||
if (response.metadata.config) {
|
||||
configs.push({
|
||||
url: response.url,
|
||||
source: response.source,
|
||||
dir: response.dir,
|
||||
content: response.metadata.config,
|
||||
})
|
||||
}
|
||||
if (response.metadata.remote_config) {
|
||||
const remoteConfig = yield* substitution
|
||||
.substitute({
|
||||
text: JSON.stringify(response.metadata.remote_config),
|
||||
type: "virtual",
|
||||
dir: response.url,
|
||||
source: response.source,
|
||||
env,
|
||||
})
|
||||
.pipe(
|
||||
Effect.flatMap((text) =>
|
||||
Effect.try({
|
||||
try: () => JSON.parse(text) as unknown,
|
||||
catch: (cause) => new RemoteConfigError({ url: response.source, cause }),
|
||||
}),
|
||||
),
|
||||
Effect.flatMap(decodeRemoteConfig),
|
||||
Effect.mapError((cause) => new RemoteConfigError({ url: response.source, cause })),
|
||||
)
|
||||
configs.push({
|
||||
url: remoteConfig.url,
|
||||
source: remoteConfig.url,
|
||||
dir: path.dirname(remoteConfig.url),
|
||||
content: yield* remote({ url: remoteConfig.url, headers: remoteConfig.headers }),
|
||||
})
|
||||
}
|
||||
return configs
|
||||
}),
|
||||
),
|
||||
{ concurrency: "unbounded" },
|
||||
)
|
||||
return documents.flat()
|
||||
}),
|
||||
})
|
||||
}),
|
||||
)
|
||||
|
||||
export const defaultLayer = layer.pipe(
|
||||
Layer.provide(AppFileSystem.defaultLayer),
|
||||
Layer.provide(Global.defaultLayer),
|
||||
Layer.provide(FetchHttpClient.layer),
|
||||
Layer.provide(Substitution.defaultLayer),
|
||||
)
|
||||
+2
@@ -0,0 +1,2 @@
|
||||
// Auto-generated by build.ts - do not edit
|
||||
export declare const snapshot: Record<string, unknown>
|
||||
File diff suppressed because it is too large
Load Diff
@@ -105,8 +105,6 @@ export const Provider = Schema.Struct({
|
||||
|
||||
export type Provider = Schema.Schema.Type<typeof Provider>
|
||||
|
||||
declare const OPENCODE_MODELS_DEV: Record<string, Provider> | undefined
|
||||
|
||||
export interface Interface {
|
||||
readonly get: () => Effect.Effect<Record<string, Provider>>
|
||||
readonly refresh: (force?: boolean) => Effect.Effect<void>
|
||||
@@ -114,7 +112,9 @@ export interface Interface {
|
||||
|
||||
export class Service extends Context.Service<Service, Interface>()("@opencode/ModelsDev") {}
|
||||
|
||||
export const layer = Layer.effect(
|
||||
type Requirements = AppFileSystem.Service | HttpClient.HttpClient
|
||||
|
||||
export const layer: Layer.Layer<Service, never, Requirements> = Layer.effect(
|
||||
Service,
|
||||
Effect.gen(function* () {
|
||||
const fs = yield* AppFileSystem.Service
|
||||
@@ -157,9 +157,12 @@ export const layer = Layer.effect(
|
||||
Effect.map((v) => v as Record<string, Provider> | undefined),
|
||||
)
|
||||
|
||||
const loadSnapshot = Effect.sync(() =>
|
||||
typeof OPENCODE_MODELS_DEV === "undefined" ? undefined : OPENCODE_MODELS_DEV,
|
||||
)
|
||||
// Bundled at build time; absent in dev — `tryPromise` covers both.
|
||||
const loadSnapshot = Effect.tryPromise({
|
||||
// @ts-ignore — generated at build time, may not exist in dev
|
||||
try: () => import("./models-snapshot.js").then((m) => m.snapshot as Record<string, Provider> | undefined),
|
||||
catch: () => undefined,
|
||||
}).pipe(Effect.catch(() => Effect.succeed(undefined)))
|
||||
|
||||
const fetchAndWrite = Effect.fn("ModelsDev.fetchAndWrite")(function* () {
|
||||
const text = yield* fetchApi()
|
||||
@@ -220,4 +223,4 @@ export const defaultLayer: Layer.Layer<Service> = layer.pipe(
|
||||
Layer.provide(AppFileSystem.defaultLayer),
|
||||
)
|
||||
|
||||
export * as ModelsDev from "./models-dev"
|
||||
export * as ModelsDev from "./models"
|
||||
@@ -1,7 +1,7 @@
|
||||
import { DateTime, Effect } from "effect"
|
||||
import { Catalog } from "../catalog"
|
||||
import { ModelV2 } from "../model"
|
||||
import { ModelsDev } from "../models-dev"
|
||||
import { ModelsDev } from "../models"
|
||||
import { PluginV2 } from "../plugin"
|
||||
import { ProviderV2 } from "../provider"
|
||||
|
||||
|
||||
@@ -0,0 +1,94 @@
|
||||
export * as Substitution from "./substitution"
|
||||
|
||||
import os from "os"
|
||||
import path from "path"
|
||||
import { Context, Effect, Layer, Schema } from "effect"
|
||||
import { AppFileSystem } from "./filesystem"
|
||||
|
||||
type Source =
|
||||
| {
|
||||
type: "path"
|
||||
path: string
|
||||
}
|
||||
| {
|
||||
type: "virtual"
|
||||
source: string
|
||||
dir: string
|
||||
}
|
||||
|
||||
export type Input = Source & {
|
||||
text: string
|
||||
missing?: "error" | "empty"
|
||||
env?: Record<string, string | undefined>
|
||||
}
|
||||
|
||||
export class FileReferenceError extends Schema.TaggedErrorClass<FileReferenceError>()("Substitution.FileReferenceError", {
|
||||
source: Schema.String,
|
||||
token: Schema.String,
|
||||
resolved: Schema.String,
|
||||
cause: Schema.Defect,
|
||||
}) {}
|
||||
|
||||
export type Error = FileReferenceError
|
||||
|
||||
export interface Interface {
|
||||
readonly substitute: (input: Input) => Effect.Effect<string, Error>
|
||||
}
|
||||
|
||||
export class Service extends Context.Service<Service, Interface>()("@opencode/v2/Substitution") {}
|
||||
|
||||
export const layer = Layer.effect(
|
||||
Service,
|
||||
Effect.gen(function* () {
|
||||
const fs = yield* AppFileSystem.Service
|
||||
|
||||
return Service.of({
|
||||
substitute: Effect.fn("Substitution.substitute")(function* (input) {
|
||||
const missing = input.missing ?? "error"
|
||||
const text = input.text.replace(/\{env:([^}]+)\}/g, (_, varName) => {
|
||||
return input.env?.[varName] ?? process.env[varName] ?? ""
|
||||
})
|
||||
|
||||
const fileMatches = Array.from(text.matchAll(/\{file:[^}]+\}/g))
|
||||
if (!fileMatches.length) return text
|
||||
|
||||
const configDir = input.type === "path" ? path.dirname(input.path) : input.dir
|
||||
const configSource = input.type === "path" ? input.path : input.source
|
||||
let out = ""
|
||||
let cursor = 0
|
||||
|
||||
for (const match of fileMatches) {
|
||||
const token = match[0]
|
||||
const index = match.index!
|
||||
out += text.slice(cursor, index)
|
||||
|
||||
const lineStart = text.lastIndexOf("\n", index - 1) + 1
|
||||
const prefix = text.slice(lineStart, index).trimStart()
|
||||
if (prefix.startsWith("//")) {
|
||||
out += token
|
||||
cursor = index + token.length
|
||||
continue
|
||||
}
|
||||
|
||||
const reference = token.replace(/^\{file:/, "").replace(/\}$/, "")
|
||||
const filepath = reference.startsWith("~/") ? path.join(os.homedir(), reference.slice(2)) : reference
|
||||
const resolved = path.isAbsolute(filepath) ? filepath : path.resolve(configDir, filepath)
|
||||
const content = yield* fs.readFileString(resolved).pipe(
|
||||
Effect.catch((cause) => {
|
||||
if (missing === "empty") return Effect.succeed("")
|
||||
return Effect.fail(new FileReferenceError({ source: configSource, token, resolved, cause }))
|
||||
}),
|
||||
)
|
||||
|
||||
out += JSON.stringify(content.trim()).slice(1, -1)
|
||||
cursor = index + token.length
|
||||
}
|
||||
|
||||
out += text.slice(cursor)
|
||||
return out
|
||||
}),
|
||||
})
|
||||
}),
|
||||
)
|
||||
|
||||
export const defaultLayer = layer.pipe(Layer.provide(AppFileSystem.defaultLayer))
|
||||
@@ -0,0 +1,161 @@
|
||||
import { describe, expect } from "bun:test"
|
||||
import path from "path"
|
||||
import { Effect, Layer } from "effect"
|
||||
import { HttpClient, HttpClientResponse } from "effect/unstable/http"
|
||||
import { AppFileSystem } from "@opencode-ai/core/filesystem"
|
||||
import { Global } from "@opencode-ai/core/global"
|
||||
import { Substitution } from "@opencode-ai/core/substitution"
|
||||
import { AuthWellKnown } from "@opencode-ai/core/auth-well-known"
|
||||
import { tmpdir } from "./fixture/tmpdir"
|
||||
import { testEffect } from "./lib/effect"
|
||||
|
||||
const it = testEffect(Layer.empty)
|
||||
|
||||
const unexpectedHttpClient = HttpClient.make((request) => Effect.die(`unexpected http request: ${request.url}`))
|
||||
|
||||
const withAuthWellKnown = <A, E, R>(
|
||||
dir: string,
|
||||
effect: Effect.Effect<A, E, R | AuthWellKnown.Service>,
|
||||
client = unexpectedHttpClient,
|
||||
) =>
|
||||
effect.pipe(
|
||||
Effect.provide(AuthWellKnown.layer),
|
||||
Effect.provide(AppFileSystem.defaultLayer),
|
||||
Effect.provide(Global.layerWith({ data: dir })),
|
||||
Effect.provide(Layer.succeed(HttpClient.HttpClient, client)),
|
||||
Effect.provide(Substitution.defaultLayer),
|
||||
)
|
||||
|
||||
const wellKnownConfigClient = HttpClient.make((request) => {
|
||||
if (request.url === "https://example.com/.well-known/opencode") {
|
||||
return Effect.succeed(
|
||||
HttpClientResponse.fromWeb(
|
||||
request,
|
||||
Response.json({
|
||||
config: { instructions: ["local"] },
|
||||
remote_config: {
|
||||
url: "https://remote.example.com/config",
|
||||
headers: {
|
||||
authorization: "Bearer {env:TEST_TOKEN}",
|
||||
},
|
||||
},
|
||||
}),
|
||||
),
|
||||
)
|
||||
}
|
||||
if (request.url === "https://remote.example.com/config") {
|
||||
expect(request.headers.authorization).toBe("Bearer secret")
|
||||
return Effect.succeed(HttpClientResponse.fromWeb(request, Response.json({ model: "remote/model" })))
|
||||
}
|
||||
return Effect.succeed(HttpClientResponse.fromWeb(request, new Response(null, { status: 404 })))
|
||||
})
|
||||
|
||||
describe("AuthWellKnown", () => {
|
||||
it.live("stores well-known credentials", () =>
|
||||
Effect.gen(function* () {
|
||||
const tmp = yield* Effect.acquireRelease(
|
||||
Effect.promise(() => tmpdir()),
|
||||
(tmp) => Effect.promise(() => tmp[Symbol.asyncDispose]()),
|
||||
)
|
||||
|
||||
yield* withAuthWellKnown(
|
||||
tmp.path,
|
||||
Effect.gen(function* () {
|
||||
const auth = yield* AuthWellKnown.Service
|
||||
yield* auth.set("https://example.com/", new AuthWellKnown.Entry({ key: "TEST_TOKEN", token: "secret" }))
|
||||
}),
|
||||
)
|
||||
|
||||
expect(yield* Effect.promise(() => Bun.file(path.join(tmp.path, "well-known.json")).json())).toEqual({
|
||||
"https://example.com": {
|
||||
key: "TEST_TOKEN",
|
||||
token: "secret",
|
||||
},
|
||||
})
|
||||
}),
|
||||
)
|
||||
|
||||
it.live("migrates legacy well-known auth records", () =>
|
||||
Effect.gen(function* () {
|
||||
const tmp = yield* Effect.acquireRelease(
|
||||
Effect.promise(() => tmpdir()),
|
||||
(tmp) => Effect.promise(() => tmp[Symbol.asyncDispose]()),
|
||||
)
|
||||
yield* Effect.promise(() =>
|
||||
Bun.write(
|
||||
path.join(tmp.path, "auth.json"),
|
||||
JSON.stringify({
|
||||
"https://example.com": {
|
||||
type: "wellknown",
|
||||
key: "TEST_TOKEN",
|
||||
token: "secret",
|
||||
},
|
||||
}),
|
||||
),
|
||||
)
|
||||
|
||||
const entry = yield* withAuthWellKnown(
|
||||
tmp.path,
|
||||
Effect.gen(function* () {
|
||||
const auth = yield* AuthWellKnown.Service
|
||||
return yield* auth.get("https://example.com/")
|
||||
}),
|
||||
)
|
||||
|
||||
expect(entry).toEqual({
|
||||
key: "TEST_TOKEN",
|
||||
token: "secret",
|
||||
})
|
||||
expect(yield* Effect.promise(() => Bun.file(path.join(tmp.path, "well-known.json")).json())).toEqual({
|
||||
"https://example.com": {
|
||||
key: "TEST_TOKEN",
|
||||
token: "secret",
|
||||
},
|
||||
})
|
||||
}),
|
||||
)
|
||||
|
||||
it.live("loads config documents", () =>
|
||||
Effect.gen(function* () {
|
||||
const tmp = yield* Effect.acquireRelease(
|
||||
Effect.promise(() => tmpdir()),
|
||||
(tmp) => Effect.promise(() => tmp[Symbol.asyncDispose]()),
|
||||
)
|
||||
yield* Effect.promise(() =>
|
||||
Bun.write(
|
||||
path.join(tmp.path, "well-known.json"),
|
||||
JSON.stringify({
|
||||
"https://example.com": {
|
||||
key: "TEST_TOKEN",
|
||||
token: "secret",
|
||||
},
|
||||
}),
|
||||
),
|
||||
)
|
||||
|
||||
const result = yield* withAuthWellKnown(
|
||||
tmp.path,
|
||||
Effect.gen(function* () {
|
||||
const auth = yield* AuthWellKnown.Service
|
||||
return yield* auth.configs()
|
||||
}),
|
||||
wellKnownConfigClient,
|
||||
)
|
||||
|
||||
expect(result).toEqual([
|
||||
{
|
||||
url: "https://example.com",
|
||||
source: "https://example.com/.well-known/opencode",
|
||||
dir: "https://example.com/.well-known",
|
||||
content: { instructions: ["local"] },
|
||||
},
|
||||
{
|
||||
url: "https://remote.example.com/config",
|
||||
source: "https://remote.example.com/config",
|
||||
dir: "https://remote.example.com",
|
||||
content: { model: "remote/model" },
|
||||
},
|
||||
])
|
||||
}),
|
||||
)
|
||||
})
|
||||
@@ -0,0 +1,49 @@
|
||||
import { describe, expect } from "bun:test"
|
||||
import { Effect, Layer } from "effect"
|
||||
import { AppFileSystem } from "@opencode-ai/core/filesystem"
|
||||
import { Global } from "@opencode-ai/core/global"
|
||||
import { AuthV2 } from "@opencode-ai/core/auth"
|
||||
import { tmpdir } from "./fixture/tmpdir"
|
||||
import { testEffect } from "./lib/effect"
|
||||
|
||||
const it = testEffect(Layer.empty)
|
||||
|
||||
const withAuth = <A, E, R>(dir: string, effect: Effect.Effect<A, E, R | AuthV2.Service>) =>
|
||||
effect.pipe(
|
||||
Effect.provide(AuthV2.layer),
|
||||
Effect.provide(AppFileSystem.defaultLayer),
|
||||
Effect.provide(Global.layerWith({ data: dir })),
|
||||
)
|
||||
|
||||
describe("AuthV2", () => {
|
||||
it.live("stores api credentials", () =>
|
||||
Effect.gen(function* () {
|
||||
const tmp = yield* Effect.acquireRelease(
|
||||
Effect.promise(() => tmpdir()),
|
||||
(tmp) => Effect.promise(() => tmp[Symbol.asyncDispose]()),
|
||||
)
|
||||
|
||||
const account = yield* withAuth(
|
||||
tmp.path,
|
||||
Effect.gen(function* () {
|
||||
const auth = yield* AuthV2.Service
|
||||
return yield* auth.create({
|
||||
serviceID: AuthV2.ServiceID.make("anthropic"),
|
||||
credential: new AuthV2.ApiKeyCredential({ type: "api", key: "sk-test" }),
|
||||
})
|
||||
}),
|
||||
)
|
||||
|
||||
const active = yield* withAuth(
|
||||
tmp.path,
|
||||
Effect.gen(function* () {
|
||||
const auth = yield* AuthV2.Service
|
||||
return yield* auth.active(AuthV2.ServiceID.make("anthropic"))
|
||||
}),
|
||||
)
|
||||
|
||||
expect(active?.id).toBe(account.id)
|
||||
expect(active?.credential).toEqual({ type: "api", key: "sk-test" })
|
||||
}),
|
||||
)
|
||||
})
|
||||
@@ -4,7 +4,7 @@ import { HttpClient, HttpClientResponse } from "effect/unstable/http"
|
||||
import { AppFileSystem } from "@opencode-ai/core/filesystem"
|
||||
import { Flag } from "@opencode-ai/core/flag/flag"
|
||||
import { Global } from "@opencode-ai/core/global"
|
||||
import { ModelsDev } from "@opencode-ai/core/models-dev"
|
||||
import { ModelsDev } from "@opencode-ai/core/models"
|
||||
import { it } from "./lib/effect"
|
||||
import { rm, writeFile, utimes, mkdir } from "fs/promises"
|
||||
import path from "path"
|
||||
@@ -136,14 +136,14 @@ describe("ModelsDev Service", () => {
|
||||
}),
|
||||
)
|
||||
|
||||
it.live("get() returns empty catalog when disk empty, fetch disabled, and no bundled snapshot is injected", () =>
|
||||
it.live("get() returns bundled snapshot when disk empty and fetch disabled", () =>
|
||||
Effect.gen(function* () {
|
||||
const state = yield* Ref.make(initialState)
|
||||
const result = yield* provided(
|
||||
state,
|
||||
ModelsDev.Service.use((s) => s.get()),
|
||||
)
|
||||
expect(result).toEqual({})
|
||||
expect(Object.keys(result).length).toBeGreaterThan(0)
|
||||
const final = yield* Ref.get(state)
|
||||
expect(final.calls).toEqual([])
|
||||
}),
|
||||
|
||||
@@ -6,6 +6,8 @@ import { initLogging } from "./logging"
|
||||
const logger = initLogging()
|
||||
const { autoUpdater } = pkg
|
||||
|
||||
let downloadedUpdateVersion: string | undefined
|
||||
|
||||
export function setupAutoUpdater() {
|
||||
if (!UPDATER_ENABLED) return
|
||||
autoUpdater.logger = logger
|
||||
@@ -24,6 +26,12 @@ export function setupAutoUpdater() {
|
||||
|
||||
export async function checkUpdate() {
|
||||
if (!UPDATER_ENABLED) return { updateAvailable: false }
|
||||
if (downloadedUpdateVersion) {
|
||||
logger.log("returning cached downloaded update", {
|
||||
version: downloadedUpdateVersion,
|
||||
})
|
||||
return { updateAvailable: true, version: downloadedUpdateVersion }
|
||||
}
|
||||
logger.log("checking for updates", {
|
||||
currentVersion: app.getVersion(),
|
||||
channel: autoUpdater.channel,
|
||||
@@ -49,6 +57,7 @@ export async function checkUpdate() {
|
||||
logger.log("update available", { version })
|
||||
await autoUpdater.downloadUpdate()
|
||||
logger.log("update download completed", { version })
|
||||
downloadedUpdateVersion = version
|
||||
return { updateAvailable: true, version }
|
||||
} catch (error) {
|
||||
logger.error("update check failed", error)
|
||||
@@ -57,15 +66,14 @@ export async function checkUpdate() {
|
||||
}
|
||||
|
||||
export async function installUpdate(killSidecar: () => Promise<void>) {
|
||||
const result = await checkUpdate()
|
||||
if (!result.updateAvailable) {
|
||||
if (!downloadedUpdateVersion) {
|
||||
logger.log("install update skipped", {
|
||||
reason: result.failed ? "update check failed" : "no update available",
|
||||
reason: "no downloaded update ready",
|
||||
})
|
||||
return
|
||||
}
|
||||
logger.log("installing downloaded update", {
|
||||
version: result.version ?? null,
|
||||
version: downloadedUpdateVersion,
|
||||
})
|
||||
await killSidecar()
|
||||
autoUpdater.quitAndInstall()
|
||||
|
||||
@@ -9,6 +9,7 @@
|
||||
<link rel="shortcut icon" href="./favicon-v3.ico" />
|
||||
<link rel="apple-touch-icon" sizes="180x180" href="./apple-touch-icon-v3.png" />
|
||||
<meta name="theme-color" content="#F8F7F7" />
|
||||
<meta name="theme-color" content="#131010" media="(prefers-color-scheme: dark)" />
|
||||
<meta property="og:image" content="./social-share.png" />
|
||||
<meta property="twitter:image" content="./social-share.png" />
|
||||
<script id="oc-theme-preload-script" src="./oc-theme-preload.js"></script>
|
||||
|
||||
@@ -9,6 +9,7 @@
|
||||
<link rel="shortcut icon" href="./favicon-v3.ico" />
|
||||
<link rel="apple-touch-icon" sizes="180x180" href="./apple-touch-icon-v3.png" />
|
||||
<meta name="theme-color" content="#F8F7F7" />
|
||||
<meta name="theme-color" content="#131010" media="(prefers-color-scheme: dark)" />
|
||||
<meta property="og:image" content="./social-share.png" />
|
||||
<meta property="twitter:image" content="./social-share.png" />
|
||||
<script id="oc-theme-preload-script" src="./oc-theme-preload.js"></script>
|
||||
|
||||
@@ -25,6 +25,7 @@ export default createHandler(() => (
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<title>OpenCode</title>
|
||||
<meta name="theme-color" content="#F8F7F7" />
|
||||
<meta name="theme-color" content="#131010" media="(prefers-color-scheme: dark)" />
|
||||
{assets}
|
||||
</head>
|
||||
<body class="antialiased overscroll-none text-12-regular">
|
||||
|
||||
@@ -3,6 +3,8 @@ dist
|
||||
dist-*
|
||||
gen
|
||||
app.log
|
||||
src/provider/models-snapshot.js
|
||||
src/provider/models-snapshot.d.ts
|
||||
script/build-*.ts
|
||||
temporary-*.md
|
||||
.artifacts
|
||||
|
||||
@@ -11,7 +11,7 @@ const dir = path.resolve(__dirname, "..")
|
||||
|
||||
process.chdir(dir)
|
||||
|
||||
const generated = await import("./generate.ts")
|
||||
await import("./generate.ts")
|
||||
|
||||
// Load migrations from migration directories
|
||||
const migrationDirs = (
|
||||
@@ -52,7 +52,6 @@ await Bun.build({
|
||||
external: ["jsonc-parser", "@lydell/node-pty"],
|
||||
define: {
|
||||
OPENCODE_MIGRATIONS: JSON.stringify(migrations),
|
||||
OPENCODE_MODELS_DEV: generated.modelsData,
|
||||
OPENCODE_CHANNEL: `'${Script.channel}'`,
|
||||
},
|
||||
files: {
|
||||
|
||||
@@ -12,7 +12,7 @@ const dir = path.resolve(__dirname, "..")
|
||||
|
||||
process.chdir(dir)
|
||||
|
||||
const generated = await import("./generate.ts")
|
||||
await import("./generate.ts")
|
||||
|
||||
import { Script } from "@opencode-ai/script"
|
||||
import pkg from "../package.json"
|
||||
@@ -218,7 +218,6 @@ for (const item of targets) {
|
||||
define: {
|
||||
OPENCODE_VERSION: `'${Script.version}'`,
|
||||
OPENCODE_MIGRATIONS: JSON.stringify(migrations),
|
||||
OPENCODE_MODELS_DEV: generated.modelsData,
|
||||
OTUI_TREE_SITTER_WORKER_PATH: bunfsRoot + workerRelativePath,
|
||||
OPENCODE_WORKER_PATH: workerPath,
|
||||
OPENCODE_CHANNEL: `'${Script.channel}'`,
|
||||
|
||||
@@ -8,7 +8,16 @@ const dir = path.resolve(__dirname, "..")
|
||||
process.chdir(dir)
|
||||
|
||||
const modelsUrl = process.env.OPENCODE_MODELS_URL || "https://models.dev"
|
||||
export const modelsData = process.env.MODELS_DEV_API_JSON
|
||||
// Fetch and generate models.dev snapshot
|
||||
const modelsData = process.env.MODELS_DEV_API_JSON
|
||||
? await Bun.file(process.env.MODELS_DEV_API_JSON).text()
|
||||
: await fetch(`${modelsUrl}/api.json`).then((x) => x.text())
|
||||
console.log("Loaded models.dev snapshot")
|
||||
await Bun.write(
|
||||
path.join(dir, "../core/src/models-snapshot.js"),
|
||||
`// @ts-nocheck\n// Auto-generated by build.ts - do not edit\nexport const snapshot = ${modelsData}\n`,
|
||||
)
|
||||
await Bun.write(
|
||||
path.join(dir, "../core/src/models-snapshot.d.ts"),
|
||||
`// Auto-generated by build.ts - do not edit\nexport declare const snapshot: Record<string, unknown>\n`,
|
||||
)
|
||||
console.log("Generated models-snapshot.js")
|
||||
|
||||
@@ -19,7 +19,7 @@ import type {
|
||||
import { UI } from "../ui"
|
||||
import { cmd } from "./cmd"
|
||||
import { effectCmd } from "../effect-cmd"
|
||||
import { ModelsDev } from "@opencode-ai/core/models-dev"
|
||||
import { ModelsDev } from "@opencode-ai/core/models"
|
||||
import { InstanceRef } from "@/effect/instance-ref"
|
||||
import { SessionShare } from "@/share/session"
|
||||
import { Session } from "@/session/session"
|
||||
|
||||
@@ -2,7 +2,7 @@ import { EOL } from "os"
|
||||
import { Effect } from "effect"
|
||||
import { Provider } from "@/provider/provider"
|
||||
import { ProviderID } from "../../provider/schema"
|
||||
import { ModelsDev } from "@opencode-ai/core/models-dev"
|
||||
import { ModelsDev } from "@opencode-ai/core/models"
|
||||
import { effectCmd, fail } from "../effect-cmd"
|
||||
import { UI } from "../ui"
|
||||
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
import { Auth } from "../../auth"
|
||||
import { AuthWellKnown } from "@opencode-ai/core/auth-well-known"
|
||||
import { cmd } from "./cmd"
|
||||
import { CliError, effectCmd, fail } from "../effect-cmd"
|
||||
import { UI } from "../ui"
|
||||
import * as Prompt from "../effect/prompt"
|
||||
import { ModelsDev } from "@opencode-ai/core/models-dev"
|
||||
import { ModelsDev } from "@opencode-ai/core/models"
|
||||
|
||||
import { map, pipe, sortBy, values } from "remeda"
|
||||
import path from "path"
|
||||
@@ -252,6 +253,7 @@ export const ProvidersListCommand = effectCmd({
|
||||
instance: false,
|
||||
handler: Effect.fn("Cli.providers.list")(function* (_args) {
|
||||
const authSvc = yield* Auth.Service
|
||||
const authWellKnown = yield* AuthWellKnown.Service
|
||||
const modelsDev = yield* ModelsDev.Service
|
||||
|
||||
UI.empty()
|
||||
@@ -259,7 +261,8 @@ export const ProvidersListCommand = effectCmd({
|
||||
const homedir = os.homedir()
|
||||
const displayPath = authPath.startsWith(homedir) ? authPath.replace(homedir, "~") : authPath
|
||||
yield* Prompt.intro(`Credentials ${UI.Style.TEXT_DIM}${displayPath}`)
|
||||
const results = Object.entries(yield* Effect.orDie(authSvc.all()))
|
||||
const results = Object.entries(yield* Effect.orDie(authSvc.all())).filter(([, result]) => result.type !== "wellknown")
|
||||
const wellKnownResults = Object.entries(yield* Effect.orDie(authWellKnown.all()))
|
||||
const database = yield* modelsDev.get()
|
||||
|
||||
for (const [providerID, result] of results) {
|
||||
@@ -267,7 +270,11 @@ export const ProvidersListCommand = effectCmd({
|
||||
yield* Prompt.log.info(`${name} ${UI.Style.TEXT_DIM}${result.type}`)
|
||||
}
|
||||
|
||||
yield* Prompt.outro(`${results.length} credentials`)
|
||||
for (const [url] of wellKnownResults) {
|
||||
yield* Prompt.log.info(`${url} ${UI.Style.TEXT_DIM}wellknown`)
|
||||
}
|
||||
|
||||
yield* Prompt.outro(`${results.length + wellKnownResults.length} credentials`)
|
||||
|
||||
const activeEnvVars: Array<{ provider: string; envVar: string }> = []
|
||||
|
||||
@@ -316,19 +323,19 @@ export const ProvidersLoginCommand = effectCmd({
|
||||
}),
|
||||
handler: Effect.fn("Cli.providers.login")(function* (args) {
|
||||
const authSvc = yield* Auth.Service
|
||||
const authWellKnown = yield* AuthWellKnown.Service
|
||||
|
||||
UI.empty()
|
||||
yield* Prompt.intro("Add credential")
|
||||
if (args.url) {
|
||||
const url = args.url.replace(/\/+$/, "")
|
||||
const wellknown = (yield* cliTry(`Failed to load auth provider metadata from ${url}: `, () =>
|
||||
fetch(`${url}/.well-known/opencode`).then((x) => x.json()),
|
||||
)) as {
|
||||
auth: { command: string[]; env: string }
|
||||
}
|
||||
const wellknown = yield* authWellKnown.metadata(url).pipe(
|
||||
Effect.mapError((error) => new CliError({ message: `Failed to load auth provider metadata from ${url}: ${errorMessage(error)}` })),
|
||||
)
|
||||
if (!wellknown.auth) return yield* fail(`Auth provider metadata from ${url} is missing auth configuration`)
|
||||
yield* Prompt.log.info(`Running \`${wellknown.auth.command.join(" ")}\``)
|
||||
const abort = new AbortController()
|
||||
const proc = Process.spawn(wellknown.auth.command, { stdout: "pipe", stderr: "inherit", abort: abort.signal })
|
||||
const proc = Process.spawn([...wellknown.auth.command], { stdout: "pipe", stderr: "inherit", abort: abort.signal })
|
||||
if (!proc.stdout) {
|
||||
yield* Prompt.log.error("Failed")
|
||||
yield* Prompt.outro("Done")
|
||||
@@ -342,7 +349,7 @@ export const ProvidersLoginCommand = effectCmd({
|
||||
yield* Prompt.outro("Done")
|
||||
return
|
||||
}
|
||||
yield* Effect.orDie(authSvc.set(url, { type: "wellknown", key: wellknown.auth.env, token: token.trim() }))
|
||||
yield* Effect.orDie(authWellKnown.set(url, new AuthWellKnown.Entry({ key: wellknown.auth.env, token: token.trim() })))
|
||||
yield* Prompt.log.success("Logged into " + url)
|
||||
yield* Prompt.outro("Done")
|
||||
return
|
||||
@@ -492,10 +499,20 @@ export const ProvidersLogoutCommand = effectCmd({
|
||||
instance: false,
|
||||
handler: Effect.fn("Cli.providers.logout")(function* (_args) {
|
||||
const authSvc = yield* Auth.Service
|
||||
const authWellKnown = yield* AuthWellKnown.Service
|
||||
const modelsDev = yield* ModelsDev.Service
|
||||
|
||||
UI.empty()
|
||||
const credentials: Array<[string, Auth.Info]> = Object.entries(yield* Effect.orDie(authSvc.all()))
|
||||
const credentials = [
|
||||
...Object.entries(yield* Effect.orDie(authSvc.all()))
|
||||
.filter(([, value]) => value.type !== "wellknown")
|
||||
.map(([key, value]) => ({ key, type: value.type, auth: "provider" as const })),
|
||||
...Object.keys(yield* Effect.orDie(authWellKnown.all())).map((key) => ({
|
||||
key,
|
||||
type: "wellknown" as const,
|
||||
auth: "wellknown" as const,
|
||||
})),
|
||||
]
|
||||
yield* Prompt.intro("Remove credential")
|
||||
if (credentials.length === 0) {
|
||||
yield* Prompt.log.error("No credentials found")
|
||||
@@ -504,12 +521,15 @@ export const ProvidersLogoutCommand = effectCmd({
|
||||
const database = yield* modelsDev.get()
|
||||
const selected = yield* Prompt.select({
|
||||
message: "Select provider",
|
||||
options: credentials.map(([key, value]) => ({
|
||||
label: (database[key]?.name || key) + UI.Style.TEXT_DIM + " (" + value.type + ")",
|
||||
value: key,
|
||||
options: credentials.map((item, index) => ({
|
||||
label: (database[item.key]?.name || item.key) + UI.Style.TEXT_DIM + " (" + item.type + ")",
|
||||
value: index,
|
||||
})),
|
||||
})
|
||||
yield* Effect.orDie(authSvc.remove(yield* promptValue(selected)))
|
||||
const credential = credentials[yield* promptValue(selected)]
|
||||
if (!credential) return
|
||||
if (credential.auth === "wellknown") yield* Effect.orDie(authWellKnown.remove(credential.key))
|
||||
else yield* Effect.orDie(authSvc.remove(credential.key))
|
||||
yield* Prompt.outro("Logout successful")
|
||||
}),
|
||||
})
|
||||
|
||||
@@ -188,7 +188,6 @@ export const Definitions = {
|
||||
"dialog.select.home": keybind("home", "Move to first dialog item"),
|
||||
"dialog.select.end": keybind("end", "Move to last dialog item"),
|
||||
"dialog.select.submit": keybind("return", "Submit selected dialog item"),
|
||||
"dialog.prompt.submit": keybind("return", "Submit dialog prompt"),
|
||||
"dialog.mcp.toggle": keybind("space", "Toggle MCP in MCP dialog"),
|
||||
"prompt.autocomplete.prev": keybind("up,ctrl+p", "Move to previous autocomplete item"),
|
||||
"prompt.autocomplete.next": keybind("down,ctrl+n", "Move to next autocomplete item"),
|
||||
|
||||
@@ -12,6 +12,7 @@ import { Flag } from "@opencode-ai/core/flag/flag"
|
||||
import { isRecord } from "@/util/record"
|
||||
import { Global } from "@opencode-ai/core/global"
|
||||
import { AppFileSystem } from "@opencode-ai/core/filesystem"
|
||||
import { Substitution } from "@opencode-ai/core/substitution"
|
||||
import { CurrentWorkingDirectory } from "./cwd"
|
||||
import { ConfigPlugin } from "@/config/plugin"
|
||||
import { TuiKeybind } from "./keybind"
|
||||
@@ -19,7 +20,6 @@ import { InstallationLocal, InstallationVersion } from "@opencode-ai/core/instal
|
||||
import { makeRuntime } from "@opencode-ai/core/effect/runtime"
|
||||
import { Filesystem } from "@/util/filesystem"
|
||||
import * as Log from "@opencode-ai/core/util/log"
|
||||
import { ConfigVariable } from "@/config/variable"
|
||||
import { Npm } from "@opencode-ai/core/npm"
|
||||
import type { DeepMutable } from "@opencode-ai/core/schema"
|
||||
import type { TuiAttentionSoundName } from "@opencode-ai/plugin/tui"
|
||||
@@ -98,6 +98,7 @@ function dropUnknownKeybinds(input: Record<string, unknown>, configFilepath: str
|
||||
|
||||
const loadState = Effect.fn("TuiConfig.loadState")(function* (ctx: { directory: string }) {
|
||||
const afs = yield* AppFileSystem.Service
|
||||
const substitution = yield* Substitution.Service
|
||||
let appliedOrder = 0
|
||||
|
||||
const resolvePlugins = (config: Info, configFilepath: string): Effect.Effect<Info> =>
|
||||
@@ -112,9 +113,7 @@ const loadState = Effect.fn("TuiConfig.loadState")(function* (ctx: { directory:
|
||||
|
||||
const load = (text: string, configFilepath: string): Effect.Effect<Info> =>
|
||||
Effect.gen(function* () {
|
||||
const expanded = yield* Effect.promise(() =>
|
||||
ConfigVariable.substitute({ text, type: "path", path: configFilepath, missing: "empty" }),
|
||||
)
|
||||
const expanded = yield* substitution.substitute({ text, type: "path", path: configFilepath, missing: "empty" }).pipe(Effect.orDie)
|
||||
const data = ConfigParse.jsonc(expanded, configFilepath)
|
||||
if (!isRecord(data)) return {} as Info
|
||||
// Flatten a nested "tui" key so users who wrote `{ "tui": { ... } }` inside tui.json
|
||||
@@ -295,7 +294,11 @@ export const layer = Layer.effect(
|
||||
}).pipe(Effect.withSpan("TuiConfig.layer")),
|
||||
)
|
||||
|
||||
export const defaultLayer = layer.pipe(Layer.provide(Npm.defaultLayer), Layer.provide(AppFileSystem.defaultLayer))
|
||||
export const defaultLayer = layer.pipe(
|
||||
Layer.provide(Npm.defaultLayer),
|
||||
Layer.provide(AppFileSystem.defaultLayer),
|
||||
Layer.provide(Substitution.defaultLayer),
|
||||
)
|
||||
|
||||
const { runPromise } = makeRuntime(Service, defaultLayer)
|
||||
|
||||
|
||||
@@ -1,10 +1,8 @@
|
||||
import { TextareaRenderable, TextAttributes } from "@opentui/core"
|
||||
import { useTheme } from "../context/theme"
|
||||
import { useDialog, type DialogContext } from "./dialog"
|
||||
import { Show, createEffect, createSignal, onMount, type JSX } from "solid-js"
|
||||
import { Show, createEffect, onMount, type JSX } from "solid-js"
|
||||
import { Spinner } from "../component/spinner"
|
||||
import { useTuiConfig } from "../context/tui-config"
|
||||
import { useBindings, useCommandShortcut } from "../keymap"
|
||||
|
||||
export type DialogPromptProps = {
|
||||
title: string
|
||||
@@ -20,32 +18,8 @@ export type DialogPromptProps = {
|
||||
export function DialogPrompt(props: DialogPromptProps) {
|
||||
const dialog = useDialog()
|
||||
const { theme } = useTheme()
|
||||
const tuiConfig = useTuiConfig()
|
||||
const submitShortcut = useCommandShortcut("dialog.prompt.submit")
|
||||
const [textareaTarget, setTextareaTarget] = createSignal<TextareaRenderable>()
|
||||
let textarea: TextareaRenderable
|
||||
|
||||
function confirm() {
|
||||
if (props.busy) return
|
||||
props.onConfirm?.(textarea.plainText)
|
||||
}
|
||||
|
||||
useBindings(() => ({
|
||||
target: textareaTarget,
|
||||
enabled: textareaTarget() !== undefined && !props.busy,
|
||||
// Dialog form semantics must win over the global managed textarea input layer.
|
||||
priority: 1,
|
||||
commands: [
|
||||
{
|
||||
name: "dialog.prompt.submit",
|
||||
title: "Submit dialog prompt",
|
||||
category: "Dialog",
|
||||
run: confirm,
|
||||
},
|
||||
],
|
||||
bindings: tuiConfig.keybinds.gather("dialog.prompt", ["dialog.prompt.submit"]),
|
||||
}))
|
||||
|
||||
onMount(() => {
|
||||
dialog.setSize("medium")
|
||||
setTimeout(() => {
|
||||
@@ -85,10 +59,13 @@ export function DialogPrompt(props: DialogPromptProps) {
|
||||
<box gap={1}>
|
||||
{props.description}
|
||||
<textarea
|
||||
onSubmit={() => {
|
||||
if (props.busy) return
|
||||
props.onConfirm?.(textarea.plainText)
|
||||
}}
|
||||
height={3}
|
||||
ref={(val: TextareaRenderable) => {
|
||||
textarea = val
|
||||
setTextareaTarget(val)
|
||||
}}
|
||||
initialValue={props.value}
|
||||
placeholder={props.placeholder ?? "Enter text"}
|
||||
@@ -103,11 +80,9 @@ export function DialogPrompt(props: DialogPromptProps) {
|
||||
</box>
|
||||
<box paddingBottom={1} gap={1} flexDirection="row">
|
||||
<Show when={!props.busy} fallback={<text fg={theme.textMuted}>processing...</text>}>
|
||||
<Show when={submitShortcut()}>
|
||||
<text fg={theme.text}>
|
||||
{submitShortcut()} <span style={{ fg: theme.textMuted }}>submit</span>
|
||||
</text>
|
||||
</Show>
|
||||
<text fg={theme.text}>
|
||||
enter <span style={{ fg: theme.textMuted }}>submit</span>
|
||||
</text>
|
||||
</Show>
|
||||
</box>
|
||||
</box>
|
||||
|
||||
@@ -7,7 +7,8 @@ import { Global } from "@opencode-ai/core/global"
|
||||
import fsNode from "fs/promises"
|
||||
import { NamedError } from "@opencode-ai/core/util/error"
|
||||
import { Flag } from "@opencode-ai/core/flag/flag"
|
||||
import { Auth } from "../auth"
|
||||
import { AuthWellKnown } from "@opencode-ai/core/auth-well-known"
|
||||
import { Substitution } from "@opencode-ai/core/substitution"
|
||||
import { Env } from "../env"
|
||||
import { applyEdits, modify } from "jsonc-parser"
|
||||
import { InstallationLocal, InstallationVersion } from "@opencode-ai/core/installation/version"
|
||||
@@ -38,7 +39,6 @@ import { ConfigProvider } from "./provider"
|
||||
import { ConfigReference } from "./reference"
|
||||
import { ConfigServer } from "./server"
|
||||
import { ConfigSkills } from "./skills"
|
||||
import { ConfigVariable } from "./variable"
|
||||
import { Npm } from "@opencode-ai/core/npm"
|
||||
|
||||
const log = Log.create({ service: "config" })
|
||||
@@ -69,36 +69,6 @@ function normalizeLoadedConfig(data: unknown, source: string) {
|
||||
return copy
|
||||
}
|
||||
|
||||
async function substituteWellKnownRemoteConfig(input: { value: unknown; dir: string; source: string }) {
|
||||
if (!isRecord(input.value) || typeof input.value.url !== "string") return
|
||||
|
||||
const url = await ConfigVariable.substitute({
|
||||
text: input.value.url,
|
||||
type: "virtual",
|
||||
dir: input.dir,
|
||||
source: input.source,
|
||||
})
|
||||
const headers = isRecord(input.value.headers)
|
||||
? Object.fromEntries(
|
||||
await Promise.all(
|
||||
Object.entries(input.value.headers)
|
||||
.filter((entry): entry is [string, string] => typeof entry[1] === "string")
|
||||
.map(async ([key, value]) => [
|
||||
key,
|
||||
await ConfigVariable.substitute({
|
||||
text: value,
|
||||
type: "virtual",
|
||||
dir: input.dir,
|
||||
source: input.source,
|
||||
}),
|
||||
]),
|
||||
),
|
||||
)
|
||||
: undefined
|
||||
|
||||
return { url, headers }
|
||||
}
|
||||
|
||||
async function resolveLoadedPlugins<T extends { plugin?: ConfigPlugin.Spec[] }>(config: T, filepath: string) {
|
||||
if (!config.plugin) return config
|
||||
for (let i = 0; i < config.plugin.length; i++) {
|
||||
@@ -365,7 +335,8 @@ export const layer = Layer.effect(
|
||||
Service,
|
||||
Effect.gen(function* () {
|
||||
const fs = yield* AppFileSystem.Service
|
||||
const authSvc = yield* Auth.Service
|
||||
const authWellKnown = yield* AuthWellKnown.Service
|
||||
const substitution = yield* Substitution.Service
|
||||
const accountSvc = yield* Account.Service
|
||||
const env = yield* Env.Service
|
||||
const npmSvc = yield* Npm.Service
|
||||
@@ -377,11 +348,9 @@ export const layer = Layer.effect(
|
||||
options: { path: string } | { dir: string; source: string },
|
||||
) {
|
||||
const source = "path" in options ? options.path : options.source
|
||||
const expanded = yield* Effect.promise(() =>
|
||||
ConfigVariable.substitute(
|
||||
"path" in options ? { text, type: "path", path: options.path } : { text, type: "virtual", ...options },
|
||||
),
|
||||
)
|
||||
const expanded = yield* substitution.substitute(
|
||||
"path" in options ? { text, type: "path", path: options.path } : { text, type: "virtual", ...options },
|
||||
).pipe(Effect.orDie)
|
||||
const parsed = ConfigParse.jsonc(expanded, source)
|
||||
const data = ConfigParse.schema(Info, normalizeLoadedConfig(parsed, source), source)
|
||||
if (!("path" in options)) return data
|
||||
@@ -471,8 +440,6 @@ export const layer = Layer.effect(
|
||||
|
||||
const loadInstanceState = Effect.fn("Config.loadInstanceState")(
|
||||
function* (ctx: InstanceContext) {
|
||||
const auth = yield* authSvc.all().pipe(Effect.orDie)
|
||||
|
||||
let result: Info = {}
|
||||
const consoleManagedProviders = new Set<string>()
|
||||
let activeOrgName: string | undefined
|
||||
@@ -510,46 +477,13 @@ export const layer = Layer.effect(
|
||||
return mergePluginOrigins(source, next.plugin, kind)
|
||||
}
|
||||
|
||||
for (const [key, value] of Object.entries(auth)) {
|
||||
if (value.type === "wellknown") {
|
||||
const url = key.replace(/\/+$/, "")
|
||||
process.env[value.key] = value.token
|
||||
log.debug("fetching remote config", { url: `${url}/.well-known/opencode` })
|
||||
const response = yield* Effect.promise(() => fetch(`${url}/.well-known/opencode`))
|
||||
if (!response.ok) {
|
||||
throw new Error(`failed to fetch remote config from ${url}: ${response.status}`)
|
||||
}
|
||||
const wellknown = (yield* Effect.promise(() => response.json())) as {
|
||||
config?: Record<string, unknown>
|
||||
remote_config?: unknown
|
||||
}
|
||||
const remote = yield* Effect.promise(() =>
|
||||
substituteWellKnownRemoteConfig({
|
||||
value: wellknown.remote_config,
|
||||
dir: url,
|
||||
source: `${url}/.well-known/opencode`,
|
||||
}),
|
||||
)
|
||||
const fetchedConfig = remote
|
||||
? ((yield* Effect.promise(async () => {
|
||||
log.debug("fetching remote config", { url: remote.url })
|
||||
const response = await fetch(remote.url, { headers: remote.headers })
|
||||
if (!response.ok)
|
||||
throw new Error(`failed to fetch remote config from ${remote.url}: ${response.status}`)
|
||||
const data = await response.json()
|
||||
return isRecord(data) && isRecord(data.config) ? data.config : data
|
||||
})) as Record<string, unknown>)
|
||||
: {}
|
||||
const remoteConfig = mergeConfig(wellknown.config ?? {}, fetchedConfig as Info)
|
||||
if (!remoteConfig.$schema) remoteConfig.$schema = "https://opencode.ai/config.json"
|
||||
const source = `${url}/.well-known/opencode`
|
||||
const next = yield* loadConfig(JSON.stringify(remoteConfig), {
|
||||
dir: path.dirname(source),
|
||||
source,
|
||||
})
|
||||
yield* merge(source, next, "global")
|
||||
log.debug("loaded remote config from well-known", { url })
|
||||
}
|
||||
for (const item of yield* authWellKnown.configs().pipe(Effect.orDie)) {
|
||||
yield* merge(
|
||||
item.source,
|
||||
yield* loadConfig(JSON.stringify(item.content), { dir: item.dir, source: item.source }),
|
||||
"global",
|
||||
)
|
||||
log.debug("loaded well-known config", { url: item.url })
|
||||
}
|
||||
|
||||
const global = yield* getGlobal()
|
||||
@@ -825,7 +759,8 @@ export const defaultLayer = layer.pipe(
|
||||
Layer.provide(EffectFlock.defaultLayer),
|
||||
Layer.provide(AppFileSystem.defaultLayer),
|
||||
Layer.provide(Env.defaultLayer),
|
||||
Layer.provide(Auth.defaultLayer),
|
||||
Layer.provide(AuthWellKnown.defaultLayer),
|
||||
Layer.provide(Substitution.defaultLayer),
|
||||
Layer.provide(Account.defaultLayer),
|
||||
Layer.provide(Npm.defaultLayer),
|
||||
)
|
||||
|
||||
@@ -1,90 +0,0 @@
|
||||
export * as ConfigVariable from "./variable"
|
||||
|
||||
import path from "path"
|
||||
import os from "os"
|
||||
import { Filesystem } from "@/util/filesystem"
|
||||
import { InvalidError } from "./error"
|
||||
|
||||
type ParseSource =
|
||||
| {
|
||||
type: "path"
|
||||
path: string
|
||||
}
|
||||
| {
|
||||
type: "virtual"
|
||||
source: string
|
||||
dir: string
|
||||
}
|
||||
|
||||
type SubstituteInput = ParseSource & {
|
||||
text: string
|
||||
missing?: "error" | "empty"
|
||||
}
|
||||
|
||||
function source(input: ParseSource) {
|
||||
return input.type === "path" ? input.path : input.source
|
||||
}
|
||||
|
||||
function dir(input: ParseSource) {
|
||||
return input.type === "path" ? path.dirname(input.path) : input.dir
|
||||
}
|
||||
|
||||
/** Apply {env:VAR} and {file:path} substitutions to config text. */
|
||||
export async function substitute(input: SubstituteInput) {
|
||||
const missing = input.missing ?? "error"
|
||||
let text = input.text.replace(/\{env:([^}]+)\}/g, (_, varName) => {
|
||||
return process.env[varName] || ""
|
||||
})
|
||||
|
||||
const fileMatches = Array.from(text.matchAll(/\{file:[^}]+\}/g))
|
||||
if (!fileMatches.length) return text
|
||||
|
||||
const configDir = dir(input)
|
||||
const configSource = source(input)
|
||||
let out = ""
|
||||
let cursor = 0
|
||||
|
||||
for (const match of fileMatches) {
|
||||
const token = match[0]
|
||||
const index = match.index!
|
||||
out += text.slice(cursor, index)
|
||||
|
||||
const lineStart = text.lastIndexOf("\n", index - 1) + 1
|
||||
const prefix = text.slice(lineStart, index).trimStart()
|
||||
if (prefix.startsWith("//")) {
|
||||
out += token
|
||||
cursor = index + token.length
|
||||
continue
|
||||
}
|
||||
|
||||
let filePath = token.replace(/^\{file:/, "").replace(/\}$/, "")
|
||||
if (filePath.startsWith("~/")) {
|
||||
filePath = path.join(os.homedir(), filePath.slice(2))
|
||||
}
|
||||
|
||||
const resolvedPath = path.isAbsolute(filePath) ? filePath : path.resolve(configDir, filePath)
|
||||
const fileContent = (
|
||||
await Filesystem.readText(resolvedPath).catch((error: NodeJS.ErrnoException) => {
|
||||
if (missing === "empty") return ""
|
||||
|
||||
const errMsg = `bad file reference: "${token}"`
|
||||
if (error.code === "ENOENT") {
|
||||
throw new InvalidError(
|
||||
{
|
||||
path: configSource,
|
||||
message: errMsg + ` ${resolvedPath} does not exist`,
|
||||
},
|
||||
{ cause: error },
|
||||
)
|
||||
}
|
||||
throw new InvalidError({ path: configSource, message: errMsg }, { cause: error })
|
||||
})
|
||||
).trim()
|
||||
|
||||
out += JSON.stringify(fileContent).slice(1, -1)
|
||||
cursor = index + token.length
|
||||
}
|
||||
|
||||
out += text.slice(cursor)
|
||||
return out
|
||||
}
|
||||
@@ -3,6 +3,7 @@ import { attach } from "./run-service"
|
||||
import * as Observability from "@opencode-ai/core/effect/observability"
|
||||
|
||||
import { AppFileSystem } from "@opencode-ai/core/filesystem"
|
||||
import { AuthWellKnown } from "@opencode-ai/core/auth-well-known"
|
||||
import { Bus } from "@/bus"
|
||||
import { Auth } from "@/auth"
|
||||
import { Account } from "@/account/account"
|
||||
@@ -14,7 +15,7 @@ import { FileWatcher } from "@/file/watcher"
|
||||
import { Storage } from "@/storage/storage"
|
||||
import { Snapshot } from "@/snapshot"
|
||||
import { Plugin } from "@/plugin"
|
||||
import { ModelsDev } from "@opencode-ai/core/models-dev"
|
||||
import { ModelsDev } from "@opencode-ai/core/models"
|
||||
import { Provider } from "@/provider/provider"
|
||||
import { ProviderAuth } from "@/provider/auth"
|
||||
import { Agent } from "@/agent/agent"
|
||||
@@ -62,6 +63,7 @@ import { RuntimeFlags } from "@/effect/runtime-flags"
|
||||
export const AppLayer = Layer.mergeAll(
|
||||
Npm.defaultLayer,
|
||||
AppFileSystem.defaultLayer,
|
||||
AuthWellKnown.defaultLayer,
|
||||
Bus.defaultLayer,
|
||||
Auth.defaultLayer,
|
||||
Account.defaultLayer,
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { Schema } from "effect"
|
||||
|
||||
export { CatalogModelStatus } from "@opencode-ai/core/models-dev"
|
||||
export { CatalogModelStatus } from "@opencode-ai/core/models"
|
||||
|
||||
export const ModelStatus = Schema.Literals(["alpha", "beta", "deprecated", "active"])
|
||||
export type ModelStatus = typeof ModelStatus.Type
|
||||
|
||||
@@ -8,7 +8,7 @@ import { Npm } from "@opencode-ai/core/npm"
|
||||
import { Hash } from "@opencode-ai/core/util/hash"
|
||||
import { Plugin } from "../plugin"
|
||||
import { type LanguageModelV3 } from "@ai-sdk/provider"
|
||||
import * as ModelsDev from "@opencode-ai/core/models-dev"
|
||||
import * as ModelsDev from "@opencode-ai/core/models"
|
||||
import { Auth } from "../auth"
|
||||
import { Env } from "../env"
|
||||
import { InstallationVersion } from "@opencode-ai/core/installation/version"
|
||||
|
||||
@@ -2,7 +2,7 @@ import type { ModelMessage, ToolResultPart } from "ai"
|
||||
import { mergeDeep, unique } from "remeda"
|
||||
import type { JSONSchema7 } from "@ai-sdk/provider"
|
||||
import type * as Provider from "./provider"
|
||||
import type * as ModelsDev from "@opencode-ai/core/models-dev"
|
||||
import type * as ModelsDev from "@opencode-ai/core/models"
|
||||
import { iife } from "@/util/iife"
|
||||
|
||||
type Modality = NonNullable<ModelsDev.Model["modalities"]>["input"][number]
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { ProviderAuth } from "@/provider/auth"
|
||||
import { Config } from "@/config/config"
|
||||
import { ModelsDev } from "@opencode-ai/core/models-dev"
|
||||
import { ModelsDev } from "@opencode-ai/core/models"
|
||||
import { Provider } from "@/provider/provider"
|
||||
import { ProviderID } from "@/provider/schema"
|
||||
import { mapValues } from "remeda"
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { WorkspaceID } from "@/control-plane/schema"
|
||||
import { SessionV2 } from "@/v2/session"
|
||||
import { DateTime, Effect, Schema } from "effect"
|
||||
import { Effect, Schema } from "effect"
|
||||
import { HttpApiBuilder, HttpApiError, HttpApiSchema } from "effect/unstable/httpapi"
|
||||
import { InstanceHttpApi } from "../../api"
|
||||
|
||||
@@ -55,13 +55,7 @@ const sessionCursor = {
|
||||
filters: Pick<SessionCursor, "directory" | "path" | "workspaceID" | "roots" | "start" | "search">,
|
||||
) {
|
||||
return Buffer.from(
|
||||
JSON.stringify({
|
||||
...filters,
|
||||
id: session.id,
|
||||
time: DateTime.toEpochMillis(session.time.updated),
|
||||
order,
|
||||
direction,
|
||||
}),
|
||||
JSON.stringify({ id: session.id, time: session.time.created, order, direction, ...filters }),
|
||||
).toString("base64url")
|
||||
},
|
||||
decode(input: string) {
|
||||
|
||||
@@ -30,7 +30,7 @@ import { InstanceLayer } from "@/project/instance-layer"
|
||||
import { Plugin } from "@/plugin"
|
||||
import { Project } from "@/project/project"
|
||||
import { ProviderAuth } from "@/provider/auth"
|
||||
import { ModelsDev } from "@opencode-ai/core/models-dev"
|
||||
import { ModelsDev } from "@opencode-ai/core/models"
|
||||
import { Provider } from "@/provider/provider"
|
||||
import { Pty } from "@/pty"
|
||||
import { PtyTicket } from "@/pty/ticket"
|
||||
|
||||
@@ -177,8 +177,6 @@ export const layer = Layer.effect(
|
||||
list: Effect.fn("V2Session.list")(function* (input) {
|
||||
const direction = input.cursor?.direction ?? "next"
|
||||
let order = input.order ?? "desc"
|
||||
// This is a load bearing sort, desktop relies on this
|
||||
const sortColumn = SessionTable.time_updated
|
||||
// Query the adjacent rows in reverse, then flip them back into the requested order below.
|
||||
if (direction === "previous" && order === "asc") order = "desc"
|
||||
if (direction === "previous" && order === "desc") order = "asc"
|
||||
@@ -188,18 +186,18 @@ export const layer = Layer.effect(
|
||||
conditions.push(or(eq(SessionTable.path, input.path), like(SessionTable.path, `${input.path}/%`))!)
|
||||
if (input.workspaceID) conditions.push(eq(SessionTable.workspace_id, input.workspaceID))
|
||||
if (input.roots) conditions.push(isNull(SessionTable.parent_id))
|
||||
if (input.start) conditions.push(gte(sortColumn, input.start))
|
||||
if (input.start) conditions.push(gte(SessionTable.time_created, input.start))
|
||||
if (input.search) conditions.push(like(SessionTable.title, `%${input.search}%`))
|
||||
if (input.cursor) {
|
||||
conditions.push(
|
||||
order === "asc"
|
||||
? or(
|
||||
gt(sortColumn, input.cursor.time),
|
||||
and(eq(sortColumn, input.cursor.time), gt(SessionTable.id, input.cursor.id)),
|
||||
gt(SessionTable.time_created, input.cursor.time),
|
||||
and(eq(SessionTable.time_created, input.cursor.time), gt(SessionTable.id, input.cursor.id)),
|
||||
)!
|
||||
: or(
|
||||
lt(sortColumn, input.cursor.time),
|
||||
and(eq(sortColumn, input.cursor.time), lt(SessionTable.id, input.cursor.id)),
|
||||
lt(SessionTable.time_created, input.cursor.time),
|
||||
and(eq(SessionTable.time_created, input.cursor.time), lt(SessionTable.id, input.cursor.id)),
|
||||
)!,
|
||||
)
|
||||
}
|
||||
@@ -208,7 +206,7 @@ export const layer = Layer.effect(
|
||||
.from(SessionTable)
|
||||
.where(conditions.length > 0 ? and(...conditions) : undefined)
|
||||
.orderBy(
|
||||
order === "asc" ? asc(sortColumn) : desc(sortColumn),
|
||||
order === "asc" ? asc(SessionTable.time_created) : desc(SessionTable.time_created),
|
||||
order === "asc" ? asc(SessionTable.id) : desc(SessionTable.id),
|
||||
)
|
||||
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { expect } from "bun:test"
|
||||
import { AppFileSystem } from "@opencode-ai/core/filesystem"
|
||||
import { Substitution } from "@opencode-ai/core/substitution"
|
||||
import { Effect, Layer } from "effect"
|
||||
import path from "path"
|
||||
import { pathToFileURL } from "url"
|
||||
@@ -11,6 +12,7 @@ import { RuntimeFlags } from "../../src/effect/runtime-flags"
|
||||
import { Plugin } from "../../src/plugin"
|
||||
import { AccountTest } from "../fake/account"
|
||||
import { AuthTest } from "../fake/auth"
|
||||
import { AuthWellKnownTest } from "../fake/auth-well-known"
|
||||
import { NpmTest } from "../fake/npm"
|
||||
import { ProviderTest } from "../fake/provider"
|
||||
import { SkillTest } from "../fake/skill"
|
||||
@@ -25,6 +27,8 @@ const pluginUrl = pathToFileURL(path.join(import.meta.dir, "..", "fixture", "age
|
||||
const provider = ProviderTest.fake()
|
||||
const configLayer = Config.layer.pipe(
|
||||
Layer.provide(AppFileSystem.defaultLayer),
|
||||
Layer.provide(AuthWellKnownTest.empty),
|
||||
Layer.provide(Substitution.defaultLayer),
|
||||
Layer.provide(Env.defaultLayer),
|
||||
Layer.provide(AuthTest.empty),
|
||||
Layer.provide(AccountTest.empty),
|
||||
|
||||
@@ -1,146 +0,0 @@
|
||||
/** @jsxImportSource @opentui/solid */
|
||||
import { TextareaRenderable } from "@opentui/core"
|
||||
import { createDefaultOpenTuiKeymap } from "@opentui/keymap/opentui"
|
||||
import { testRender, useRenderer } from "@opentui/solid"
|
||||
import { expect, test } from "bun:test"
|
||||
import { mkdir } from "node:fs/promises"
|
||||
import path from "node:path"
|
||||
import { onCleanup } from "solid-js"
|
||||
import { tmpdir } from "../../fixture/fixture"
|
||||
import { createTuiResolvedConfig } from "../../fixture/tui-runtime"
|
||||
import type { TuiKeybind } from "../../../src/cli/cmd/tui/config/keybind"
|
||||
|
||||
async function wait(fn: () => boolean, timeout = 2000) {
|
||||
const start = Date.now()
|
||||
while (!fn()) {
|
||||
if (Date.now() - start > timeout) throw new Error("timed out waiting for condition")
|
||||
await Bun.sleep(10)
|
||||
}
|
||||
}
|
||||
|
||||
async function mountPrompt(input: {
|
||||
root: string
|
||||
keybinds: Partial<TuiKeybind.Keybinds>
|
||||
onConfirm: (value: string) => void
|
||||
}) {
|
||||
const { Global } = await import("@opencode-ai/core/global")
|
||||
const previous = {
|
||||
config: Global.Path.config,
|
||||
state: Global.Path.state,
|
||||
}
|
||||
Global.Path.config = path.join(input.root, "config")
|
||||
Global.Path.state = path.join(input.root, "state")
|
||||
await mkdir(Global.Path.config, { recursive: true })
|
||||
await mkdir(Global.Path.state, { recursive: true })
|
||||
await Bun.write(path.join(Global.Path.state, "kv.json"), "{}")
|
||||
|
||||
const [
|
||||
{ DialogProvider },
|
||||
{ DialogPrompt },
|
||||
{ KVProvider },
|
||||
{ ThemeProvider },
|
||||
{ TuiConfigProvider },
|
||||
{ ToastProvider },
|
||||
{ OpencodeKeymapProvider, registerOpencodeKeymap },
|
||||
] = await Promise.all([
|
||||
import("../../../src/cli/cmd/tui/ui/dialog"),
|
||||
import("../../../src/cli/cmd/tui/ui/dialog-prompt"),
|
||||
import("../../../src/cli/cmd/tui/context/kv"),
|
||||
import("../../../src/cli/cmd/tui/context/theme"),
|
||||
import("../../../src/cli/cmd/tui/context/tui-config"),
|
||||
import("../../../src/cli/cmd/tui/ui/toast"),
|
||||
import("../../../src/cli/cmd/tui/keymap"),
|
||||
])
|
||||
|
||||
function Harness() {
|
||||
const renderer = useRenderer()
|
||||
const keymap = createDefaultOpenTuiKeymap(renderer)
|
||||
const resolvedConfig = createTuiResolvedConfig({
|
||||
keybinds: input.keybinds,
|
||||
leader_timeout: 1000,
|
||||
})
|
||||
const off = registerOpencodeKeymap(keymap, renderer, resolvedConfig)
|
||||
onCleanup(off)
|
||||
|
||||
return (
|
||||
<OpencodeKeymapProvider keymap={keymap}>
|
||||
<TuiConfigProvider config={resolvedConfig}>
|
||||
<KVProvider>
|
||||
<ThemeProvider mode="dark">
|
||||
<ToastProvider>
|
||||
<DialogProvider>
|
||||
<DialogPrompt title="Rename Session" value="draft" onConfirm={input.onConfirm} />
|
||||
</DialogProvider>
|
||||
</ToastProvider>
|
||||
</ThemeProvider>
|
||||
</KVProvider>
|
||||
</TuiConfigProvider>
|
||||
</OpencodeKeymapProvider>
|
||||
)
|
||||
}
|
||||
|
||||
const app = await testRender(() => <Harness />, { kittyKeyboard: true })
|
||||
return {
|
||||
app,
|
||||
async cleanup() {
|
||||
app.renderer.destroy()
|
||||
Global.Path.config = previous.config
|
||||
Global.Path.state = previous.state
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
test("dialog prompt submit wins when return is also input newline", async () => {
|
||||
await using tmp = await tmpdir()
|
||||
const confirmed: string[] = []
|
||||
const prompt = await mountPrompt({
|
||||
root: tmp.path,
|
||||
keybinds: {
|
||||
input_submit: "super+return",
|
||||
input_newline: "return,shift+return,alt+return,ctrl+j",
|
||||
},
|
||||
onConfirm: (value) => confirmed.push(value),
|
||||
})
|
||||
|
||||
try {
|
||||
await wait(() => prompt.app.renderer.currentFocusedEditor instanceof TextareaRenderable)
|
||||
const textarea = prompt.app.renderer.currentFocusedEditor
|
||||
if (!(textarea instanceof TextareaRenderable)) throw new Error("expected focused dialog textarea")
|
||||
|
||||
prompt.app.mockInput.pressEnter()
|
||||
|
||||
expect(confirmed).toEqual(["draft"])
|
||||
expect(textarea.plainText).toBe("draft")
|
||||
} finally {
|
||||
await prompt.cleanup()
|
||||
}
|
||||
})
|
||||
|
||||
test("dialog prompt submit can be rebound separately from input submit", async () => {
|
||||
await using tmp = await tmpdir()
|
||||
const confirmed: string[] = []
|
||||
const prompt = await mountPrompt({
|
||||
root: tmp.path,
|
||||
keybinds: {
|
||||
input_submit: "return",
|
||||
"dialog.prompt.submit": "ctrl+y",
|
||||
},
|
||||
onConfirm: (value) => confirmed.push(value),
|
||||
})
|
||||
|
||||
try {
|
||||
await wait(() => prompt.app.renderer.currentFocusedEditor instanceof TextareaRenderable)
|
||||
const textarea = prompt.app.renderer.currentFocusedEditor
|
||||
if (!(textarea instanceof TextareaRenderable)) throw new Error("expected focused dialog textarea")
|
||||
|
||||
prompt.app.mockInput.pressEnter()
|
||||
expect(confirmed).toEqual([])
|
||||
expect(textarea.plainText).toBe("draft")
|
||||
|
||||
prompt.app.mockInput.pressKey("y", { ctrl: true })
|
||||
|
||||
expect(confirmed).toEqual(["draft"])
|
||||
} finally {
|
||||
await prompt.cleanup()
|
||||
}
|
||||
})
|
||||
@@ -8,7 +8,7 @@ import { EffectFlock } from "@opencode-ai/core/util/effect-flock"
|
||||
|
||||
import { InstanceRef } from "../../src/effect/instance-ref"
|
||||
import type { InstanceContext } from "../../src/project/instance-context"
|
||||
import { Auth } from "../../src/auth"
|
||||
import { AuthWellKnown } from "@opencode-ai/core/auth-well-known"
|
||||
import { Account } from "../../src/account/account"
|
||||
import { AccessToken, AccountID, OrgID } from "../../src/account/schema"
|
||||
import { AppFileSystem } from "@opencode-ai/core/filesystem"
|
||||
@@ -31,16 +31,14 @@ import { ProjectID } from "../../src/project/schema"
|
||||
import { Filesystem } from "@/util/filesystem"
|
||||
import { ConfigPlugin } from "@/config/plugin"
|
||||
import { Npm } from "@opencode-ai/core/npm"
|
||||
import { Substitution } from "@opencode-ai/core/substitution"
|
||||
import { AuthWellKnownTest } from "../fake/auth-well-known"
|
||||
|
||||
const emptyAccount = Layer.mock(Account.Service)({
|
||||
active: () => Effect.succeed(Option.none()),
|
||||
activeOrg: () => Effect.succeed(Option.none()),
|
||||
})
|
||||
|
||||
const emptyAuth = Layer.mock(Auth.Service)({
|
||||
all: () => Effect.succeed({}),
|
||||
})
|
||||
|
||||
const testFlock = EffectFlock.defaultLayer
|
||||
|
||||
const noopNpm = Layer.mock(Npm.Service)({
|
||||
@@ -49,11 +47,15 @@ const noopNpm = Layer.mock(Npm.Service)({
|
||||
which: () => Effect.succeed(Option.none()),
|
||||
})
|
||||
|
||||
const runSubstitution = <A, E>(effect: Effect.Effect<A, E, Substitution.Service>) =>
|
||||
Effect.runPromise(effect.pipe(Effect.provide(Substitution.defaultLayer)))
|
||||
|
||||
const layer = Config.layer.pipe(
|
||||
Layer.provide(testFlock),
|
||||
Layer.provide(AppFileSystem.defaultLayer),
|
||||
Layer.provide(Substitution.defaultLayer),
|
||||
Layer.provide(Env.defaultLayer),
|
||||
Layer.provide(emptyAuth),
|
||||
Layer.provide(AuthWellKnownTest.empty),
|
||||
Layer.provide(emptyAccount),
|
||||
Layer.provideMerge(infra),
|
||||
Layer.provide(noopNpm),
|
||||
@@ -486,6 +488,30 @@ test("handles environment variable substitution", async () => {
|
||||
}
|
||||
})
|
||||
|
||||
test("environment variable substitution accepts an env overlay", async () => {
|
||||
const originalEnv = process.env["TEST_VAR"]
|
||||
delete process.env["TEST_VAR"]
|
||||
|
||||
try {
|
||||
expect(
|
||||
await runSubstitution(
|
||||
Substitution.Service.use((substitution) =>
|
||||
substitution.substitute({
|
||||
text: "{env:TEST_VAR}",
|
||||
type: "virtual",
|
||||
dir: "/tmp",
|
||||
source: "test",
|
||||
env: { TEST_VAR: "overlay" },
|
||||
}),
|
||||
),
|
||||
),
|
||||
).toBe("overlay")
|
||||
} finally {
|
||||
if (originalEnv === undefined) delete process.env["TEST_VAR"]
|
||||
else process.env["TEST_VAR"] = originalEnv
|
||||
}
|
||||
})
|
||||
|
||||
test("preserves env variables when adding $schema to config", async () => {
|
||||
const originalEnv = process.env["PRESERVE_VAR"]
|
||||
process.env["PRESERVE_VAR"] = "secret_value"
|
||||
@@ -564,8 +590,9 @@ test("resolves env templates in account config with account token", async () =>
|
||||
const layer = Config.layer.pipe(
|
||||
Layer.provide(testFlock),
|
||||
Layer.provide(AppFileSystem.defaultLayer),
|
||||
Layer.provide(Substitution.defaultLayer),
|
||||
Layer.provide(Env.defaultLayer),
|
||||
Layer.provide(emptyAuth),
|
||||
Layer.provide(AuthWellKnownTest.empty),
|
||||
Layer.provide(fakeAccount),
|
||||
Layer.provideMerge(infra),
|
||||
Layer.provide(noopNpm),
|
||||
@@ -1075,8 +1102,9 @@ test("installs dependencies in writable OPENCODE_CONFIG_DIR", async () => {
|
||||
const testLayer = Config.layer.pipe(
|
||||
Layer.provide(testFlock),
|
||||
Layer.provide(AppFileSystem.defaultLayer),
|
||||
Layer.provide(Substitution.defaultLayer),
|
||||
Layer.provide(Env.defaultLayer),
|
||||
Layer.provide(emptyAuth),
|
||||
Layer.provide(AuthWellKnownTest.empty),
|
||||
Layer.provide(emptyAccount),
|
||||
Layer.provideMerge(infra),
|
||||
Layer.provide(noopNpm),
|
||||
@@ -1938,192 +1966,44 @@ test("local .opencode config can override MCP from project config", async () =>
|
||||
})
|
||||
|
||||
test("project config overrides remote well-known config", async () => {
|
||||
const originalFetch = globalThis.fetch
|
||||
let fetchedUrl: string | undefined
|
||||
globalThis.fetch = mock((url: string | URL | Request) => {
|
||||
const urlStr = url instanceof Request ? url.url : url instanceof URL ? url.href : url
|
||||
if (urlStr.includes(".well-known/opencode")) {
|
||||
fetchedUrl = urlStr
|
||||
return Promise.resolve(
|
||||
new Response(
|
||||
JSON.stringify({
|
||||
config: {
|
||||
mcp: { jira: { type: "remote", url: "https://jira.example.com/mcp", enabled: false } },
|
||||
},
|
||||
}),
|
||||
{ status: 200 },
|
||||
),
|
||||
)
|
||||
}
|
||||
return originalFetch(url)
|
||||
}) as unknown as typeof fetch
|
||||
|
||||
const fakeAuth = Layer.mock(Auth.Service)({
|
||||
all: () =>
|
||||
Effect.succeed({
|
||||
"https://example.com": new Auth.WellKnown({ type: "wellknown", key: "TEST_TOKEN", token: "test-token" }),
|
||||
}),
|
||||
const fakeAuthWellKnown = Layer.mock(AuthWellKnown.Service)({
|
||||
configs: () =>
|
||||
Effect.succeed([
|
||||
{
|
||||
url: "https://example.com",
|
||||
source: "https://example.com/.well-known/opencode",
|
||||
dir: "https://example.com/.well-known",
|
||||
content: {
|
||||
mcp: { jira: { type: "remote", url: "https://jira.example.com/mcp", enabled: false } },
|
||||
},
|
||||
},
|
||||
]),
|
||||
})
|
||||
|
||||
const layer = Config.layer.pipe(
|
||||
Layer.provide(testFlock),
|
||||
Layer.provide(AppFileSystem.defaultLayer),
|
||||
Layer.provide(Substitution.defaultLayer),
|
||||
Layer.provide(Env.defaultLayer),
|
||||
Layer.provide(fakeAuth),
|
||||
Layer.provide(fakeAuthWellKnown),
|
||||
Layer.provide(emptyAccount),
|
||||
Layer.provideMerge(infra),
|
||||
Layer.provide(noopNpm),
|
||||
)
|
||||
|
||||
try {
|
||||
await provideTmpdirInstance(
|
||||
() =>
|
||||
Config.Service.use((svc) =>
|
||||
Effect.gen(function* () {
|
||||
const config = yield* svc.get()
|
||||
expect(fetchedUrl).toBe("https://example.com/.well-known/opencode")
|
||||
expect(config.mcp?.jira?.enabled).toBe(true)
|
||||
}),
|
||||
),
|
||||
{
|
||||
git: true,
|
||||
config: { mcp: { jira: { type: "remote", url: "https://jira.example.com/mcp", enabled: true } } },
|
||||
},
|
||||
).pipe(Effect.scoped, Effect.provide(layer), Effect.runPromise)
|
||||
} finally {
|
||||
globalThis.fetch = originalFetch
|
||||
}
|
||||
})
|
||||
|
||||
test("wellknown URL with trailing slash is normalized", async () => {
|
||||
const originalFetch = globalThis.fetch
|
||||
let fetchedUrl: string | undefined
|
||||
globalThis.fetch = mock((url: string | URL | Request) => {
|
||||
const urlStr = url instanceof Request ? url.url : url instanceof URL ? url.href : url
|
||||
if (urlStr.includes(".well-known/opencode")) {
|
||||
fetchedUrl = urlStr
|
||||
return Promise.resolve(
|
||||
new Response(
|
||||
JSON.stringify({
|
||||
config: {
|
||||
mcp: { slack: { type: "remote", url: "https://slack.example.com/mcp", enabled: true } },
|
||||
},
|
||||
}),
|
||||
{ status: 200 },
|
||||
),
|
||||
)
|
||||
}
|
||||
return originalFetch(url)
|
||||
}) as unknown as typeof fetch
|
||||
|
||||
const fakeAuth = Layer.mock(Auth.Service)({
|
||||
all: () =>
|
||||
Effect.succeed({
|
||||
"https://example.com/": new Auth.WellKnown({ type: "wellknown", key: "TEST_TOKEN", token: "test-token" }),
|
||||
}),
|
||||
})
|
||||
|
||||
const layer = Config.layer.pipe(
|
||||
Layer.provide(testFlock),
|
||||
Layer.provide(AppFileSystem.defaultLayer),
|
||||
Layer.provide(Env.defaultLayer),
|
||||
Layer.provide(fakeAuth),
|
||||
Layer.provide(emptyAccount),
|
||||
Layer.provideMerge(infra),
|
||||
Layer.provide(noopNpm),
|
||||
)
|
||||
|
||||
try {
|
||||
await provideTmpdirInstance(
|
||||
() =>
|
||||
Config.Service.use((svc) =>
|
||||
Effect.gen(function* () {
|
||||
yield* svc.get()
|
||||
expect(fetchedUrl).toBe("https://example.com/.well-known/opencode")
|
||||
}),
|
||||
),
|
||||
{ git: true },
|
||||
).pipe(Effect.scoped, Effect.provide(layer), Effect.runPromise)
|
||||
} finally {
|
||||
globalThis.fetch = originalFetch
|
||||
}
|
||||
})
|
||||
|
||||
test("wellknown remote_config supports templated env vars in headers", async () => {
|
||||
const originalFetch = globalThis.fetch
|
||||
const originalToken = process.env.TEST_TOKEN
|
||||
let wellknownFetchedUrl: string | undefined
|
||||
let remoteFetchedUrl: string | undefined
|
||||
let remoteHeaders: HeadersInit | undefined
|
||||
globalThis.fetch = mock((url: string | URL | Request, init?: RequestInit) => {
|
||||
const urlStr = url instanceof Request ? url.url : url instanceof URL ? url.href : url
|
||||
if (urlStr.includes(".well-known/opencode")) {
|
||||
wellknownFetchedUrl = urlStr
|
||||
return Promise.resolve(
|
||||
new Response(
|
||||
JSON.stringify({
|
||||
remote_config: {
|
||||
url: "https://config.example.com/opencode.json",
|
||||
headers: {
|
||||
Authorization: "Bearer {env:TEST_TOKEN}",
|
||||
},
|
||||
},
|
||||
}),
|
||||
{ status: 200 },
|
||||
),
|
||||
)
|
||||
}
|
||||
if (urlStr.includes("config.example.com")) {
|
||||
remoteFetchedUrl = urlStr
|
||||
remoteHeaders = init?.headers
|
||||
return Promise.resolve(
|
||||
new Response(
|
||||
JSON.stringify({
|
||||
mcp: { confluence: { type: "remote", url: "https://confluence.example.com/mcp", enabled: true } },
|
||||
}),
|
||||
{ status: 200 },
|
||||
),
|
||||
)
|
||||
}
|
||||
return originalFetch(url, init)
|
||||
}) as unknown as typeof fetch
|
||||
|
||||
const fakeAuth = Layer.mock(Auth.Service)({
|
||||
all: () =>
|
||||
Effect.succeed({
|
||||
"https://example.com": new Auth.WellKnown({ type: "wellknown", key: "TEST_TOKEN", token: "test-token" }),
|
||||
}),
|
||||
})
|
||||
|
||||
const layer = Config.layer.pipe(
|
||||
Layer.provide(testFlock),
|
||||
Layer.provide(AppFileSystem.defaultLayer),
|
||||
Layer.provide(Env.defaultLayer),
|
||||
Layer.provide(fakeAuth),
|
||||
Layer.provide(emptyAccount),
|
||||
Layer.provideMerge(infra),
|
||||
Layer.provide(noopNpm),
|
||||
)
|
||||
|
||||
try {
|
||||
await provideTmpdirInstance(
|
||||
() =>
|
||||
Config.Service.use((svc) =>
|
||||
Effect.gen(function* () {
|
||||
const config = yield* svc.get()
|
||||
expect(wellknownFetchedUrl).toBe("https://example.com/.well-known/opencode")
|
||||
expect(remoteFetchedUrl).toBe("https://config.example.com/opencode.json")
|
||||
expect(remoteHeaders).toEqual({ Authorization: "Bearer test-token" })
|
||||
expect(config.mcp?.confluence?.enabled).toBe(true)
|
||||
}),
|
||||
),
|
||||
{ git: true },
|
||||
).pipe(Effect.scoped, Effect.provide(layer), Effect.runPromise)
|
||||
} finally {
|
||||
globalThis.fetch = originalFetch
|
||||
if (originalToken === undefined) delete process.env.TEST_TOKEN
|
||||
else process.env.TEST_TOKEN = originalToken
|
||||
}
|
||||
await provideTmpdirInstance(
|
||||
() =>
|
||||
Config.Service.use((svc) =>
|
||||
Effect.gen(function* () {
|
||||
const config = yield* svc.get()
|
||||
expect(config.mcp?.jira?.enabled).toBe(true)
|
||||
}),
|
||||
),
|
||||
{
|
||||
git: true,
|
||||
config: { mcp: { jira: { type: "remote", url: "https://jira.example.com/mcp", enabled: true } } },
|
||||
},
|
||||
).pipe(Effect.scoped, Effect.provide(layer), Effect.runPromise)
|
||||
})
|
||||
|
||||
describe("resolvePluginSpec", () => {
|
||||
|
||||
@@ -470,7 +470,6 @@ it.instance("resolves keybind lookup from canonical keybinds", () =>
|
||||
which_key_toggle: "alt+k",
|
||||
editor_open: "ctrl+e",
|
||||
"prompt.autocomplete.next": "ctrl+j",
|
||||
"dialog.prompt.submit": "ctrl+s",
|
||||
"dialog.mcp.toggle": "ctrl+t",
|
||||
model_favorite_toggle: "ctrl+f",
|
||||
"dialog.plugins.install": "shift+i",
|
||||
@@ -492,7 +491,6 @@ it.instance("resolves keybind lookup from canonical keybinds", () =>
|
||||
)
|
||||
expect(config.keybinds.get("prompt.editor")?.[0]?.key).toBe("ctrl+e")
|
||||
expect(config.keybinds.get("prompt.autocomplete.next")?.[0]?.key).toBe("ctrl+j")
|
||||
expect(config.keybinds.get("dialog.prompt.submit")?.[0]?.key).toBe("ctrl+s")
|
||||
expect(config.keybinds.get("dialog.mcp.toggle")?.[0]?.key).toBe("ctrl+t")
|
||||
expect(config.keybinds.get("model.dialog.favorite")?.[0]?.key).toBe("ctrl+f")
|
||||
expect(config.keybinds.get("dialog.plugins.install")?.[0]?.key).toBe("shift+i")
|
||||
|
||||
@@ -0,0 +1,8 @@
|
||||
import { AuthWellKnown } from "@opencode-ai/core/auth-well-known"
|
||||
import { Effect, Layer } from "effect"
|
||||
|
||||
export const AuthWellKnownTest = {
|
||||
empty: Layer.mock(AuthWellKnown.Service, {
|
||||
configs: () => Effect.succeed([]),
|
||||
}),
|
||||
}
|
||||
@@ -3,6 +3,7 @@ import { Effect, Layer, Option } from "effect"
|
||||
import { CrossSpawnSpawner } from "@opencode-ai/core/cross-spawn-spawner"
|
||||
import { AppFileSystem } from "@opencode-ai/core/filesystem"
|
||||
import { EffectFlock } from "@opencode-ai/core/util/effect-flock"
|
||||
import { Substitution } from "@opencode-ai/core/substitution"
|
||||
import path from "path"
|
||||
import { pathToFileURL } from "url"
|
||||
import { Account } from "../../src/account/account"
|
||||
@@ -16,6 +17,7 @@ import { ModelID, ProviderID } from "../../src/provider/schema"
|
||||
import { provideTmpdirInstance } from "../fixture/fixture"
|
||||
import { testEffect } from "../lib/effect"
|
||||
import { NpmTest } from "../fake/npm"
|
||||
import { AuthWellKnownTest } from "../fake/auth-well-known"
|
||||
|
||||
const emptyAccount = Layer.mock(Account.Service)({
|
||||
active: () => Effect.succeed(Option.none()),
|
||||
@@ -27,6 +29,8 @@ const emptyAuth = Layer.mock(Auth.Service)({
|
||||
const configLayer = Config.layer.pipe(
|
||||
Layer.provide(EffectFlock.defaultLayer),
|
||||
Layer.provide(AppFileSystem.defaultLayer),
|
||||
Layer.provide(AuthWellKnownTest.empty),
|
||||
Layer.provide(Substitution.defaultLayer),
|
||||
Layer.provide(Env.defaultLayer),
|
||||
Layer.provide(emptyAuth),
|
||||
Layer.provide(emptyAccount),
|
||||
|
||||
@@ -4,6 +4,7 @@ import { FetchHttpClient } from "effect/unstable/http"
|
||||
import { CrossSpawnSpawner } from "@opencode-ai/core/cross-spawn-spawner"
|
||||
import { AppFileSystem } from "@opencode-ai/core/filesystem"
|
||||
import { EffectFlock } from "@opencode-ai/core/util/effect-flock"
|
||||
import { Substitution } from "@opencode-ai/core/substitution"
|
||||
import path from "path"
|
||||
import { pathToFileURL } from "url"
|
||||
import { Account } from "../../src/account/account"
|
||||
@@ -25,6 +26,7 @@ import { SyncEvent } from "../../src/sync"
|
||||
import { disposeAllInstances, provideTmpdirInstance } from "../fixture/fixture"
|
||||
import { testEffect } from "../lib/effect"
|
||||
import { NpmTest } from "../fake/npm"
|
||||
import { AuthWellKnownTest } from "../fake/auth-well-known"
|
||||
|
||||
const emptyAccount = Layer.mock(Account.Service)({
|
||||
active: () => Effect.succeed(Option.none()),
|
||||
@@ -36,6 +38,8 @@ const emptyAuth = Layer.mock(Auth.Service)({
|
||||
const configLayer = Config.layer.pipe(
|
||||
Layer.provide(EffectFlock.defaultLayer),
|
||||
Layer.provide(AppFileSystem.defaultLayer),
|
||||
Layer.provide(AuthWellKnownTest.empty),
|
||||
Layer.provide(Substitution.defaultLayer),
|
||||
Layer.provide(Env.defaultLayer),
|
||||
Layer.provide(emptyAuth),
|
||||
Layer.provide(emptyAccount),
|
||||
|
||||
@@ -2,7 +2,7 @@ import { describe, expect, test } from "bun:test"
|
||||
import { Schema } from "effect"
|
||||
import { ConfigProvider } from "@/config/provider"
|
||||
import { CatalogModelStatus, ModelStatus } from "@/provider/model-status"
|
||||
import { ModelsDev } from "@opencode-ai/core/models-dev"
|
||||
import { ModelsDev } from "@opencode-ai/core/models"
|
||||
import { Provider } from "@/provider/provider"
|
||||
|
||||
describe("provider model status schemas", () => {
|
||||
|
||||
@@ -6,7 +6,7 @@ import { disposeAllInstances, tmpdir, withTestInstance } from "../fixture/fixtur
|
||||
import { Global } from "@opencode-ai/core/global"
|
||||
import type { InstanceContext } from "../../src/project/instance-context"
|
||||
import { Plugin } from "../../src/plugin/index"
|
||||
import { ModelsDev } from "@opencode-ai/core/models-dev"
|
||||
import { ModelsDev } from "@opencode-ai/core/models"
|
||||
import { Provider } from "@/provider/provider"
|
||||
import { ProviderID, ModelID } from "../../src/provider/schema"
|
||||
import { Filesystem } from "@/util/filesystem"
|
||||
|
||||
@@ -9,7 +9,7 @@ import { LLM } from "../../src/session/llm"
|
||||
import type { InstanceContext } from "../../src/project/instance-context"
|
||||
import { Provider } from "@/provider/provider"
|
||||
import { ProviderTransform } from "@/provider/transform"
|
||||
import { ModelsDev } from "@opencode-ai/core/models-dev"
|
||||
import { ModelsDev } from "@opencode-ai/core/models"
|
||||
import { ProviderID, ModelID } from "../../src/provider/schema"
|
||||
import { Filesystem } from "@/util/filesystem"
|
||||
import { tmpdir, withTestInstance } from "../fixture/fixture"
|
||||
|
||||
@@ -22,9 +22,9 @@
|
||||
"zod": "catalog:"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@opentui/core": ">=0.2.13",
|
||||
"@opentui/keymap": ">=0.2.13",
|
||||
"@opentui/solid": ">=0.2.13"
|
||||
"@opentui/core": ">=0.2.11",
|
||||
"@opentui/keymap": ">=0.2.11",
|
||||
"@opentui/solid": ">=0.2.11"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@opentui/core": {
|
||||
|
||||
@@ -5,12 +5,6 @@ export type ClientOptions = {
|
||||
}
|
||||
|
||||
export type Event =
|
||||
| EventTuiPromptAppend
|
||||
| EventTuiCommandExecute
|
||||
| EventTuiToastShow1
|
||||
| EventTuiSessionSelect
|
||||
| EventServerConnected
|
||||
| EventGlobalDisposed
|
||||
| EventServerInstanceDisposed
|
||||
| EventFileEdited
|
||||
| EventFileWatcherUpdated
|
||||
@@ -27,6 +21,10 @@ export type Event =
|
||||
| EventTodoUpdated
|
||||
| EventSessionStatus
|
||||
| EventSessionIdle
|
||||
| EventTuiPromptAppend
|
||||
| EventTuiCommandExecute
|
||||
| EventTuiToastShow1
|
||||
| EventTuiSessionSelect
|
||||
| EventMcpToolsChanged
|
||||
| EventMcpBrowserOpenFailed
|
||||
| EventCommandExecuted
|
||||
@@ -77,6 +75,8 @@ export type Event =
|
||||
| EventSessionNextCompactionStarted
|
||||
| EventSessionNextCompactionDelta
|
||||
| EventSessionNextCompactionEnded
|
||||
| EventServerConnected
|
||||
| EventGlobalDisposed
|
||||
| EventCatalogModelUpdated
|
||||
|
||||
export type OAuth = {
|
||||
@@ -104,61 +104,6 @@ export type WellKnownAuth = {
|
||||
|
||||
export type Auth = OAuth | ApiAuth | WellKnownAuth
|
||||
|
||||
export type EventTuiPromptAppend = {
|
||||
id: string
|
||||
type: "tui.prompt.append"
|
||||
properties: {
|
||||
text: string
|
||||
}
|
||||
}
|
||||
|
||||
export type EventTuiCommandExecute = {
|
||||
id: string
|
||||
type: "tui.command.execute"
|
||||
properties: {
|
||||
command:
|
||||
| "session.list"
|
||||
| "session.new"
|
||||
| "session.share"
|
||||
| "session.interrupt"
|
||||
| "session.compact"
|
||||
| "session.page.up"
|
||||
| "session.page.down"
|
||||
| "session.line.up"
|
||||
| "session.line.down"
|
||||
| "session.half.page.up"
|
||||
| "session.half.page.down"
|
||||
| "session.first"
|
||||
| "session.last"
|
||||
| "prompt.clear"
|
||||
| "prompt.submit"
|
||||
| "agent.cycle"
|
||||
| string
|
||||
}
|
||||
}
|
||||
|
||||
export type EventTuiToastShow = {
|
||||
id: string
|
||||
type: "tui.toast.show"
|
||||
properties: {
|
||||
title?: string
|
||||
message: string
|
||||
variant: "info" | "success" | "warning" | "error"
|
||||
duration?: number
|
||||
}
|
||||
}
|
||||
|
||||
export type EventTuiSessionSelect = {
|
||||
id: string
|
||||
type: "tui.session.select"
|
||||
properties: {
|
||||
/**
|
||||
* Session ID to navigate to
|
||||
*/
|
||||
sessionID: string
|
||||
}
|
||||
}
|
||||
|
||||
export type PermissionRequest = {
|
||||
id: string
|
||||
sessionID: string
|
||||
@@ -336,6 +281,61 @@ export type SessionStatus =
|
||||
type: "busy"
|
||||
}
|
||||
|
||||
export type EventTuiPromptAppend = {
|
||||
id: string
|
||||
type: "tui.prompt.append"
|
||||
properties: {
|
||||
text: string
|
||||
}
|
||||
}
|
||||
|
||||
export type EventTuiCommandExecute = {
|
||||
id: string
|
||||
type: "tui.command.execute"
|
||||
properties: {
|
||||
command:
|
||||
| "session.list"
|
||||
| "session.new"
|
||||
| "session.share"
|
||||
| "session.interrupt"
|
||||
| "session.compact"
|
||||
| "session.page.up"
|
||||
| "session.page.down"
|
||||
| "session.line.up"
|
||||
| "session.line.down"
|
||||
| "session.half.page.up"
|
||||
| "session.half.page.down"
|
||||
| "session.first"
|
||||
| "session.last"
|
||||
| "prompt.clear"
|
||||
| "prompt.submit"
|
||||
| "agent.cycle"
|
||||
| string
|
||||
}
|
||||
}
|
||||
|
||||
export type EventTuiToastShow = {
|
||||
id: string
|
||||
type: "tui.toast.show"
|
||||
properties: {
|
||||
title?: string
|
||||
message: string
|
||||
variant: "info" | "success" | "warning" | "error"
|
||||
duration?: number
|
||||
}
|
||||
}
|
||||
|
||||
export type EventTuiSessionSelect = {
|
||||
id: string
|
||||
type: "tui.session.select"
|
||||
properties: {
|
||||
/**
|
||||
* Session ID to navigate to
|
||||
*/
|
||||
sessionID: string
|
||||
}
|
||||
}
|
||||
|
||||
export type Project = {
|
||||
id: string
|
||||
worktree: string
|
||||
@@ -790,12 +790,6 @@ export type GlobalEvent = {
|
||||
project?: string
|
||||
workspace?: string
|
||||
payload:
|
||||
| EventTuiPromptAppend
|
||||
| EventTuiCommandExecute
|
||||
| EventTuiToastShow
|
||||
| EventTuiSessionSelect
|
||||
| EventServerConnected
|
||||
| EventGlobalDisposed
|
||||
| EventServerInstanceDisposed
|
||||
| EventFileEdited
|
||||
| EventFileWatcherUpdated
|
||||
@@ -812,6 +806,10 @@ export type GlobalEvent = {
|
||||
| EventTodoUpdated
|
||||
| EventSessionStatus
|
||||
| EventSessionIdle
|
||||
| EventTuiPromptAppend
|
||||
| EventTuiCommandExecute
|
||||
| EventTuiToastShow
|
||||
| EventTuiSessionSelect
|
||||
| EventMcpToolsChanged
|
||||
| EventMcpBrowserOpenFailed
|
||||
| EventCommandExecuted
|
||||
@@ -862,6 +860,8 @@ export type GlobalEvent = {
|
||||
| EventSessionNextCompactionStarted
|
||||
| EventSessionNextCompactionDelta
|
||||
| EventSessionNextCompactionEnded
|
||||
| EventServerConnected
|
||||
| EventGlobalDisposed
|
||||
| EventCatalogModelUpdated
|
||||
| SyncEventMessageUpdated
|
||||
| SyncEventMessageRemoved
|
||||
@@ -2403,22 +2403,6 @@ export type SyncEventSessionNextCompactionEnded = {
|
||||
}
|
||||
}
|
||||
|
||||
export type EventServerConnected = {
|
||||
id: string
|
||||
type: "server.connected"
|
||||
properties: {
|
||||
[key: string]: unknown
|
||||
}
|
||||
}
|
||||
|
||||
export type EventGlobalDisposed = {
|
||||
id: string
|
||||
type: "global.disposed"
|
||||
properties: {
|
||||
[key: string]: unknown
|
||||
}
|
||||
}
|
||||
|
||||
export type EventServerInstanceDisposed = {
|
||||
id: string
|
||||
type: "server.instance.disposed"
|
||||
@@ -3145,6 +3129,22 @@ export type EventSessionNextCompactionEnded = {
|
||||
}
|
||||
}
|
||||
|
||||
export type EventServerConnected = {
|
||||
id: string
|
||||
type: "server.connected"
|
||||
properties: {
|
||||
[key: string]: unknown
|
||||
}
|
||||
}
|
||||
|
||||
export type EventGlobalDisposed = {
|
||||
id: string
|
||||
type: "global.disposed"
|
||||
properties: {
|
||||
[key: string]: unknown
|
||||
}
|
||||
}
|
||||
|
||||
export type ModelV2Info = {
|
||||
id: string
|
||||
apiID: string
|
||||
|
||||
+206
-206
@@ -9043,24 +9043,6 @@
|
||||
"schemas": {
|
||||
"Event": {
|
||||
"anyOf": [
|
||||
{
|
||||
"$ref": "#/components/schemas/Event.tui.prompt.append"
|
||||
},
|
||||
{
|
||||
"$ref": "#/components/schemas/Event.tui.command.execute"
|
||||
},
|
||||
{
|
||||
"$ref": "#/components/schemas/EventTuiToastShow1"
|
||||
},
|
||||
{
|
||||
"$ref": "#/components/schemas/Event.tui.session.select"
|
||||
},
|
||||
{
|
||||
"$ref": "#/components/schemas/EventServerConnected"
|
||||
},
|
||||
{
|
||||
"$ref": "#/components/schemas/EventGlobalDisposed"
|
||||
},
|
||||
{
|
||||
"$ref": "#/components/schemas/EventServerInstanceDisposed"
|
||||
},
|
||||
@@ -9109,6 +9091,18 @@
|
||||
{
|
||||
"$ref": "#/components/schemas/EventSessionIdle"
|
||||
},
|
||||
{
|
||||
"$ref": "#/components/schemas/Event.tui.prompt.append"
|
||||
},
|
||||
{
|
||||
"$ref": "#/components/schemas/Event.tui.command.execute"
|
||||
},
|
||||
{
|
||||
"$ref": "#/components/schemas/EventTuiToastShow1"
|
||||
},
|
||||
{
|
||||
"$ref": "#/components/schemas/Event.tui.session.select"
|
||||
},
|
||||
{
|
||||
"$ref": "#/components/schemas/EventMcpToolsChanged"
|
||||
},
|
||||
@@ -9259,6 +9253,12 @@
|
||||
{
|
||||
"$ref": "#/components/schemas/EventSessionNextCompactionEnded"
|
||||
},
|
||||
{
|
||||
"$ref": "#/components/schemas/EventServerConnected"
|
||||
},
|
||||
{
|
||||
"$ref": "#/components/schemas/EventGlobalDisposed"
|
||||
},
|
||||
{
|
||||
"$ref": "#/components/schemas/EventCatalogModelUpdated"
|
||||
},
|
||||
@@ -9419,140 +9419,6 @@
|
||||
}
|
||||
]
|
||||
},
|
||||
"Event.tui.prompt.append": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"id": {
|
||||
"type": "string"
|
||||
},
|
||||
"type": {
|
||||
"type": "string",
|
||||
"enum": ["tui.prompt.append"]
|
||||
},
|
||||
"properties": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"text": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": ["text"],
|
||||
"additionalProperties": false
|
||||
}
|
||||
},
|
||||
"required": ["id", "type", "properties"],
|
||||
"additionalProperties": false
|
||||
},
|
||||
"Event.tui.command.execute": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"id": {
|
||||
"type": "string"
|
||||
},
|
||||
"type": {
|
||||
"type": "string",
|
||||
"enum": ["tui.command.execute"]
|
||||
},
|
||||
"properties": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"command": {
|
||||
"anyOf": [
|
||||
{
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"session.list",
|
||||
"session.new",
|
||||
"session.share",
|
||||
"session.interrupt",
|
||||
"session.compact",
|
||||
"session.page.up",
|
||||
"session.page.down",
|
||||
"session.line.up",
|
||||
"session.line.down",
|
||||
"session.half.page.up",
|
||||
"session.half.page.down",
|
||||
"session.first",
|
||||
"session.last",
|
||||
"prompt.clear",
|
||||
"prompt.submit",
|
||||
"agent.cycle"
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "string"
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"required": ["command"],
|
||||
"additionalProperties": false
|
||||
}
|
||||
},
|
||||
"required": ["id", "type", "properties"],
|
||||
"additionalProperties": false
|
||||
},
|
||||
"Event.tui.toast.show": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"id": {
|
||||
"type": "string"
|
||||
},
|
||||
"type": {
|
||||
"type": "string",
|
||||
"enum": ["tui.toast.show"]
|
||||
},
|
||||
"properties": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"title": {
|
||||
"type": "string"
|
||||
},
|
||||
"message": {
|
||||
"type": "string"
|
||||
},
|
||||
"variant": {
|
||||
"type": "string",
|
||||
"enum": ["info", "success", "warning", "error"]
|
||||
},
|
||||
"duration": {
|
||||
"type": "integer",
|
||||
"exclusiveMinimum": 0
|
||||
}
|
||||
},
|
||||
"required": ["message", "variant"],
|
||||
"additionalProperties": false
|
||||
}
|
||||
},
|
||||
"required": ["id", "type", "properties"],
|
||||
"additionalProperties": false
|
||||
},
|
||||
"Event.tui.session.select": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"id": {
|
||||
"type": "string"
|
||||
},
|
||||
"type": {
|
||||
"type": "string",
|
||||
"enum": ["tui.session.select"]
|
||||
},
|
||||
"properties": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"sessionID": {
|
||||
"type": "string",
|
||||
"pattern": "^ses",
|
||||
"description": "Session ID to navigate to"
|
||||
}
|
||||
},
|
||||
"required": ["sessionID"],
|
||||
"additionalProperties": false
|
||||
}
|
||||
},
|
||||
"required": ["id", "type", "properties"],
|
||||
"additionalProperties": false
|
||||
},
|
||||
"PermissionRequest": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
@@ -10012,6 +9878,140 @@
|
||||
}
|
||||
]
|
||||
},
|
||||
"Event.tui.prompt.append": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"id": {
|
||||
"type": "string"
|
||||
},
|
||||
"type": {
|
||||
"type": "string",
|
||||
"enum": ["tui.prompt.append"]
|
||||
},
|
||||
"properties": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"text": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": ["text"],
|
||||
"additionalProperties": false
|
||||
}
|
||||
},
|
||||
"required": ["id", "type", "properties"],
|
||||
"additionalProperties": false
|
||||
},
|
||||
"Event.tui.command.execute": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"id": {
|
||||
"type": "string"
|
||||
},
|
||||
"type": {
|
||||
"type": "string",
|
||||
"enum": ["tui.command.execute"]
|
||||
},
|
||||
"properties": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"command": {
|
||||
"anyOf": [
|
||||
{
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"session.list",
|
||||
"session.new",
|
||||
"session.share",
|
||||
"session.interrupt",
|
||||
"session.compact",
|
||||
"session.page.up",
|
||||
"session.page.down",
|
||||
"session.line.up",
|
||||
"session.line.down",
|
||||
"session.half.page.up",
|
||||
"session.half.page.down",
|
||||
"session.first",
|
||||
"session.last",
|
||||
"prompt.clear",
|
||||
"prompt.submit",
|
||||
"agent.cycle"
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "string"
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"required": ["command"],
|
||||
"additionalProperties": false
|
||||
}
|
||||
},
|
||||
"required": ["id", "type", "properties"],
|
||||
"additionalProperties": false
|
||||
},
|
||||
"Event.tui.toast.show": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"id": {
|
||||
"type": "string"
|
||||
},
|
||||
"type": {
|
||||
"type": "string",
|
||||
"enum": ["tui.toast.show"]
|
||||
},
|
||||
"properties": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"title": {
|
||||
"type": "string"
|
||||
},
|
||||
"message": {
|
||||
"type": "string"
|
||||
},
|
||||
"variant": {
|
||||
"type": "string",
|
||||
"enum": ["info", "success", "warning", "error"]
|
||||
},
|
||||
"duration": {
|
||||
"type": "integer",
|
||||
"exclusiveMinimum": 0
|
||||
}
|
||||
},
|
||||
"required": ["message", "variant"],
|
||||
"additionalProperties": false
|
||||
}
|
||||
},
|
||||
"required": ["id", "type", "properties"],
|
||||
"additionalProperties": false
|
||||
},
|
||||
"Event.tui.session.select": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"id": {
|
||||
"type": "string"
|
||||
},
|
||||
"type": {
|
||||
"type": "string",
|
||||
"enum": ["tui.session.select"]
|
||||
},
|
||||
"properties": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"sessionID": {
|
||||
"type": "string",
|
||||
"pattern": "^ses",
|
||||
"description": "Session ID to navigate to"
|
||||
}
|
||||
},
|
||||
"required": ["sessionID"],
|
||||
"additionalProperties": false
|
||||
}
|
||||
},
|
||||
"required": ["id", "type", "properties"],
|
||||
"additionalProperties": false
|
||||
},
|
||||
"Project": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
@@ -11423,24 +11423,6 @@
|
||||
},
|
||||
"payload": {
|
||||
"anyOf": [
|
||||
{
|
||||
"$ref": "#/components/schemas/Event.tui.prompt.append"
|
||||
},
|
||||
{
|
||||
"$ref": "#/components/schemas/Event.tui.command.execute"
|
||||
},
|
||||
{
|
||||
"$ref": "#/components/schemas/Event.tui.toast.show"
|
||||
},
|
||||
{
|
||||
"$ref": "#/components/schemas/Event.tui.session.select"
|
||||
},
|
||||
{
|
||||
"$ref": "#/components/schemas/EventServerConnected"
|
||||
},
|
||||
{
|
||||
"$ref": "#/components/schemas/EventGlobalDisposed"
|
||||
},
|
||||
{
|
||||
"$ref": "#/components/schemas/EventServerInstanceDisposed"
|
||||
},
|
||||
@@ -11489,6 +11471,18 @@
|
||||
{
|
||||
"$ref": "#/components/schemas/EventSessionIdle"
|
||||
},
|
||||
{
|
||||
"$ref": "#/components/schemas/Event.tui.prompt.append"
|
||||
},
|
||||
{
|
||||
"$ref": "#/components/schemas/Event.tui.command.execute"
|
||||
},
|
||||
{
|
||||
"$ref": "#/components/schemas/Event.tui.toast.show"
|
||||
},
|
||||
{
|
||||
"$ref": "#/components/schemas/Event.tui.session.select"
|
||||
},
|
||||
{
|
||||
"$ref": "#/components/schemas/EventMcpToolsChanged"
|
||||
},
|
||||
@@ -11639,6 +11633,12 @@
|
||||
{
|
||||
"$ref": "#/components/schemas/EventSessionNextCompactionEnded"
|
||||
},
|
||||
{
|
||||
"$ref": "#/components/schemas/EventServerConnected"
|
||||
},
|
||||
{
|
||||
"$ref": "#/components/schemas/EventGlobalDisposed"
|
||||
},
|
||||
{
|
||||
"$ref": "#/components/schemas/EventCatalogModelUpdated"
|
||||
},
|
||||
@@ -16400,42 +16400,6 @@
|
||||
"required": ["type", "name", "id", "seq", "aggregateID", "data"],
|
||||
"additionalProperties": false
|
||||
},
|
||||
"EventServerConnected": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"id": {
|
||||
"type": "string"
|
||||
},
|
||||
"type": {
|
||||
"type": "string",
|
||||
"enum": ["server.connected"]
|
||||
},
|
||||
"properties": {
|
||||
"type": "object",
|
||||
"properties": {}
|
||||
}
|
||||
},
|
||||
"required": ["id", "type", "properties"],
|
||||
"additionalProperties": false
|
||||
},
|
||||
"EventGlobalDisposed": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"id": {
|
||||
"type": "string"
|
||||
},
|
||||
"type": {
|
||||
"type": "string",
|
||||
"enum": ["global.disposed"]
|
||||
},
|
||||
"properties": {
|
||||
"type": "object",
|
||||
"properties": {}
|
||||
}
|
||||
},
|
||||
"required": ["id", "type", "properties"],
|
||||
"additionalProperties": false
|
||||
},
|
||||
"EventServerInstanceDisposed": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
@@ -18639,6 +18603,42 @@
|
||||
"required": ["id", "type", "properties"],
|
||||
"additionalProperties": false
|
||||
},
|
||||
"EventServerConnected": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"id": {
|
||||
"type": "string"
|
||||
},
|
||||
"type": {
|
||||
"type": "string",
|
||||
"enum": ["server.connected"]
|
||||
},
|
||||
"properties": {
|
||||
"type": "object",
|
||||
"properties": {}
|
||||
}
|
||||
},
|
||||
"required": ["id", "type", "properties"],
|
||||
"additionalProperties": false
|
||||
},
|
||||
"EventGlobalDisposed": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"id": {
|
||||
"type": "string"
|
||||
},
|
||||
"type": {
|
||||
"type": "string",
|
||||
"enum": ["global.disposed"]
|
||||
},
|
||||
"properties": {
|
||||
"type": "object",
|
||||
"properties": {}
|
||||
}
|
||||
},
|
||||
"required": ["id", "type", "properties"],
|
||||
"additionalProperties": false
|
||||
},
|
||||
"ModelV2Info": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
|
||||
@@ -318,7 +318,7 @@ const TOOL_SAMPLES = {
|
||||
tool: "bash",
|
||||
input: { command: "bun test --filter session", description: "Run session tests" },
|
||||
output:
|
||||
"bun test v1.3.14\n\n✓ session-turn.test.tsx (3 tests) 45ms\n✓ message-part.test.tsx (7 tests) 120ms\n\nTest Suites: 2 passed, 2 total\nTests: 10 passed, 10 total\nTime: 0.89s",
|
||||
"bun test v1.3.13\n\n✓ session-turn.test.tsx (3 tests) 45ms\n✓ message-part.test.tsx (7 tests) 120ms\n\nTest Suites: 2 passed, 2 total\nTests: 10 passed, 10 total\nTime: 0.89s",
|
||||
title: "Run session tests",
|
||||
metadata: { command: "bun test --filter session" },
|
||||
},
|
||||
|
||||
@@ -147,10 +147,6 @@ function applyThemeCss(theme: DesktopTheme, themeId: string, mode: "light" | "da
|
||||
ensureThemeStyleElement().textContent = fullCss
|
||||
document.documentElement.dataset.theme = themeId
|
||||
document.documentElement.dataset.colorScheme = mode
|
||||
|
||||
// Update theme-color meta tag to match light/dark mode
|
||||
const meta = document.querySelector('meta[name="theme-color"]')
|
||||
if (meta) meta.setAttribute("content", isDark ? "#131010" : "#F8F7F7")
|
||||
}
|
||||
|
||||
function cacheThemeVariants(theme: DesktopTheme, themeId: string) {
|
||||
|
||||
@@ -145,7 +145,6 @@ OpenCode has a list of keybinds that you can customize through `tui.json`.
|
||||
"dialog.select.home": "home",
|
||||
"dialog.select.end": "end",
|
||||
"dialog.select.submit": "return",
|
||||
"dialog.prompt.submit": "return",
|
||||
"dialog.mcp.toggle": "space",
|
||||
"prompt.autocomplete.prev": "up,ctrl+p",
|
||||
"prompt.autocomplete.next": "down,ctrl+n",
|
||||
|
||||
+10
-52
@@ -2,33 +2,9 @@
|
||||
|
||||
import path from "node:path"
|
||||
|
||||
const args = process.argv.slice(2)
|
||||
const usage = "Usage: bun run script/upgrade-opentui.ts [--snapshot] <version>"
|
||||
|
||||
if (args.includes("--help") || args.includes("-h")) {
|
||||
console.log(usage)
|
||||
process.exit(0)
|
||||
}
|
||||
|
||||
const snapshotArg = args.find((arg) => arg.startsWith("--snapshot="))
|
||||
const snapshot = args.includes("--snapshot") || snapshotArg !== undefined
|
||||
const unknown = args.find((arg) => arg.startsWith("-") && arg !== "--snapshot" && !arg.startsWith("--snapshot="))
|
||||
if (unknown) {
|
||||
console.error(`Unknown option: ${unknown}`)
|
||||
console.error(usage)
|
||||
process.exit(1)
|
||||
}
|
||||
|
||||
const positional = args.filter((arg) => arg !== "--snapshot" && !arg.startsWith("--snapshot="))
|
||||
const raw = snapshotArg?.slice("--snapshot=".length) || positional[0]
|
||||
if (!raw || positional.length > (snapshotArg ? 0 : 1)) {
|
||||
console.error(usage)
|
||||
process.exit(1)
|
||||
}
|
||||
|
||||
if (snapshotArg === "--snapshot=") {
|
||||
console.error("Missing snapshot version")
|
||||
console.error(usage)
|
||||
const raw = process.argv[2]
|
||||
if (!raw) {
|
||||
console.error("Usage: bun run script/upgrade-opentui.ts <version>")
|
||||
process.exit(1)
|
||||
}
|
||||
|
||||
@@ -41,24 +17,22 @@ const files = (await Array.fromAsync(new Bun.Glob("**/package.json").scan({ cwd:
|
||||
(file) => !file.split("/").some((part) => skip.has(part)),
|
||||
)
|
||||
|
||||
const setVersion = (cur: string, kind: "dep" | "peer") => {
|
||||
const setVersion = (cur: string) => {
|
||||
if (cur === "catalog:" || cur.startsWith("workspace:")) return cur
|
||||
if (snapshot) return ver
|
||||
if (kind === "peer") return `>=${ver}`
|
||||
if (cur.startsWith(">=")) return `>=${ver}`
|
||||
if (cur.startsWith("^")) return `^${ver}`
|
||||
if (cur.startsWith("~")) return `~${ver}`
|
||||
return ver
|
||||
}
|
||||
|
||||
const editDeps = (obj: unknown, kind: "dep" | "peer") => {
|
||||
const editDeps = (obj: unknown) => {
|
||||
if (!obj || typeof obj !== "object") return false
|
||||
const map = obj as Record<string, unknown>
|
||||
return keys
|
||||
.map((key) => {
|
||||
const cur = map[key]
|
||||
if (typeof cur !== "string") return false
|
||||
const next = setVersion(cur, kind)
|
||||
const next = setVersion(cur)
|
||||
if (next === cur) return false
|
||||
map[key] = next
|
||||
return true
|
||||
@@ -79,21 +53,6 @@ const editCatalog = (obj: unknown) => {
|
||||
.some(Boolean)
|
||||
}
|
||||
|
||||
const editOverrides = (obj: unknown) => {
|
||||
if (!obj || typeof obj !== "object") return false
|
||||
const map = obj as Record<string, unknown>
|
||||
return keys
|
||||
.map((key) => {
|
||||
const cur = map[key]
|
||||
if (typeof cur !== "string") return false
|
||||
const next = snapshot ? ver : "catalog:"
|
||||
if (next === cur) return false
|
||||
map[key] = next
|
||||
return true
|
||||
})
|
||||
.some(Boolean)
|
||||
}
|
||||
|
||||
const out = (
|
||||
await Promise.all(
|
||||
files.map(async (rel) => {
|
||||
@@ -102,10 +61,9 @@ const out = (
|
||||
const json = JSON.parse(txt)
|
||||
const hit = [
|
||||
editCatalog(json.workspaces?.catalog),
|
||||
editOverrides(json.overrides),
|
||||
editDeps(json.dependencies, "dep"),
|
||||
editDeps(json.devDependencies, "dep"),
|
||||
editDeps(json.peerDependencies, "peer"),
|
||||
editDeps(json.dependencies),
|
||||
editDeps(json.devDependencies),
|
||||
editDeps(json.peerDependencies),
|
||||
].some(Boolean)
|
||||
if (!hit) return null
|
||||
await Bun.write(file, `${JSON.stringify(json, null, 2)}\n`)
|
||||
@@ -119,7 +77,7 @@ if (out.length === 0) {
|
||||
process.exit(0)
|
||||
}
|
||||
|
||||
console.log(`Updated opentui${snapshot ? " snapshot" : ""} to ${ver} in:`)
|
||||
console.log(`Updated opentui to ${ver} in:`)
|
||||
for (const file of out) {
|
||||
console.log(`- ${file}`)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user