mirror of
https://fastgit.cc/https://github.com/anomalyco/opencode
synced 2026-05-03 15:21:31 +08:00
Compare commits
9 Commits
v1.14.33
...
kit/instan
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
481f8667a4 | ||
|
|
d9252a09dd | ||
|
|
74373f85c7 | ||
|
|
1b146ad094 | ||
|
|
8a63cbe79c | ||
|
|
c565bd54e2 | ||
|
|
f0136f947b | ||
|
|
f1470c1a88 | ||
|
|
f5398e7e1e |
88
bun.lock
88
bun.lock
@@ -29,7 +29,7 @@
|
||||
},
|
||||
"packages/app": {
|
||||
"name": "@opencode-ai/app",
|
||||
"version": "1.14.33",
|
||||
"version": "1.14.31",
|
||||
"dependencies": {
|
||||
"@kobalte/core": "catalog:",
|
||||
"@opencode-ai/core": "workspace:*",
|
||||
@@ -85,7 +85,7 @@
|
||||
},
|
||||
"packages/console/app": {
|
||||
"name": "@opencode-ai/console-app",
|
||||
"version": "1.14.33",
|
||||
"version": "1.14.31",
|
||||
"dependencies": {
|
||||
"@cloudflare/vite-plugin": "1.15.2",
|
||||
"@ibm/plex": "6.4.1",
|
||||
@@ -119,7 +119,7 @@
|
||||
},
|
||||
"packages/console/core": {
|
||||
"name": "@opencode-ai/console-core",
|
||||
"version": "1.14.33",
|
||||
"version": "1.14.31",
|
||||
"dependencies": {
|
||||
"@aws-sdk/client-sts": "3.782.0",
|
||||
"@jsx-email/render": "1.1.1",
|
||||
@@ -146,7 +146,7 @@
|
||||
},
|
||||
"packages/console/function": {
|
||||
"name": "@opencode-ai/console-function",
|
||||
"version": "1.14.33",
|
||||
"version": "1.14.31",
|
||||
"dependencies": {
|
||||
"@ai-sdk/anthropic": "3.0.64",
|
||||
"@ai-sdk/openai": "3.0.48",
|
||||
@@ -170,7 +170,7 @@
|
||||
},
|
||||
"packages/console/mail": {
|
||||
"name": "@opencode-ai/console-mail",
|
||||
"version": "1.14.33",
|
||||
"version": "1.14.31",
|
||||
"dependencies": {
|
||||
"@jsx-email/all": "2.2.3",
|
||||
"@jsx-email/cli": "1.4.3",
|
||||
@@ -194,7 +194,7 @@
|
||||
},
|
||||
"packages/core": {
|
||||
"name": "@opencode-ai/core",
|
||||
"version": "1.14.33",
|
||||
"version": "1.14.31",
|
||||
"bin": {
|
||||
"opencode": "./bin/opencode",
|
||||
},
|
||||
@@ -228,7 +228,7 @@
|
||||
},
|
||||
"packages/desktop": {
|
||||
"name": "@opencode-ai/desktop",
|
||||
"version": "1.14.33",
|
||||
"version": "1.14.31",
|
||||
"dependencies": {
|
||||
"@opencode-ai/app": "workspace:*",
|
||||
"@opencode-ai/ui": "workspace:*",
|
||||
@@ -263,7 +263,7 @@
|
||||
},
|
||||
"packages/desktop-electron": {
|
||||
"name": "@opencode-ai/desktop-electron",
|
||||
"version": "1.14.33",
|
||||
"version": "1.14.31",
|
||||
"dependencies": {
|
||||
"drizzle-orm": "catalog:",
|
||||
"effect": "catalog:",
|
||||
@@ -309,7 +309,7 @@
|
||||
},
|
||||
"packages/enterprise": {
|
||||
"name": "@opencode-ai/enterprise",
|
||||
"version": "1.14.33",
|
||||
"version": "1.14.31",
|
||||
"dependencies": {
|
||||
"@opencode-ai/core": "workspace:*",
|
||||
"@opencode-ai/ui": "workspace:*",
|
||||
@@ -338,7 +338,7 @@
|
||||
},
|
||||
"packages/function": {
|
||||
"name": "@opencode-ai/function",
|
||||
"version": "1.14.33",
|
||||
"version": "1.14.31",
|
||||
"dependencies": {
|
||||
"@octokit/auth-app": "8.0.1",
|
||||
"@octokit/rest": "catalog:",
|
||||
@@ -354,7 +354,7 @@
|
||||
},
|
||||
"packages/opencode": {
|
||||
"name": "opencode",
|
||||
"version": "1.14.33",
|
||||
"version": "1.14.31",
|
||||
"bin": {
|
||||
"opencode": "./bin/opencode",
|
||||
},
|
||||
@@ -496,7 +496,7 @@
|
||||
},
|
||||
"packages/plugin": {
|
||||
"name": "@opencode-ai/plugin",
|
||||
"version": "1.14.33",
|
||||
"version": "1.14.31",
|
||||
"dependencies": {
|
||||
"@opencode-ai/sdk": "workspace:*",
|
||||
"effect": "catalog:",
|
||||
@@ -511,8 +511,8 @@
|
||||
"typescript": "catalog:",
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@opentui/core": ">=0.2.2",
|
||||
"@opentui/solid": ">=0.2.2",
|
||||
"@opentui/core": ">=0.2.0",
|
||||
"@opentui/solid": ">=0.2.0",
|
||||
},
|
||||
"optionalPeers": [
|
||||
"@opentui/core",
|
||||
@@ -531,7 +531,7 @@
|
||||
},
|
||||
"packages/sdk/js": {
|
||||
"name": "@opencode-ai/sdk",
|
||||
"version": "1.14.33",
|
||||
"version": "1.14.31",
|
||||
"dependencies": {
|
||||
"cross-spawn": "catalog:",
|
||||
},
|
||||
@@ -546,7 +546,7 @@
|
||||
},
|
||||
"packages/slack": {
|
||||
"name": "@opencode-ai/slack",
|
||||
"version": "1.14.33",
|
||||
"version": "1.14.31",
|
||||
"dependencies": {
|
||||
"@opencode-ai/sdk": "workspace:*",
|
||||
"@slack/bolt": "^3.17.1",
|
||||
@@ -581,7 +581,7 @@
|
||||
},
|
||||
"packages/ui": {
|
||||
"name": "@opencode-ai/ui",
|
||||
"version": "1.14.33",
|
||||
"version": "1.14.31",
|
||||
"dependencies": {
|
||||
"@kobalte/core": "catalog:",
|
||||
"@opencode-ai/core": "workspace:*",
|
||||
@@ -630,7 +630,7 @@
|
||||
},
|
||||
"packages/web": {
|
||||
"name": "@opencode-ai/web",
|
||||
"version": "1.14.33",
|
||||
"version": "1.14.31",
|
||||
"dependencies": {
|
||||
"@astrojs/cloudflare": "12.6.3",
|
||||
"@astrojs/markdown-remark": "6.3.1",
|
||||
@@ -690,8 +690,8 @@
|
||||
"@npmcli/arborist": "9.4.0",
|
||||
"@octokit/rest": "22.0.0",
|
||||
"@openauthjs/openauth": "0.0.0-20250322224806",
|
||||
"@opentui/core": "0.2.2",
|
||||
"@opentui/solid": "0.2.2",
|
||||
"@opentui/core": "0.2.0",
|
||||
"@opentui/solid": "0.2.0",
|
||||
"@pierre/diffs": "1.1.0-beta.18",
|
||||
"@playwright/test": "1.59.1",
|
||||
"@sentry/solid": "10.36.0",
|
||||
@@ -1618,21 +1618,21 @@
|
||||
|
||||
"@opentelemetry/semantic-conventions": ["@opentelemetry/semantic-conventions@1.40.0", "", {}, "sha512-cifvXDhcqMwwTlTK04GBNeIe7yyo28Mfby85QXFe1Yk8nmi36Ab/5UQwptOx84SsoGNRg+EVSjwzfSZMy6pmlw=="],
|
||||
|
||||
"@opentui/core": ["@opentui/core@0.2.2", "", { "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.2", "@opentui/core-darwin-x64": "0.2.2", "@opentui/core-linux-arm64": "0.2.2", "@opentui/core-linux-x64": "0.2.2", "@opentui/core-win32-arm64": "0.2.2", "@opentui/core-win32-x64": "0.2.2" }, "peerDependencies": { "web-tree-sitter": "0.25.10" } }, "sha512-wxg1CD58SVrowu+WgbhZNi3UP/wWxPio2Kj2IeTjomoIE+6EXLxR8eCCxHYVuQUd9E4fknrKkY5HmiSsp6oPow=="],
|
||||
"@opentui/core": ["@opentui/core@0.2.0", "", { "dependencies": { "bun-ffi-structs": "0.1.2", "diff": "8.0.2", "jimp": "1.6.0", "marked": "17.0.1", "string-width": "7.2.0", "strip-ansi": "7.1.2", "yoga-layout": "3.2.1" }, "optionalDependencies": { "@dimforge/rapier2d-simd-compat": "^0.17.3", "@opentui/core-darwin-arm64": "0.2.0", "@opentui/core-darwin-x64": "0.2.0", "@opentui/core-linux-arm64": "0.2.0", "@opentui/core-linux-x64": "0.2.0", "@opentui/core-win32-arm64": "0.2.0", "@opentui/core-win32-x64": "0.2.0", "bun-webgpu": "0.1.7", "planck": "^1.4.2", "three": "0.177.0" }, "peerDependencies": { "web-tree-sitter": "0.25.10" } }, "sha512-7YOEqPUQmsgrOb9nmLEBlX8RVHPFy4HquK1C489DwfvvPTiws8nTbZ+webNQDWha7shgnYQK4Zo1EcOlpQ5+1Q=="],
|
||||
|
||||
"@opentui/core-darwin-arm64": ["@opentui/core-darwin-arm64@0.2.2", "", { "os": "darwin", "cpu": "arm64" }, "sha512-tY5n3ZRQx+b0kyhQJJLsyJMeZ+0w4FV37YZc/Qqv3qvOqE9kZPw/7adR77FYwWDm/7fax94mLMrR8Y5bKUkDmw=="],
|
||||
"@opentui/core-darwin-arm64": ["@opentui/core-darwin-arm64@0.2.0", "", { "os": "darwin", "cpu": "arm64" }, "sha512-VVmKwth3hzsQPjAZ7WGJxmzuzx0uCtynd79JJDg26D7QRM9V5beVGbKwwU5SKsDlK74EyQoY85Mv9xFY5E4jrA=="],
|
||||
|
||||
"@opentui/core-darwin-x64": ["@opentui/core-darwin-x64@0.2.2", "", { "os": "darwin", "cpu": "x64" }, "sha512-W/R7OnqY30FXcTG0tiP2JkQFmgtYbIte5afQ5PC12TliRoee1RqG3iCG6kY1jxW+3Vg6jge88uiSjUEDpeV2gA=="],
|
||||
"@opentui/core-darwin-x64": ["@opentui/core-darwin-x64@0.2.0", "", { "os": "darwin", "cpu": "x64" }, "sha512-eX+WNdbSNr7Bozdq/MH6p1vXIALGt0SqBHR4YtWyTh6X7KDz9FTtJT3ylxMPqiVRUGBNAiWOxoqKGXW7JLQ0TA=="],
|
||||
|
||||
"@opentui/core-linux-arm64": ["@opentui/core-linux-arm64@0.2.2", "", { "os": "linux", "cpu": "arm64" }, "sha512-1pzTYFEZauYuw6AGycw2TYGtAlZVGjuUtSdxH1fP51kBPS3oVWduUY2j7GKREz3SU5NulvO2Wc6HWsm3feMqwQ=="],
|
||||
"@opentui/core-linux-arm64": ["@opentui/core-linux-arm64@0.2.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-ARZa+ywbN/OV7esT5ZdJMlQW3a4Pr56qLlEI/X65ik88C2sgmDze4Kf2FmqtvJ1hbv1YsMfLHH9MfhLl5twyHQ=="],
|
||||
|
||||
"@opentui/core-linux-x64": ["@opentui/core-linux-x64@0.2.2", "", { "os": "linux", "cpu": "x64" }, "sha512-ucVwUtUYeOYGVFPBLbPoxzbrPdhD0PDyKNQ2X4n1AJ9jlQX4gqBZRcXMEF8hiXDjFxsikZwef7De0ciCcWvAMg=="],
|
||||
"@opentui/core-linux-x64": ["@opentui/core-linux-x64@0.2.0", "", { "os": "linux", "cpu": "x64" }, "sha512-ZjNxrD45P51cdbABoivVQLBakVYwDqAridJbHhkK6T/+EU7YsTrmAu9ae19N9ZGnrlKzLViQF8GOavNUNjAbhw=="],
|
||||
|
||||
"@opentui/core-win32-arm64": ["@opentui/core-win32-arm64@0.2.2", "", { "os": "win32", "cpu": "arm64" }, "sha512-MPhYdJNdxmC5Bqsq6sis/+VkjRgkEjm+bQ1Tl++NSKLuiTU32Re0ImcZlgHbe+LZtZoGMZHVSgZlkGd3oYXO2g=="],
|
||||
"@opentui/core-win32-arm64": ["@opentui/core-win32-arm64@0.2.0", "", { "os": "win32", "cpu": "arm64" }, "sha512-ImMjFPOWE8wcZQ2lUz1D418xonS/5EwnItUF1g5dbp1q9+A0vv2P3bxTenLwMqcYvG4wjO6gKT3n2QLnRd6qKg=="],
|
||||
|
||||
"@opentui/core-win32-x64": ["@opentui/core-win32-x64@0.2.2", "", { "os": "win32", "cpu": "x64" }, "sha512-19BroLfn2h0RDYfJS5o96Fc8kYCDhRBcseIXtHIkoKIsKMxx62KiDLo/byVye6rp+yQRRB7Xkd2uWqsbdiWo9w=="],
|
||||
"@opentui/core-win32-x64": ["@opentui/core-win32-x64@0.2.0", "", { "os": "win32", "cpu": "x64" }, "sha512-6yfYHTtJ4yzbl8kXCW3Pc4eWbZDYVw21GumwdNgkjJJ2JqQAQ861em0riEoucYAa5qPYYTiMUEw7X4Fv8lGwuQ=="],
|
||||
|
||||
"@opentui/solid": ["@opentui/solid@0.2.2", "", { "dependencies": { "@babel/core": "7.28.0", "@babel/preset-typescript": "7.27.1", "@opentui/core": "0.2.2", "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-ZBVfCoVAhcUGQWPAWOTdzuVldMaRkuPpCu4U1VZCqmIw9DtbCuiVr0WnDocDxKhJLbTu8bl3qEWtVCf6lTSi3w=="],
|
||||
"@opentui/solid": ["@opentui/solid@0.2.0", "", { "dependencies": { "@babel/core": "7.28.0", "@babel/preset-typescript": "7.27.1", "@opentui/core": "0.2.0", "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-kZR9i0FPAcVtomrPsKuSb+D9smooplo9zggFfU2vnnguNuQjGNbEmuJtxhCacy7ig9g3GomdNtQAzD4LiAY+3w=="],
|
||||
|
||||
"@oslojs/asn1": ["@oslojs/asn1@1.0.0", "", { "dependencies": { "@oslojs/binary": "1.0.0" } }, "sha512-zw/wn0sj0j0QKbIXfIlnEcTviaCzYOY3V5rAyjR6YtOByFtJiT574+8p9Wlach0lZH9fddD4yb9laEAIl4vXQA=="],
|
||||
|
||||
@@ -2768,21 +2768,21 @@
|
||||
|
||||
"builder-util-runtime": ["builder-util-runtime@9.5.1", "", { "dependencies": { "debug": "^4.3.4", "sax": "^1.2.4" } }, "sha512-qt41tMfgHTllhResqM5DcnHyDIWNgzHvuY2jDcYP9iaGpkWxTUzV6GQjDeLnlR1/DtdlcsWQbA7sByMpmJFTLQ=="],
|
||||
|
||||
"bun-ffi-structs": ["bun-ffi-structs@0.2.2", "", { "peerDependencies": { "typescript": "^5" } }, "sha512-N/ZWtyN0piZlrXQT7TO0V+q952orYqkfhXRXM1Hcbb+R3QSiBH4vLnib187Mrs1H7pWIYECAmPeapGYDOMCl+w=="],
|
||||
"bun-ffi-structs": ["bun-ffi-structs@0.1.2", "", { "peerDependencies": { "typescript": "^5" } }, "sha512-Lh1oQAYHDcnesJauieA4UNkWGXY9hYck7OA5IaRwE3Bp6K2F2pJSNYqq+hIy7P3uOvo3km3oxS8304g5gDMl/w=="],
|
||||
|
||||
"bun-pty": ["bun-pty@0.4.8", "", {}, "sha512-rO70Mrbr13+jxHHHu2YBkk2pNqrJE5cJn29WE++PUr+GFA0hq/VgtQPZANJ8dJo6d7XImvBk37Innt8GM7O28w=="],
|
||||
|
||||
"bun-types": ["bun-types@1.3.12", "", { "dependencies": { "@types/node": "*" } }, "sha512-HqOLj5PoFajAQciOMRiIZGNoKxDJSr6qigAttOX40vJuSp6DN/CxWp9s3C1Xwm4oH7ybueITwiaOcWXoYVoRkA=="],
|
||||
|
||||
"bun-webgpu": ["bun-webgpu@0.1.5", "", { "dependencies": { "@webgpu/types": "^0.1.60" }, "optionalDependencies": { "bun-webgpu-darwin-arm64": "^0.1.5", "bun-webgpu-darwin-x64": "^0.1.5", "bun-webgpu-linux-x64": "^0.1.5", "bun-webgpu-win32-x64": "^0.1.5" } }, "sha512-91/K6S5whZKX7CWAm9AylhyKrLGRz6BUiiPiM/kXadSnD4rffljCD/q9cNFftm5YXhx4MvLqw33yEilxogJvwA=="],
|
||||
"bun-webgpu": ["bun-webgpu@0.1.7", "", { "dependencies": { "@webgpu/types": "^0.1.60" }, "optionalDependencies": { "bun-webgpu-darwin-arm64": "^0.1.7", "bun-webgpu-darwin-x64": "^0.1.7", "bun-webgpu-linux-x64": "^0.1.7", "bun-webgpu-win32-x64": "^0.1.7" } }, "sha512-KUxUp+oQIf7pPBMD4Hv1TUu7DWaOZ4ciKulTk9to9+Uc8yHoYrMW7L2SJCJ4FHHkywgf/7aLRgRx0b7i6DvGIQ=="],
|
||||
|
||||
"bun-webgpu-darwin-arm64": ["bun-webgpu-darwin-arm64@0.1.6", "", { "os": "darwin", "cpu": "arm64" }, "sha512-lIsDkPzJzPl6yrB5CUOINJFPnTRv6fF/Q8J1mAr43ogSp86WZEg9XZKaT6f3EUJ+9ETogGoMnoj1q0AwHUTbAQ=="],
|
||||
"bun-webgpu-darwin-arm64": ["bun-webgpu-darwin-arm64@0.1.7", "", { "os": "darwin", "cpu": "arm64" }, "sha512-mRrFFyHzPWjsTRidAZBRcu808CPQBOUL0P6b4nxLhp+XHcV/mbUHERZMgW9s58tsojQfSdzschiQa8q+JCgRWA=="],
|
||||
|
||||
"bun-webgpu-darwin-x64": ["bun-webgpu-darwin-x64@0.1.6", "", { "os": "darwin", "cpu": "x64" }, "sha512-uEddf5U7GvKIkM/BV18rUKtYHL6d0KeqBjNHwfqDH9QgEo9KVSKvJXS5I/sMefk5V5pIYE+8tQhtrREevhocng=="],
|
||||
"bun-webgpu-darwin-x64": ["bun-webgpu-darwin-x64@0.1.7", "", { "os": "darwin", "cpu": "x64" }, "sha512-g0NXGNgvaVCSH/jCWWlfdiquOHkbUN6vP4zqzSkIxWKQeLnqm3oADcok7SO3yIgI7v5mKpRc/ks7NDEKNH+jNQ=="],
|
||||
|
||||
"bun-webgpu-linux-x64": ["bun-webgpu-linux-x64@0.1.6", "", { "os": "linux", "cpu": "x64" }, "sha512-Y/f15j9r8ba0xUz+3lATtS74OE+PPzQXO7Do/1eCluJcuOlfa77kMjvBK/ShWnem3Y9xqi59pebTPOGRB+CaJA=="],
|
||||
"bun-webgpu-linux-x64": ["bun-webgpu-linux-x64@0.1.7", "", { "os": "linux", "cpu": "x64" }, "sha512-UEP7UZdEhx9otvkZczjsszL8ZVlrODANQvgl+C88/bNVmxDoFi7w1fWzGi1sZyakiETjmtFDq2/xCLhbSZxjqw=="],
|
||||
|
||||
"bun-webgpu-win32-x64": ["bun-webgpu-win32-x64@0.1.6", "", { "os": "win32", "cpu": "x64" }, "sha512-MHSFAKqizISb+C5NfDrFe3g0Al5Njnu0j/A+oO2Q+bIWX+fUYjBSowiYE1ZXJx65KuryuB+tiM7Qh6cQbVvkEg=="],
|
||||
"bun-webgpu-win32-x64": ["bun-webgpu-win32-x64@0.1.7", "", { "os": "win32", "cpu": "x64" }, "sha512-KZktiFkBz6sN7PEm1NVdeaLP5Q5X/PlSHZqefY4nNuWtf0LNvh54NhZe7yVv/Plz/nGbv92b0KHMBY3ki/pp6g=="],
|
||||
|
||||
"bundle-name": ["bundle-name@4.1.0", "", { "dependencies": { "run-applescript": "^7.0.0" } }, "sha512-tjwM5exMg6BGRI+kNmTntNsvdZS1X8BFYS6tnJ2hdH0kVxM6/eVZ2xy+FqStSWvYmtfFMDLIxurorHwDKfDz5Q=="],
|
||||
|
||||
@@ -4204,7 +4204,7 @@
|
||||
|
||||
"pagefind": ["pagefind@1.5.2", "", { "optionalDependencies": { "@pagefind/darwin-arm64": "1.5.2", "@pagefind/darwin-x64": "1.5.2", "@pagefind/freebsd-x64": "1.5.2", "@pagefind/linux-arm64": "1.5.2", "@pagefind/linux-x64": "1.5.2", "@pagefind/windows-arm64": "1.5.2", "@pagefind/windows-x64": "1.5.2" }, "bin": { "pagefind": "lib/runner/bin.cjs" } }, "sha512-XTUaK0hXMCu2jszWE584JGQT7y284TmMV9l/HX3rnG5uo3rHI/uHU56XTyyyPFjeWEBxECbAi0CaFDJOONtG0Q=="],
|
||||
|
||||
"pako": ["pako@0.2.9", "", {}, "sha512-NUcwaKxUxWrZLpDG+z/xZaCgQITkA/Dv4V/T6bw7VON6l1Xz/VnrBqrYjZQ12TamKHzITTfOEIYUj48y2KXImA=="],
|
||||
"pako": ["pako@1.0.11", "", {}, "sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw=="],
|
||||
|
||||
"param-case": ["param-case@3.0.4", "", { "dependencies": { "dot-case": "^3.0.4", "tslib": "^2.0.3" } }, "sha512-RXlj7zCYokReqWpOPH9oYivUzLYZ5vAPIfEmCTNViosC78F8F0H9y7T7gG2M39ymgutxF5gcFEsyZQSph9Bp3A=="],
|
||||
|
||||
@@ -5640,8 +5640,6 @@
|
||||
|
||||
"@opencode-ai/web/@shikijs/transformers": ["@shikijs/transformers@3.20.0", "", { "dependencies": { "@shikijs/core": "3.20.0", "@shikijs/types": "3.20.0" } }, "sha512-PrHHMRr3Q5W1qB/42kJW6laqFyWdhrPF2hNR9qjOm1xcSiAO3hAHo7HaVyHE6pMyevmy3i51O8kuGGXC78uK3g=="],
|
||||
|
||||
"@opentui/core/diff": ["diff@9.0.0", "", {}, "sha512-svtcdpS8CgJyqAjEQIXdb3OjhFVVYjzGAPO8WGCmRbrml64SPw/jJD4GoE98aR7r25A0XcgrK3F02yw9R/vhQw=="],
|
||||
|
||||
"@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=="],
|
||||
|
||||
"@oslojs/jwt/@oslojs/encoding": ["@oslojs/encoding@0.4.1", "", {}, "sha512-hkjo6MuIK/kQR5CrGNdAPZhS01ZCXuWDRJ187zh6qqF2+yMHZpD9fAYpX8q2bOO6Ryhl3XpCT6kUX76N8hhm4Q=="],
|
||||
@@ -6124,6 +6122,8 @@
|
||||
|
||||
"type-is/mime-types": ["mime-types@2.1.35", "", { "dependencies": { "mime-db": "1.52.0" } }, "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw=="],
|
||||
|
||||
"unicode-trie/pako": ["pako@0.2.9", "", {}, "sha512-NUcwaKxUxWrZLpDG+z/xZaCgQITkA/Dv4V/T6bw7VON6l1Xz/VnrBqrYjZQ12TamKHzITTfOEIYUj48y2KXImA=="],
|
||||
|
||||
"unifont/ofetch": ["ofetch@1.5.1", "", { "dependencies": { "destr": "^2.0.5", "node-fetch-native": "^1.6.7", "ufo": "^1.6.1" } }, "sha512-2W4oUZlVaqAPAil6FUg/difl6YhqhUR7x2eZY4bQCko22UXg3hptq9KLQdqFClV+Wu85UX7hNtdGTngi/1BxcA=="],
|
||||
|
||||
"unplugin/chokidar": ["chokidar@3.6.0", "", { "dependencies": { "anymatch": "~3.1.2", "braces": "~3.0.2", "glob-parent": "~5.1.2", "is-binary-path": "~2.1.0", "is-glob": "~4.0.1", "normalize-path": "~3.0.0", "readdirp": "~3.6.0" }, "optionalDependencies": { "fsevents": "~2.3.2" } }, "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw=="],
|
||||
@@ -6132,8 +6132,6 @@
|
||||
|
||||
"uri-js/punycode": ["punycode@2.3.1", "", {}, "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg=="],
|
||||
|
||||
"utif2/pako": ["pako@1.0.11", "", {}, "sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw=="],
|
||||
|
||||
"venice-ai-sdk-provider/@ai-sdk/openai-compatible": ["@ai-sdk/openai-compatible@2.0.41", "", { "dependencies": { "@ai-sdk/provider": "3.0.8", "@ai-sdk/provider-utils": "4.0.23" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-kNAGINk71AlOXx10Dq/PXw4t/9XjdK8uxfpVElRwtSFMdeSiLVt58p9TPx4/FJD+hxZuVhvxYj9r42osxWq79g=="],
|
||||
|
||||
"vite-plugin-icons-spritesheet/glob": ["glob@11.1.0", "", { "dependencies": { "foreground-child": "^3.3.1", "jackspeak": "^4.1.1", "minimatch": "^10.1.1", "minipass": "^7.1.2", "package-json-from-dist": "^1.0.0", "path-scurry": "^2.0.0" }, "bin": { "glob": "dist/esm/bin.mjs" } }, "sha512-vuNwKSaKiqm7g0THUBu2x7ckSs3XJLXE+2ssL7/MfTGPLLcrJQ/4Uq1CjPTtO5cCIiRxqvN6Twy1qOwhL0Xjcw=="],
|
||||
@@ -6800,7 +6798,7 @@
|
||||
|
||||
"opentui-spinner/@opentui/core/@opentui/core-win32-x64": ["@opentui/core-win32-x64@0.1.105", "", { "os": "win32", "cpu": "x64" }, "sha512-f9FqqUmxehwhF+cgyazm0YT0v0BYTTCPzd6eztqhl74N3x/kC+jOOz2rdJDC/tTBo1JVsF64KupOnhIs6/Cogg=="],
|
||||
|
||||
"opentui-spinner/@opentui/core/bun-ffi-structs": ["bun-ffi-structs@0.1.2", "", { "peerDependencies": { "typescript": "^5" } }, "sha512-Lh1oQAYHDcnesJauieA4UNkWGXY9hYck7OA5IaRwE3Bp6K2F2pJSNYqq+hIy7P3uOvo3km3oxS8304g5gDMl/w=="],
|
||||
"opentui-spinner/@opentui/core/bun-webgpu": ["bun-webgpu@0.1.5", "", { "dependencies": { "@webgpu/types": "^0.1.60" }, "optionalDependencies": { "bun-webgpu-darwin-arm64": "^0.1.5", "bun-webgpu-darwin-x64": "^0.1.5", "bun-webgpu-linux-x64": "^0.1.5", "bun-webgpu-win32-x64": "^0.1.5" } }, "sha512-91/K6S5whZKX7CWAm9AylhyKrLGRz6BUiiPiM/kXadSnD4rffljCD/q9cNFftm5YXhx4MvLqw33yEilxogJvwA=="],
|
||||
|
||||
"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=="],
|
||||
|
||||
@@ -7160,6 +7158,16 @@
|
||||
|
||||
"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/core/bun-webgpu/@webgpu/types": ["@webgpu/types@0.1.69", "", {}, "sha512-RPmm6kgRbI8e98zSD3RVACvnuktIja5+yLgDAkTmxLr90BEwdTXRQWNLF3ETTTyH/8mKhznZuN5AveXYFEsMGQ=="],
|
||||
|
||||
"opentui-spinner/@opentui/core/bun-webgpu/bun-webgpu-darwin-arm64": ["bun-webgpu-darwin-arm64@0.1.6", "", { "os": "darwin", "cpu": "arm64" }, "sha512-lIsDkPzJzPl6yrB5CUOINJFPnTRv6fF/Q8J1mAr43ogSp86WZEg9XZKaT6f3EUJ+9ETogGoMnoj1q0AwHUTbAQ=="],
|
||||
|
||||
"opentui-spinner/@opentui/core/bun-webgpu/bun-webgpu-darwin-x64": ["bun-webgpu-darwin-x64@0.1.6", "", { "os": "darwin", "cpu": "x64" }, "sha512-uEddf5U7GvKIkM/BV18rUKtYHL6d0KeqBjNHwfqDH9QgEo9KVSKvJXS5I/sMefk5V5pIYE+8tQhtrREevhocng=="],
|
||||
|
||||
"opentui-spinner/@opentui/core/bun-webgpu/bun-webgpu-linux-x64": ["bun-webgpu-linux-x64@0.1.6", "", { "os": "linux", "cpu": "x64" }, "sha512-Y/f15j9r8ba0xUz+3lATtS74OE+PPzQXO7Do/1eCluJcuOlfa77kMjvBK/ShWnem3Y9xqi59pebTPOGRB+CaJA=="],
|
||||
|
||||
"opentui-spinner/@opentui/core/bun-webgpu/bun-webgpu-win32-x64": ["bun-webgpu-win32-x64@0.1.6", "", { "os": "win32", "cpu": "x64" }, "sha512-MHSFAKqizISb+C5NfDrFe3g0Al5Njnu0j/A+oO2Q+bIWX+fUYjBSowiYE1ZXJx65KuryuB+tiM7Qh6cQbVvkEg=="],
|
||||
|
||||
"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=="],
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
{
|
||||
"nodeModules": {
|
||||
"x86_64-linux": "sha256-SLWRe4uPSRWgU+NPa1BywmrUtNVIC0Oy2mjmxclxk+s=",
|
||||
"aarch64-linux": "sha256-toHEeIqMzrmThoV0B52juGKm4pa/aJN3gBFFtrSZp2Q=",
|
||||
"aarch64-darwin": "sha256-lYUsUxq5zR2RXjqZTEdjduOncnlwvTlxDJVKWXJuKPY=",
|
||||
"x86_64-darwin": "sha256-77XmuEYqGwb1mkEHfnghq1VtukFTneohA0FW6WDOk1U="
|
||||
"x86_64-linux": "sha256-OtyfKTBEHsJpjzAjN9vCR0PzGzdK6CDHdyU7eZ6Gl1s=",
|
||||
"aarch64-linux": "sha256-3eHJs3S/+uDUPAouWPsdBOlEvAOhOYx5bJzahL0tAJk=",
|
||||
"aarch64-darwin": "sha256-rFXzrkhPVb3yM20J8R8m7GqroNNk1vAEz+o/Ks+iAI4=",
|
||||
"x86_64-darwin": "sha256-lb1IGgbpxg723Qxj2WVPkxKUUmyOIsFOAhA5LoZ8GwY="
|
||||
}
|
||||
}
|
||||
|
||||
@@ -55,6 +55,7 @@ stdenvNoCC.mkDerivation {
|
||||
--filter './packages/opencode' \
|
||||
--filter './packages/desktop' \
|
||||
--filter './packages/app' \
|
||||
--filter './packages/shared' \
|
||||
--frozen-lockfile \
|
||||
--ignore-scripts \
|
||||
--no-progress
|
||||
|
||||
@@ -34,8 +34,8 @@
|
||||
"@types/cross-spawn": "6.0.6",
|
||||
"@octokit/rest": "22.0.0",
|
||||
"@hono/zod-validator": "0.4.2",
|
||||
"@opentui/core": "0.2.2",
|
||||
"@opentui/solid": "0.2.2",
|
||||
"@opentui/core": "0.2.0",
|
||||
"@opentui/solid": "0.2.0",
|
||||
"ulid": "3.0.1",
|
||||
"@kobalte/core": "0.13.11",
|
||||
"@types/luxon": "3.7.1",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@opencode-ai/app",
|
||||
"version": "1.14.33",
|
||||
"version": "1.14.31",
|
||||
"description": "",
|
||||
"type": "module",
|
||||
"exports": {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@opencode-ai/console-app",
|
||||
"version": "1.14.33",
|
||||
"version": "1.14.31",
|
||||
"type": "module",
|
||||
"license": "MIT",
|
||||
"scripts": {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"$schema": "https://json.schemastore.org/package.json",
|
||||
"name": "@opencode-ai/console-core",
|
||||
"version": "1.14.33",
|
||||
"version": "1.14.31",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"license": "MIT",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@opencode-ai/console-function",
|
||||
"version": "1.14.33",
|
||||
"version": "1.14.31",
|
||||
"$schema": "https://json.schemastore.org/package.json",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@opencode-ai/console-mail",
|
||||
"version": "1.14.33",
|
||||
"version": "1.14.31",
|
||||
"dependencies": {
|
||||
"@jsx-email/all": "2.2.3",
|
||||
"@jsx-email/cli": "1.4.3",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"$schema": "https://json.schemastore.org/package.json",
|
||||
"version": "1.14.33",
|
||||
"version": "1.14.31",
|
||||
"name": "@opencode-ai/core",
|
||||
"type": "module",
|
||||
"license": "MIT",
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "@opencode-ai/desktop-electron",
|
||||
"private": true,
|
||||
"version": "1.14.33",
|
||||
"version": "1.14.31",
|
||||
"type": "module",
|
||||
"license": "MIT",
|
||||
"homepage": "https://opencode.ai",
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "@opencode-ai/desktop",
|
||||
"private": true,
|
||||
"version": "1.14.33",
|
||||
"version": "1.14.31",
|
||||
"type": "module",
|
||||
"license": "MIT",
|
||||
"scripts": {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@opencode-ai/enterprise",
|
||||
"version": "1.14.33",
|
||||
"version": "1.14.31",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"license": "MIT",
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
id = "opencode"
|
||||
name = "OpenCode"
|
||||
description = "The open source coding agent."
|
||||
version = "1.14.33"
|
||||
version = "1.14.31"
|
||||
schema_version = 1
|
||||
authors = ["Anomaly"]
|
||||
repository = "https://github.com/anomalyco/opencode"
|
||||
@@ -11,26 +11,26 @@ name = "OpenCode"
|
||||
icon = "./icons/opencode.svg"
|
||||
|
||||
[agent_servers.opencode.targets.darwin-aarch64]
|
||||
archive = "https://github.com/anomalyco/opencode/releases/download/v1.14.33/opencode-darwin-arm64.zip"
|
||||
archive = "https://github.com/anomalyco/opencode/releases/download/v1.14.31/opencode-darwin-arm64.zip"
|
||||
cmd = "./opencode"
|
||||
args = ["acp"]
|
||||
|
||||
[agent_servers.opencode.targets.darwin-x86_64]
|
||||
archive = "https://github.com/anomalyco/opencode/releases/download/v1.14.33/opencode-darwin-x64.zip"
|
||||
archive = "https://github.com/anomalyco/opencode/releases/download/v1.14.31/opencode-darwin-x64.zip"
|
||||
cmd = "./opencode"
|
||||
args = ["acp"]
|
||||
|
||||
[agent_servers.opencode.targets.linux-aarch64]
|
||||
archive = "https://github.com/anomalyco/opencode/releases/download/v1.14.33/opencode-linux-arm64.tar.gz"
|
||||
archive = "https://github.com/anomalyco/opencode/releases/download/v1.14.31/opencode-linux-arm64.tar.gz"
|
||||
cmd = "./opencode"
|
||||
args = ["acp"]
|
||||
|
||||
[agent_servers.opencode.targets.linux-x86_64]
|
||||
archive = "https://github.com/anomalyco/opencode/releases/download/v1.14.33/opencode-linux-x64.tar.gz"
|
||||
archive = "https://github.com/anomalyco/opencode/releases/download/v1.14.31/opencode-linux-x64.tar.gz"
|
||||
cmd = "./opencode"
|
||||
args = ["acp"]
|
||||
|
||||
[agent_servers.opencode.targets.windows-x86_64]
|
||||
archive = "https://github.com/anomalyco/opencode/releases/download/v1.14.33/opencode-windows-x64.zip"
|
||||
archive = "https://github.com/anomalyco/opencode/releases/download/v1.14.31/opencode-windows-x64.zip"
|
||||
cmd = "./opencode.exe"
|
||||
args = ["acp"]
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@opencode-ai/function",
|
||||
"version": "1.14.33",
|
||||
"version": "1.14.31",
|
||||
"$schema": "https://json.schemastore.org/package.json",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"$schema": "https://json.schemastore.org/package.json",
|
||||
"version": "1.14.33",
|
||||
"version": "1.14.31",
|
||||
"name": "opencode",
|
||||
"type": "module",
|
||||
"license": "MIT",
|
||||
|
||||
@@ -1,17 +1,17 @@
|
||||
import { AppRuntime } from "@/effect/app-runtime"
|
||||
import { InstanceBootstrap } from "../project/bootstrap"
|
||||
import { Instance } from "../project/instance"
|
||||
import { InstanceStore } from "../project/instance-store"
|
||||
import { getBootstrapRunEffect } from "../effect/app-runtime"
|
||||
|
||||
export async function bootstrap<T>(directory: string, cb: () => Promise<T>) {
|
||||
return Instance.provide({
|
||||
directory,
|
||||
init: await getBootstrapRunEffect(),
|
||||
init: () => AppRuntime.runPromise(InstanceBootstrap),
|
||||
fn: async () => {
|
||||
try {
|
||||
const result = await cb()
|
||||
return result
|
||||
} finally {
|
||||
await InstanceStore.disposeInstance(Instance.current)
|
||||
await Instance.dispose()
|
||||
}
|
||||
},
|
||||
})
|
||||
|
||||
@@ -212,7 +212,7 @@ export const GithubInstallCommand = cmd({
|
||||
const app = await getAppInfo()
|
||||
await installGitHubApp()
|
||||
|
||||
const providers = await AppRuntime.runPromise(ModelsDev.Service.use((s) => s.get())).then((p) => {
|
||||
const providers = await ModelsDev.get().then((p) => {
|
||||
// TODO: add guide for copilot, for now just hide it
|
||||
delete p["github-copilot"]
|
||||
return p
|
||||
|
||||
@@ -1,16 +1,19 @@
|
||||
import { EOL } from "os"
|
||||
import { Effect } from "effect"
|
||||
import type { Argv } from "yargs"
|
||||
import { Instance } from "../../project/instance"
|
||||
import { Provider } from "@/provider/provider"
|
||||
import { ProviderID } from "../../provider/schema"
|
||||
import { ModelsDev } from "@/provider/models"
|
||||
import { effectCmd, fail } from "../effect-cmd"
|
||||
import { cmd } from "./cmd"
|
||||
import { UI } from "../ui"
|
||||
import { EOL } from "os"
|
||||
import { AppRuntime } from "@/effect/app-runtime"
|
||||
import { Effect } from "effect"
|
||||
|
||||
export const ModelsCommand = effectCmd({
|
||||
export const ModelsCommand = cmd({
|
||||
command: "models [provider]",
|
||||
describe: "list all available models",
|
||||
builder: (yargs) =>
|
||||
yargs
|
||||
builder: (yargs: Argv) => {
|
||||
return yargs
|
||||
.positional("provider", {
|
||||
describe: "provider ID to filter models by",
|
||||
type: "string",
|
||||
@@ -23,44 +26,63 @@ export const ModelsCommand = effectCmd({
|
||||
.option("refresh", {
|
||||
describe: "refresh the models cache from models.dev",
|
||||
type: "boolean",
|
||||
}),
|
||||
handler: Effect.fn("Cli.models")(function* (args) {
|
||||
})
|
||||
},
|
||||
handler: async (args) => {
|
||||
if (args.refresh) {
|
||||
yield* ModelsDev.Service.use((s) => s.refresh(true))
|
||||
await ModelsDev.refresh(true)
|
||||
UI.println(UI.Style.TEXT_SUCCESS_BOLD + "Models cache refreshed" + UI.Style.TEXT_NORMAL)
|
||||
}
|
||||
|
||||
const provider = yield* Provider.Service
|
||||
const providers = yield* provider.list()
|
||||
await Instance.provide({
|
||||
directory: process.cwd(),
|
||||
async fn() {
|
||||
await AppRuntime.runPromise(
|
||||
Effect.gen(function* () {
|
||||
const svc = yield* Provider.Service
|
||||
const providers = yield* svc.list()
|
||||
|
||||
const print = (providerID: ProviderID, verbose?: boolean) => {
|
||||
const p = providers[providerID]
|
||||
const sorted = Object.entries(p.models).sort(([a], [b]) => a.localeCompare(b))
|
||||
for (const [modelID, model] of sorted) {
|
||||
process.stdout.write(`${providerID}/${modelID}`)
|
||||
process.stdout.write(EOL)
|
||||
if (verbose) {
|
||||
process.stdout.write(JSON.stringify(model, null, 2))
|
||||
process.stdout.write(EOL)
|
||||
}
|
||||
}
|
||||
}
|
||||
const print = (providerID: ProviderID, verbose?: boolean) => {
|
||||
const provider = providers[providerID]
|
||||
const sorted = Object.entries(provider.models).sort(([a], [b]) => a.localeCompare(b))
|
||||
for (const [modelID, model] of sorted) {
|
||||
process.stdout.write(`${providerID}/${modelID}`)
|
||||
process.stdout.write(EOL)
|
||||
if (verbose) {
|
||||
process.stdout.write(JSON.stringify(model, null, 2))
|
||||
process.stdout.write(EOL)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (args.provider) {
|
||||
const providerID = ProviderID.make(args.provider)
|
||||
if (!providers[providerID]) return yield* fail(`Provider not found: ${args.provider}`)
|
||||
print(providerID, args.verbose)
|
||||
return
|
||||
}
|
||||
if (args.provider) {
|
||||
const providerID = ProviderID.make(args.provider)
|
||||
const provider = providers[providerID]
|
||||
if (!provider) {
|
||||
yield* Effect.sync(() => UI.error(`Provider not found: ${args.provider}`))
|
||||
return
|
||||
}
|
||||
|
||||
const ids = Object.keys(providers).sort((a, b) => {
|
||||
const aIsOpencode = a.startsWith("opencode")
|
||||
const bIsOpencode = b.startsWith("opencode")
|
||||
if (aIsOpencode && !bIsOpencode) return -1
|
||||
if (!aIsOpencode && bIsOpencode) return 1
|
||||
return a.localeCompare(b)
|
||||
yield* Effect.sync(() => print(providerID, args.verbose))
|
||||
return
|
||||
}
|
||||
|
||||
const ids = Object.keys(providers).sort((a, b) => {
|
||||
const aIsOpencode = a.startsWith("opencode")
|
||||
const bIsOpencode = b.startsWith("opencode")
|
||||
if (aIsOpencode && !bIsOpencode) return -1
|
||||
if (!aIsOpencode && bIsOpencode) return 1
|
||||
return a.localeCompare(b)
|
||||
})
|
||||
|
||||
yield* Effect.sync(() => {
|
||||
for (const providerID of ids) {
|
||||
print(ProviderID.make(providerID), args.verbose)
|
||||
}
|
||||
})
|
||||
}),
|
||||
)
|
||||
},
|
||||
})
|
||||
|
||||
for (const providerID of ids) print(ProviderID.make(providerID), args.verbose)
|
||||
}),
|
||||
},
|
||||
})
|
||||
|
||||
@@ -4,9 +4,6 @@ import { cmd } from "./cmd"
|
||||
import * as prompts from "@clack/prompts"
|
||||
import { UI } from "../ui"
|
||||
import { ModelsDev } from "@/provider/models"
|
||||
|
||||
const getModels = () => AppRuntime.runPromise(ModelsDev.Service.use((s) => s.get()))
|
||||
const refreshModels = () => AppRuntime.runPromise(ModelsDev.Service.use((s) => s.refresh(true)))
|
||||
import { map, pipe, sortBy, values } from "remeda"
|
||||
import path from "path"
|
||||
import os from "os"
|
||||
@@ -248,7 +245,7 @@ export const ProvidersListCommand = cmd({
|
||||
return Object.entries(yield* auth.all())
|
||||
}),
|
||||
)
|
||||
const database = await getModels()
|
||||
const database = await ModelsDev.get()
|
||||
|
||||
for (const [providerID, result] of results) {
|
||||
const name = database[providerID]?.name || providerID
|
||||
@@ -337,14 +334,14 @@ export const ProvidersLoginCommand = cmd({
|
||||
prompts.outro("Done")
|
||||
return
|
||||
}
|
||||
await refreshModels().catch(() => {})
|
||||
await ModelsDev.refresh(true).catch(() => {})
|
||||
|
||||
const config = await AppRuntime.runPromise(Config.Service.use((cfg) => cfg.get()))
|
||||
|
||||
const disabled = new Set(config.disabled_providers ?? [])
|
||||
const enabled = config.enabled_providers ? new Set(config.enabled_providers) : undefined
|
||||
|
||||
const providers = await getModels().then((x) => {
|
||||
const providers = await ModelsDev.get().then((x) => {
|
||||
const filtered: Record<string, (typeof x)[string]> = {}
|
||||
for (const [key, value] of Object.entries(x)) {
|
||||
if ((enabled ? enabled.has(key) : true) && !disabled.has(key)) {
|
||||
@@ -508,7 +505,7 @@ export const ProvidersLogoutCommand = cmd({
|
||||
prompts.log.error("No credentials found")
|
||||
return
|
||||
}
|
||||
const database = await getModels()
|
||||
const database = await ModelsDev.get()
|
||||
const selected = await prompts.select({
|
||||
message: "Select provider",
|
||||
options: credentials.map(([key, value]) => ({
|
||||
|
||||
@@ -133,8 +133,6 @@ export function tui(input: {
|
||||
}
|
||||
|
||||
const renderer = await createCliRenderer(rendererConfig(input.config))
|
||||
// Prewarm palette before ThemeProvider mounts so `system` theme avoids a first-paint fallback flash.
|
||||
void renderer.getPalette({ size: 16 }).catch(() => undefined)
|
||||
const mode = (await renderer.waitForThemeMode(1000)) ?? "dark"
|
||||
|
||||
await render(() => {
|
||||
|
||||
@@ -17,7 +17,6 @@ import { MessageID, PartID } from "@/session/schema"
|
||||
import { createStore, produce, unwrap } from "solid-js/store"
|
||||
import { useKeybind } from "@tui/context/keybind"
|
||||
import { usePromptHistory, type PromptInfo } from "./history"
|
||||
import { computePromptTraits } from "./traits"
|
||||
import { assign } from "./part"
|
||||
import { usePromptStash } from "./stash"
|
||||
import { DialogStash } from "../dialog-stash"
|
||||
@@ -558,11 +557,17 @@ export function Prompt(props: PromptProps) {
|
||||
|
||||
createEffect(() => {
|
||||
if (!input || input.isDestroyed) return
|
||||
input.traits = computePromptTraits({
|
||||
mode: store.mode,
|
||||
disabled: !!props.disabled,
|
||||
autocompleteVisible: !!auto()?.visible,
|
||||
})
|
||||
const capture =
|
||||
store.mode === "normal"
|
||||
? auto()?.visible
|
||||
? (["escape", "navigate", "submit", "tab"] as const)
|
||||
: (["tab"] as const)
|
||||
: undefined
|
||||
input.traits = {
|
||||
capture,
|
||||
suspend: !!props.disabled || store.mode === "shell",
|
||||
status: store.mode === "shell" ? "SHELL" : undefined,
|
||||
}
|
||||
})
|
||||
|
||||
function restoreExtmarksFromParts(parts: PromptInfo["parts"]) {
|
||||
|
||||
@@ -1,31 +0,0 @@
|
||||
import type { EditorTraits } from "@opentui/core"
|
||||
|
||||
export type PromptMode = "normal" | "shell"
|
||||
|
||||
export interface PromptTraitsInput {
|
||||
mode: PromptMode
|
||||
disabled: boolean
|
||||
autocompleteVisible: boolean
|
||||
}
|
||||
|
||||
/**
|
||||
* Compute the textarea editor traits for the prompt.
|
||||
*
|
||||
* `traits.suspend` gates the textarea's keybinding actions (backspace,
|
||||
* delete-word, arrow movement, undo/redo, etc.). Shell mode is an active
|
||||
* editing mode — only `disabled` should suspend the textarea, otherwise
|
||||
* users can type in shell mode but cannot delete or move the cursor.
|
||||
*/
|
||||
export function computePromptTraits(input: PromptTraitsInput): EditorTraits {
|
||||
const capture =
|
||||
input.mode === "normal"
|
||||
? input.autocompleteVisible
|
||||
? (["escape", "navigate", "submit", "tab"] as const)
|
||||
: (["tab"] as const)
|
||||
: undefined
|
||||
return {
|
||||
capture,
|
||||
suspend: input.disabled,
|
||||
status: input.mode === "shell" ? "SHELL" : undefined,
|
||||
}
|
||||
}
|
||||
@@ -416,16 +416,12 @@ export const { use: useTheme, provider: ThemeProvider } = createSimpleContext({
|
||||
|
||||
const values = createMemo(() => {
|
||||
const active = store.themes[store.active]
|
||||
if (active) {
|
||||
return resolveTheme(active, store.mode)
|
||||
}
|
||||
if (active) return resolveTheme(active, store.mode)
|
||||
|
||||
const saved = kv.get("theme")
|
||||
if (typeof saved === "string") {
|
||||
const theme = store.themes[saved]
|
||||
if (theme) {
|
||||
return resolveTheme(theme, store.mode)
|
||||
}
|
||||
if (theme) return resolveTheme(theme, store.mode)
|
||||
}
|
||||
|
||||
return resolveTheme(store.themes.opencode, store.mode)
|
||||
|
||||
@@ -2,7 +2,7 @@ import { Installation } from "@/installation"
|
||||
import { Server } from "@/server/server"
|
||||
import * as Log from "@opencode-ai/core/util/log"
|
||||
import { Instance } from "@/project/instance"
|
||||
import { InstanceStore } from "@/project/instance-store"
|
||||
import { InstanceBootstrap } from "@/project/bootstrap"
|
||||
import { Rpc } from "@/util/rpc"
|
||||
import { upgrade } from "@/cli/upgrade"
|
||||
import { Config } from "@/config/config"
|
||||
@@ -10,7 +10,7 @@ import { GlobalBus } from "@/bus/global"
|
||||
import { Flag } from "@opencode-ai/core/flag/flag"
|
||||
import { writeHeapSnapshot } from "node:v8"
|
||||
import { Heap } from "@/cli/heap"
|
||||
import { AppRuntime, getBootstrapRunEffect } from "@/effect/app-runtime"
|
||||
import { AppRuntime } from "@/effect/app-runtime"
|
||||
import { ensureProcessMetadata } from "@opencode-ai/core/util/opencode-process"
|
||||
|
||||
ensureProcessMetadata("worker")
|
||||
@@ -77,7 +77,7 @@ export const rpc = {
|
||||
async checkUpgrade(input: { directory: string }) {
|
||||
await Instance.provide({
|
||||
directory: input.directory,
|
||||
init: await getBootstrapRunEffect(),
|
||||
init: () => AppRuntime.runPromise(InstanceBootstrap),
|
||||
fn: async () => {
|
||||
await upgrade().catch(() => {})
|
||||
},
|
||||
@@ -89,7 +89,7 @@ export const rpc = {
|
||||
async shutdown() {
|
||||
Log.Default.info("worker shutting down")
|
||||
|
||||
await InstanceStore.disposeAllInstances()
|
||||
await Instance.disposeAll()
|
||||
if (server) await server.stop(true)
|
||||
},
|
||||
}
|
||||
|
||||
@@ -1,50 +0,0 @@
|
||||
import type { Argv } from "yargs"
|
||||
import { Effect, Schema } from "effect"
|
||||
import { AppRuntime, type AppServices } from "@/effect/app-runtime"
|
||||
import { InstanceStore } from "@/project/instance-store"
|
||||
import { cmd } from "./cmd/cmd"
|
||||
|
||||
/**
|
||||
* User-visible command failure. Throw via `fail("...")` from an effectCmd handler
|
||||
* to surface a printed message + non-zero exit. Recognised by the global error
|
||||
* formatter in `src/cli/error.ts` (FormatError), so the existing top-level
|
||||
* catch + cleanup in `src/index.ts` runs normally.
|
||||
*/
|
||||
export class CliError extends Schema.TaggedErrorClass<CliError>()("CliError", {
|
||||
message: Schema.String,
|
||||
exitCode: Schema.optional(Schema.Number),
|
||||
}) {}
|
||||
|
||||
export const fail = (message: string, exitCode = 1) => Effect.fail(new CliError({ message, exitCode }))
|
||||
|
||||
/**
|
||||
* Effect-native CLI command builder. Wraps yargs `cmd()` so the handler body is
|
||||
* an `Effect` with `InstanceRef` provided and any `AppServices` yieldable.
|
||||
*
|
||||
* Errors propagate to the existing top-level handler in `src/index.ts`; use
|
||||
* `fail("...")` for user-visible domain failures (clean exit, formatted message).
|
||||
*
|
||||
* Handlers are typically `Effect.fn("Cli.<name>")(function*(args) { ... })`,
|
||||
* which adds a named tracing span per CLI invocation. Once all commands use
|
||||
* `effectCmd`, swapping the underlying `cmd()` factory for effect/cli's
|
||||
* `Command.make(...)` won't touch any handler bodies.
|
||||
*/
|
||||
export const effectCmd = <Args, A>(opts: {
|
||||
command: string | readonly string[]
|
||||
describe: string | false
|
||||
builder?: (yargs: Argv) => Argv<Args>
|
||||
/** Defaults to process.cwd(). Override for commands that take a directory positional. */
|
||||
directory?: (args: Args) => string
|
||||
handler: (args: Args) => Effect.Effect<A, CliError, AppServices | InstanceStore.Service>
|
||||
}) =>
|
||||
cmd<{}, Args>({
|
||||
command: opts.command,
|
||||
describe: opts.describe,
|
||||
builder: opts.builder as never,
|
||||
async handler(rawArgs) {
|
||||
// yargs typing wraps Args in ArgumentsCamelCase<WithDoubleDash<...>>; cast at the boundary.
|
||||
const args = rawArgs as unknown as Args
|
||||
const directory = opts.directory?.(args) ?? process.cwd()
|
||||
await AppRuntime.runPromise(InstanceStore.Service.use((s) => s.provide({ directory }, opts.handler(args))))
|
||||
},
|
||||
})
|
||||
@@ -15,13 +15,6 @@ function isTaggedError(error: unknown, tag: string): boolean {
|
||||
}
|
||||
|
||||
export function FormatError(input: unknown) {
|
||||
// CliError: domain failure surfaced from an effectCmd handler via fail("...")
|
||||
if (isTaggedError(input, "CliError")) {
|
||||
const data = input as ErrorLike & { exitCode?: number }
|
||||
if (data.exitCode != null) process.exitCode = data.exitCode
|
||||
return data.message ?? ""
|
||||
}
|
||||
|
||||
// MCPFailed: { name: string }
|
||||
if (NamedError.hasName(input, "MCPFailed")) {
|
||||
return `MCP server "${(input as ErrorLike).data?.name}" failed. Note, opencode does not support MCP authentication yet.`
|
||||
|
||||
@@ -11,8 +11,7 @@ import { Flag } from "@opencode-ai/core/flag/flag"
|
||||
import { Auth } from "../auth"
|
||||
import { Env } from "../env"
|
||||
import { applyEdits, modify } from "jsonc-parser"
|
||||
import { type InstanceContext } from "../project/instance"
|
||||
import { InstanceStore } from "../project/instance-store"
|
||||
import { Instance, type InstanceContext } from "../project/instance"
|
||||
import { InstallationLocal, InstallationVersion } from "@opencode-ai/core/installation/version"
|
||||
import { existsSync } from "fs"
|
||||
import { GlobalBus } from "@/bus/global"
|
||||
@@ -24,7 +23,7 @@ import { AppFileSystem } from "@opencode-ai/core/filesystem"
|
||||
import { InstanceState } from "@/effect/instance-state"
|
||||
import { Context, Duration, Effect, Exit, Fiber, Layer, Option, Schema } from "effect"
|
||||
import { EffectFlock } from "@opencode-ai/core/util/effect-flock"
|
||||
import { containsPath } from "../project/instance-context"
|
||||
import { InstanceRef } from "@/effect/instance-ref"
|
||||
import { zod } from "@/util/effect-zod"
|
||||
import { NonNegativeInt, PositiveInt, withStatics, type DeepMutable } from "@/util/schema"
|
||||
import { ConfigAgent } from "./agent"
|
||||
@@ -460,7 +459,7 @@ export const layer = Layer.effect(
|
||||
const pluginScopeForSource = Effect.fnUntraced(function* (source: string) {
|
||||
if (source.startsWith("http://") || source.startsWith("https://")) return "global"
|
||||
if (source === "OPENCODE_CONFIG_CONTENT") return "local"
|
||||
if (containsPath(source, ctx)) return "local"
|
||||
if (yield* InstanceRef.use((ctx) => Effect.succeed(Instance.containsPath(source, ctx)))) return "local"
|
||||
return "global"
|
||||
})
|
||||
|
||||
@@ -737,18 +736,12 @@ export const layer = Layer.effect(
|
||||
yield* fs
|
||||
.writeFileString(file, JSON.stringify(mergeDeep(writable(existing), writable(config)), null, 2))
|
||||
.pipe(Effect.orDie)
|
||||
if (options?.dispose !== false) {
|
||||
// Fail loudly if no instance is bound — silently skipping would
|
||||
// mask "config update without an active instance" bugs. The throw
|
||||
// comes from `Instance.current` inside `InstanceState.context`.
|
||||
const ctx = yield* InstanceState.context
|
||||
yield* Effect.promise(() => InstanceStore.disposeInstance(ctx))
|
||||
}
|
||||
if (options?.dispose !== false) yield* Effect.promise(() => Instance.dispose())
|
||||
})
|
||||
|
||||
const invalidate = Effect.fn("Config.invalidate")(function* (wait?: boolean) {
|
||||
yield* invalidateGlobal
|
||||
const task = InstanceStore.disposeAllInstances()
|
||||
const task = Instance.disposeAll()
|
||||
.catch(() => undefined)
|
||||
.finally(() =>
|
||||
GlobalBus.emit("event", {
|
||||
|
||||
@@ -25,7 +25,6 @@ import { SessionID } from "@/session/schema"
|
||||
import { errorData } from "@/util/error"
|
||||
import { waitEvent } from "./util"
|
||||
import { WorkspaceContext } from "./workspace-context"
|
||||
import { EffectBridge } from "@/effect/bridge"
|
||||
import { NonNegativeInt, withStatics } from "@/util/schema"
|
||||
import { zod as effectZod, zodObject } from "@/util/effect-zod"
|
||||
|
||||
@@ -337,7 +336,7 @@ export const layer = Layer.effect(
|
||||
|
||||
const syncWorkspaceLoop = Effect.fn("Workspace.syncWorkspaceLoop")(function* (space: Info) {
|
||||
const adapter = getAdapter(space.projectID, space.type)
|
||||
const target = yield* EffectBridge.fromPromise(() => adapter.target(space))
|
||||
const target = yield* Effect.promise(() => Promise.resolve(adapter.target(space)))
|
||||
|
||||
if (target.type === "local") return
|
||||
|
||||
@@ -421,7 +420,7 @@ export const layer = Layer.effect(
|
||||
if (!Flag.OPENCODE_EXPERIMENTAL_WORKSPACES) return
|
||||
|
||||
const adapter = getAdapter(space.projectID, space.type)
|
||||
const target = yield* EffectBridge.fromPromise(() => adapter.target(space))
|
||||
const target = yield* Effect.promise(() => Promise.resolve(adapter.target(space)))
|
||||
|
||||
if (target.type === "local") {
|
||||
setStatus(space.id, (yield* Effect.promise(() => Filesystem.exists(target.directory))) ? "connected" : "error")
|
||||
@@ -460,8 +459,8 @@ export const layer = Layer.effect(
|
||||
const create = Effect.fn("Workspace.create")(function* (input: CreateInput) {
|
||||
const id = WorkspaceID.ascending(input.id)
|
||||
const adapter = getAdapter(input.projectID, input.type)
|
||||
const config = yield* EffectBridge.fromPromise(() =>
|
||||
adapter.configure({ ...input, id, name: Slug.create(), directory: null }),
|
||||
const config = yield* Effect.promise(() =>
|
||||
Promise.resolve(adapter.configure({ ...input, id, name: Slug.create(), directory: null })),
|
||||
)
|
||||
|
||||
const info: Info = {
|
||||
@@ -497,7 +496,7 @@ export const layer = Layer.effect(
|
||||
OTEL_RESOURCE_ATTRIBUTES: process.env.OTEL_RESOURCE_ATTRIBUTES,
|
||||
}
|
||||
|
||||
yield* EffectBridge.fromPromise(() => adapter.create(config, env))
|
||||
yield* Effect.promise(() => adapter.create(config, env))
|
||||
yield* Effect.all(
|
||||
[
|
||||
waitEvent({
|
||||
@@ -533,7 +532,7 @@ export const layer = Layer.effect(
|
||||
})
|
||||
|
||||
const adapter = getAdapter(space.projectID, space.type)
|
||||
const target = yield* EffectBridge.fromPromise(() => adapter.target(space))
|
||||
const target = yield* Effect.promise(() => Promise.resolve(adapter.target(space)))
|
||||
|
||||
yield* sync.run(Session.Event.Updated, {
|
||||
sessionID: input.sessionID,
|
||||
@@ -725,10 +724,10 @@ export const layer = Layer.effect(
|
||||
yield* stopSync(id)
|
||||
|
||||
const info = fromRow(row)
|
||||
yield* Effect.catchCause(
|
||||
yield* Effect.catch(
|
||||
Effect.gen(function* () {
|
||||
const adapter = getAdapter(info.projectID, row.type)
|
||||
yield* EffectBridge.fromPromise(() => adapter.remove(info))
|
||||
yield* Effect.tryPromise(() => Promise.resolve(adapter.remove(info)))
|
||||
}),
|
||||
() =>
|
||||
Effect.sync(() => {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Effect, Layer, ManagedRuntime } from "effect"
|
||||
import { Layer, ManagedRuntime } from "effect"
|
||||
import { attach } from "./run-service"
|
||||
import * as Observability from "@opencode-ai/core/effect/observability"
|
||||
|
||||
@@ -14,7 +14,6 @@ import { FileWatcher } from "@/file/watcher"
|
||||
import { Storage } from "@/storage/storage"
|
||||
import { Snapshot } from "@/snapshot"
|
||||
import { Plugin } from "@/plugin"
|
||||
import { ModelsDev } from "@/provider/models"
|
||||
import { Provider } from "@/provider/provider"
|
||||
import { ProviderAuth } from "@/provider/auth"
|
||||
import { Agent } from "@/agent/agent"
|
||||
@@ -40,8 +39,6 @@ import { Command } from "@/command"
|
||||
import { Truncate } from "@/tool/truncate"
|
||||
import { ToolRegistry } from "@/tool/registry"
|
||||
import { Format } from "@/format"
|
||||
import { InstanceBootstrap } from "@/project/bootstrap"
|
||||
import { InstanceStore } from "@/project/instance-store"
|
||||
import { Project } from "@/project/project"
|
||||
import { Vcs } from "@/project/vcs"
|
||||
import { Workspace } from "@/control-plane/workspace"
|
||||
@@ -68,7 +65,6 @@ export const AppLayer = Layer.mergeAll(
|
||||
Storage.defaultLayer,
|
||||
Snapshot.defaultLayer,
|
||||
Plugin.defaultLayer,
|
||||
ModelsDev.defaultLayer,
|
||||
Provider.defaultLayer,
|
||||
ProviderAuth.defaultLayer,
|
||||
Agent.defaultLayer,
|
||||
@@ -94,8 +90,6 @@ export const AppLayer = Layer.mergeAll(
|
||||
Truncate.defaultLayer,
|
||||
ToolRegistry.defaultLayer,
|
||||
Format.defaultLayer,
|
||||
InstanceBootstrap.defaultLayer,
|
||||
InstanceStore.defaultLayer,
|
||||
Project.defaultLayer,
|
||||
Vcs.defaultLayer,
|
||||
Workspace.defaultLayer,
|
||||
@@ -109,9 +103,6 @@ export const AppLayer = Layer.mergeAll(
|
||||
|
||||
const rt = ManagedRuntime.make(AppLayer, { memoMap })
|
||||
type Runtime = Pick<typeof rt, "runSync" | "runPromise" | "runPromiseExit" | "runFork" | "runCallback" | "dispose">
|
||||
|
||||
/** Services provided by AppRuntime — i.e. what an Effect run via AppRuntime.runPromise can yield. */
|
||||
export type AppServices = ManagedRuntime.ManagedRuntime.Services<typeof rt>
|
||||
const wrap = (effect: Parameters<typeof rt.runSync>[0]) => attach(effect as never) as never
|
||||
|
||||
export const AppRuntime: Runtime = {
|
||||
@@ -132,15 +123,3 @@ export const AppRuntime: Runtime = {
|
||||
},
|
||||
dispose: () => rt.dispose(),
|
||||
}
|
||||
|
||||
let bootstrapRun: Promise<Effect.Effect<void>>
|
||||
export function getBootstrapRunEffect(): Promise<Effect.Effect<void>> {
|
||||
if (!bootstrapRun) {
|
||||
bootstrapRun = AppRuntime.runPromise(
|
||||
Effect.gen(function* () {
|
||||
return (yield* InstanceBootstrap.Service).run
|
||||
}),
|
||||
)
|
||||
}
|
||||
return bootstrapRun
|
||||
}
|
||||
|
||||
@@ -21,25 +21,6 @@ function restore<R>(instance: InstanceContext | undefined, workspace: WorkspaceI
|
||||
return fn()
|
||||
}
|
||||
|
||||
/**
|
||||
* Bridge from Effect into a Promise-returning JS callback while installing
|
||||
* legacy `Instance.context` and `WorkspaceContext` AsyncLocalStorage for
|
||||
* the duration of the callback. Effect's `InstanceRef`/`WorkspaceRef` do
|
||||
* not propagate across async/await boundaries inside `Effect.promise(() =>
|
||||
* async fn)` callbacks that re-enter Effect via `AppRuntime.runPromise`,
|
||||
* but Node's AsyncLocalStorage does. Use this whenever an Effect crosses
|
||||
* into JS that may itself spawn new Effect runtimes (workspace adapters,
|
||||
* legacy plugins, etc.).
|
||||
*
|
||||
* Mirrors `Effect.promise` but restores legacy ALS first.
|
||||
*/
|
||||
export const fromPromise = <T>(fn: () => Promise<T> | T): Effect.Effect<T> =>
|
||||
Effect.gen(function* () {
|
||||
const instance = yield* InstanceRef
|
||||
const workspace = yield* WorkspaceRef
|
||||
return yield* Effect.promise(() => Promise.resolve(restore(instance, workspace, () => fn())))
|
||||
})
|
||||
|
||||
export function make(): Effect.Effect<Shape> {
|
||||
return Effect.gen(function* () {
|
||||
const ctx = yield* Effect.context()
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Effect, Fiber, Layer, ManagedRuntime } from "effect"
|
||||
import { Effect, Layer, ManagedRuntime } from "effect"
|
||||
import * as Context from "effect/Context"
|
||||
import { Instance } from "@/project/instance"
|
||||
import { LocalContext } from "@/util/local-context"
|
||||
@@ -24,20 +24,15 @@ export function attachWith<A, E, R>(effect: Effect.Effect<A, E, R>, refs: Refs):
|
||||
}
|
||||
|
||||
export function attach<A, E, R>(effect: Effect.Effect<A, E, R>): Effect.Effect<A, E, R> {
|
||||
const workspace = WorkspaceContext.workspaceID
|
||||
const instance = (() => {
|
||||
try {
|
||||
return Instance.current
|
||||
} catch (err) {
|
||||
if (!(err instanceof LocalContext.NotFound)) throw err
|
||||
}
|
||||
})()
|
||||
if (instance && workspace !== undefined) return attachWith(effect, { instance, workspace })
|
||||
const fiber = Fiber.getCurrent()
|
||||
return attachWith(effect, {
|
||||
instance: instance ?? (fiber ? Context.getReferenceUnsafe(fiber.context, InstanceRef) : undefined),
|
||||
workspace: workspace ?? (fiber ? Context.getReferenceUnsafe(fiber.context, WorkspaceRef) : undefined),
|
||||
})
|
||||
try {
|
||||
return attachWith(effect, {
|
||||
instance: Instance.current,
|
||||
workspace: WorkspaceContext.workspaceID,
|
||||
})
|
||||
} catch (err) {
|
||||
if (!(err instanceof LocalContext.NotFound)) throw err
|
||||
}
|
||||
return effect
|
||||
}
|
||||
|
||||
export function makeRuntime<I, S, E>(service: Context.Service<I, S>, layer: Layer.Layer<I, E>) {
|
||||
|
||||
@@ -10,7 +10,7 @@ import fuzzysort from "fuzzysort"
|
||||
import ignore from "ignore"
|
||||
import path from "path"
|
||||
import { Global } from "@opencode-ai/core/global"
|
||||
import { containsPath } from "../project/instance-context"
|
||||
import { Instance } from "../project/instance"
|
||||
import * as Log from "@opencode-ai/core/util/log"
|
||||
import { Protected } from "./protected"
|
||||
import { Ripgrep } from "./ripgrep"
|
||||
@@ -507,7 +507,7 @@ export const layer = Layer.effect(
|
||||
const ctx = yield* InstanceState.context
|
||||
const full = path.join(ctx.directory, file)
|
||||
|
||||
if (!containsPath(full, ctx)) {
|
||||
if (!Instance.containsPath(full, ctx)) {
|
||||
throw new Error("Access denied: path escapes project directory")
|
||||
}
|
||||
|
||||
@@ -587,7 +587,7 @@ export const layer = Layer.effect(
|
||||
}
|
||||
|
||||
const resolved = dir ? path.join(ctx.directory, dir) : ctx.directory
|
||||
if (!containsPath(resolved, ctx)) {
|
||||
if (!Instance.containsPath(resolved, ctx)) {
|
||||
throw new Error("Access denied: path escapes project directory")
|
||||
}
|
||||
|
||||
|
||||
@@ -12,7 +12,7 @@ import { Process } from "@/util/process"
|
||||
import { spawn as lspspawn } from "./launch"
|
||||
import { Effect, Layer, Context, Schema } from "effect"
|
||||
import { InstanceState } from "@/effect/instance-state"
|
||||
import { containsPath } from "@/project/instance-context"
|
||||
import { AppFileSystem } from "@opencode-ai/core/filesystem"
|
||||
import { NonNegativeInt, withStatics } from "@/util/schema"
|
||||
import { zod, ZodOverride } from "@/util/effect-zod"
|
||||
|
||||
@@ -221,7 +221,12 @@ export const layer = Layer.effect(
|
||||
|
||||
const getClients = Effect.fnUntraced(function* (file: string) {
|
||||
const ctx = yield* InstanceState.context
|
||||
if (!containsPath(file, ctx)) return [] as LSPClient.Info[]
|
||||
if (
|
||||
!AppFileSystem.contains(ctx.directory, file) &&
|
||||
(ctx.worktree === "/" || !AppFileSystem.contains(ctx.worktree, file))
|
||||
) {
|
||||
return [] as LSPClient.Info[]
|
||||
}
|
||||
const s = yield* InstanceState.get(state)
|
||||
return yield* Effect.promise(async () => {
|
||||
const extension = path.parse(file).ext || file
|
||||
|
||||
@@ -8,71 +8,37 @@ import * as Vcs from "./vcs"
|
||||
import { Bus } from "../bus"
|
||||
import { Command } from "../command"
|
||||
import { InstanceState } from "@/effect/instance-state"
|
||||
import * as Log from "@opencode-ai/core/util/log"
|
||||
import { FileWatcher } from "@/file/watcher"
|
||||
import { ShareNext } from "@/share/share-next"
|
||||
import { Context, Effect, Layer } from "effect"
|
||||
import * as Effect from "effect/Effect"
|
||||
import { Config } from "@/config/config"
|
||||
|
||||
export interface Interface {
|
||||
readonly run: Effect.Effect<void>
|
||||
}
|
||||
export const InstanceBootstrap = Effect.gen(function* () {
|
||||
const ctx = yield* InstanceState.context
|
||||
Log.Default.info("bootstrapping", { directory: ctx.directory })
|
||||
// everything depends on config so eager load it for nice traces
|
||||
yield* Config.Service.use((svc) => svc.get())
|
||||
// Plugin can mutate config so it has to be initialized before anything else.
|
||||
yield* Plugin.Service.use((svc) => svc.init())
|
||||
yield* Effect.all(
|
||||
[
|
||||
LSP.Service,
|
||||
ShareNext.Service,
|
||||
Format.Service,
|
||||
File.Service,
|
||||
FileWatcher.Service,
|
||||
Vcs.Service,
|
||||
Snapshot.Service,
|
||||
].map((s) => Effect.forkDetach(s.use((i) => i.init()))),
|
||||
).pipe(Effect.withSpan("InstanceBootstrap.init"))
|
||||
|
||||
export class Service extends Context.Service<Service, Interface>()("@opencode/InstanceBootstrap") {}
|
||||
|
||||
export const layer = Layer.effect(
|
||||
Service,
|
||||
Effect.gen(function* () {
|
||||
// Yield each bootstrap dep at layer init so `run` itself has R = never.
|
||||
// This breaks the circular declaration loop through Config → Instance → InstanceStore
|
||||
// (instance-store.ts only yields this Service tag, never the impl-side services).
|
||||
const bus = yield* Bus.Service
|
||||
const config = yield* Config.Service
|
||||
const file = yield* File.Service
|
||||
const fileWatcher = yield* FileWatcher.Service
|
||||
const format = yield* Format.Service
|
||||
const lsp = yield* LSP.Service
|
||||
const plugin = yield* Plugin.Service
|
||||
const shareNext = yield* ShareNext.Service
|
||||
const snapshot = yield* Snapshot.Service
|
||||
const vcs = yield* Vcs.Service
|
||||
|
||||
const run = Effect.gen(function* () {
|
||||
const ctx = yield* InstanceState.context
|
||||
yield* Effect.logInfo("bootstrapping", { directory: ctx.directory })
|
||||
// everything depends on config so eager load it for nice traces
|
||||
yield* config.get()
|
||||
// Plugin can mutate config so it has to be initialized before anything else.
|
||||
yield* plugin.init()
|
||||
yield* Effect.all(
|
||||
[lsp, shareNext, format, file, fileWatcher, vcs, snapshot].map((s) => Effect.forkDetach(s.init())),
|
||||
).pipe(Effect.withSpan("InstanceBootstrap.init"))
|
||||
|
||||
const projectID = ctx.project.id
|
||||
yield* bus.subscribeCallback(Command.Event.Executed, async (payload) => {
|
||||
if (payload.properties.name === Command.Default.INIT) {
|
||||
Project.setInitialized(projectID)
|
||||
}
|
||||
})
|
||||
}).pipe(Effect.withSpan("InstanceBootstrap"))
|
||||
|
||||
return Service.of({ run })
|
||||
}),
|
||||
)
|
||||
|
||||
export const defaultLayer: Layer.Layer<Service> = layer.pipe(
|
||||
Layer.provide([
|
||||
Bus.layer,
|
||||
Config.defaultLayer,
|
||||
File.defaultLayer,
|
||||
FileWatcher.defaultLayer,
|
||||
Format.defaultLayer,
|
||||
LSP.defaultLayer,
|
||||
Plugin.defaultLayer,
|
||||
Project.defaultLayer,
|
||||
ShareNext.defaultLayer,
|
||||
Snapshot.defaultLayer,
|
||||
Vcs.defaultLayer,
|
||||
]),
|
||||
)
|
||||
|
||||
export * as InstanceBootstrap from "./bootstrap"
|
||||
const projectID = ctx.project.id
|
||||
yield* Bus.Service.use((svc) =>
|
||||
svc.subscribeCallback(Command.Event.Executed, async (payload) => {
|
||||
if (payload.properties.name === Command.Default.INIT) {
|
||||
Project.setInitialized(projectID)
|
||||
}
|
||||
}),
|
||||
)
|
||||
}).pipe(Effect.withSpan("InstanceBootstrap"))
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import { LocalContext } from "@/util/local-context"
|
||||
import { AppFileSystem } from "@opencode-ai/core/filesystem"
|
||||
import type * as Project from "./project"
|
||||
|
||||
export interface InstanceContext {
|
||||
@@ -9,16 +8,3 @@ export interface InstanceContext {
|
||||
}
|
||||
|
||||
export const context = LocalContext.create<InstanceContext>("instance")
|
||||
|
||||
/**
|
||||
* Check if a path is within the project boundary.
|
||||
* Returns true if path is inside ctx.directory OR ctx.worktree.
|
||||
* Paths within the worktree but outside the working directory should not trigger external_directory permission.
|
||||
*/
|
||||
export function containsPath(filepath: string, ctx: InstanceContext): boolean {
|
||||
if (AppFileSystem.contains(ctx.directory, filepath)) return true
|
||||
// Non-git projects set worktree to "/" which would match ANY absolute path.
|
||||
// Skip worktree check in this case to preserve external_directory permissions.
|
||||
if (ctx.worktree === "/") return false
|
||||
return AppFileSystem.contains(ctx.worktree, filepath)
|
||||
}
|
||||
|
||||
@@ -1,33 +1,24 @@
|
||||
import { GlobalBus } from "@/bus/global"
|
||||
import { WorkspaceContext } from "@/control-plane/workspace-context"
|
||||
import { InstanceRef } from "@/effect/instance-ref"
|
||||
import { disposeInstance as runDisposers } from "@/effect/instance-registry"
|
||||
import { disposeInstance } from "@/effect/instance-registry"
|
||||
import { makeRuntime } from "@/effect/run-service"
|
||||
import { AppFileSystem } from "@opencode-ai/core/filesystem"
|
||||
import { Context, Deferred, Duration, Effect, Exit, Layer, Scope } from "effect"
|
||||
import { type InstanceContext } from "./instance-context"
|
||||
import { context, type InstanceContext } from "./instance-context"
|
||||
import * as Project from "./project"
|
||||
|
||||
export interface LoadInput<R = never> {
|
||||
export interface LoadInput {
|
||||
directory: string
|
||||
/**
|
||||
* Additional setup to run after the default InstanceBootstrap.
|
||||
* Mainly used by tests for env-var setup or file writes that need the instance ALS context.
|
||||
*/
|
||||
init?: Effect.Effect<void, never, R>
|
||||
init?: () => Promise<unknown>
|
||||
worktree?: string
|
||||
project?: Project.Info
|
||||
}
|
||||
|
||||
export interface Interface {
|
||||
readonly load: <R = never>(input: LoadInput<R>) => Effect.Effect<InstanceContext, never, R>
|
||||
readonly reload: <R = never>(input: LoadInput<R>) => Effect.Effect<InstanceContext, never, R>
|
||||
readonly load: (input: LoadInput) => Effect.Effect<InstanceContext>
|
||||
readonly reload: (input: LoadInput) => Effect.Effect<InstanceContext>
|
||||
readonly dispose: (ctx: InstanceContext) => Effect.Effect<void>
|
||||
readonly disposeAll: () => Effect.Effect<void>
|
||||
readonly provide: <A, E, R, R2 = never>(
|
||||
input: LoadInput<R2>,
|
||||
effect: Effect.Effect<A, E, R>,
|
||||
) => Effect.Effect<A, E, R | R2>
|
||||
}
|
||||
|
||||
export class Service extends Context.Service<Service, Interface>()("@opencode/InstanceStore") {}
|
||||
@@ -43,25 +34,25 @@ export const layer: Layer.Layer<Service, never, Project.Service> = Layer.effect(
|
||||
const scope = yield* Scope.Scope
|
||||
const cache = new Map<string, Entry>()
|
||||
|
||||
const boot = <R>(input: LoadInput<R> & { directory: string }) =>
|
||||
Effect.gen(function* () {
|
||||
const ctx: InstanceContext =
|
||||
input.project && input.worktree
|
||||
? {
|
||||
const boot = Effect.fn("InstanceStore.boot")(function* (input: LoadInput & { directory: string }) {
|
||||
const ctx =
|
||||
input.project && input.worktree
|
||||
? {
|
||||
directory: input.directory,
|
||||
worktree: input.worktree,
|
||||
project: input.project,
|
||||
}
|
||||
: yield* project.fromDirectory(input.directory).pipe(
|
||||
Effect.map((result) => ({
|
||||
directory: input.directory,
|
||||
worktree: input.worktree,
|
||||
project: input.project,
|
||||
}
|
||||
: yield* project.fromDirectory(input.directory).pipe(
|
||||
Effect.map((result) => ({
|
||||
directory: input.directory,
|
||||
worktree: result.sandbox,
|
||||
project: result.project,
|
||||
})),
|
||||
)
|
||||
if (input.init) yield* input.init.pipe(Effect.provideService(InstanceRef, ctx))
|
||||
return ctx
|
||||
}).pipe(Effect.withSpan("InstanceStore.boot"))
|
||||
worktree: result.sandbox,
|
||||
project: result.project,
|
||||
})),
|
||||
)
|
||||
const init = input.init
|
||||
if (init) yield* Effect.promise(() => context.provide(ctx, init))
|
||||
return ctx
|
||||
})
|
||||
|
||||
const removeEntry = (directory: string, entry: Entry) =>
|
||||
Effect.sync(() => {
|
||||
@@ -70,12 +61,11 @@ export const layer: Layer.Layer<Service, never, Project.Service> = Layer.effect(
|
||||
return true
|
||||
})
|
||||
|
||||
const completeLoad = <R>(directory: string, input: LoadInput<R>, entry: Entry) =>
|
||||
Effect.gen(function* () {
|
||||
const exit = yield* Effect.exit(boot({ ...input, directory }))
|
||||
if (Exit.isFailure(exit)) yield* removeEntry(directory, entry)
|
||||
yield* Deferred.done(entry.deferred, exit).pipe(Effect.asVoid)
|
||||
})
|
||||
const completeLoad = Effect.fnUntraced(function* (directory: string, input: LoadInput, entry: Entry) {
|
||||
const exit = yield* Effect.exit(boot({ ...input, directory }))
|
||||
if (Exit.isFailure(exit)) yield* removeEntry(directory, entry)
|
||||
yield* Deferred.done(entry.deferred, exit).pipe(Effect.asVoid)
|
||||
})
|
||||
|
||||
const emitDisposed = (input: { directory: string; project?: string }) =>
|
||||
Effect.sync(() =>
|
||||
@@ -94,7 +84,7 @@ export const layer: Layer.Layer<Service, never, Project.Service> = Layer.effect(
|
||||
|
||||
const disposeContext = Effect.fn("InstanceStore.disposeContext")(function* (ctx: InstanceContext) {
|
||||
yield* Effect.logInfo("disposing instance", { directory: ctx.directory })
|
||||
yield* Effect.promise(() => runDisposers(ctx.directory))
|
||||
yield* Effect.promise(() => disposeInstance(ctx.directory))
|
||||
yield* emitDisposed({ directory: ctx.directory, project: ctx.project.id })
|
||||
})
|
||||
|
||||
@@ -106,9 +96,9 @@ export const layer: Layer.Layer<Service, never, Project.Service> = Layer.effect(
|
||||
return true
|
||||
})
|
||||
|
||||
const load = <R>(input: LoadInput<R>): Effect.Effect<InstanceContext, never, R> => {
|
||||
const load = Effect.fn("InstanceStore.load")(function* (input: LoadInput) {
|
||||
const directory = AppFileSystem.resolve(input.directory)
|
||||
return Effect.uninterruptibleMask((restore) =>
|
||||
return yield* Effect.uninterruptibleMask((restore) =>
|
||||
Effect.gen(function* () {
|
||||
const existing = cache.get(directory)
|
||||
if (existing) return yield* restore(Deferred.await(existing.deferred))
|
||||
@@ -121,12 +111,12 @@ export const layer: Layer.Layer<Service, never, Project.Service> = Layer.effect(
|
||||
}).pipe(Effect.forkIn(scope, { startImmediately: true }))
|
||||
return yield* restore(Deferred.await(entry.deferred))
|
||||
}),
|
||||
).pipe(Effect.withSpan("InstanceStore.load"))
|
||||
}
|
||||
)
|
||||
})
|
||||
|
||||
const reload = <R>(input: LoadInput<R>): Effect.Effect<InstanceContext, never, R> => {
|
||||
const reload = Effect.fn("InstanceStore.reload")(function* (input: LoadInput) {
|
||||
const directory = AppFileSystem.resolve(input.directory)
|
||||
return Effect.uninterruptibleMask((restore) =>
|
||||
return yield* Effect.uninterruptibleMask((restore) =>
|
||||
Effect.gen(function* () {
|
||||
const previous = cache.get(directory)
|
||||
const entry: Entry = { deferred: Deferred.makeUnsafe<InstanceContext>() }
|
||||
@@ -135,15 +125,15 @@ export const layer: Layer.Layer<Service, never, Project.Service> = Layer.effect(
|
||||
yield* Effect.logInfo("reloading instance", { directory })
|
||||
if (previous) {
|
||||
yield* Deferred.await(previous.deferred).pipe(Effect.ignore)
|
||||
yield* Effect.promise(() => runDisposers(directory))
|
||||
yield* Effect.promise(() => disposeInstance(directory))
|
||||
yield* emitDisposed({ directory, project: input.project?.id })
|
||||
}
|
||||
yield* completeLoad(directory, input, entry)
|
||||
}).pipe(Effect.forkIn(scope, { startImmediately: true }))
|
||||
return yield* restore(Deferred.await(entry.deferred))
|
||||
}),
|
||||
).pipe(Effect.withSpan("InstanceStore.reload"))
|
||||
}
|
||||
)
|
||||
})
|
||||
|
||||
const dispose = Effect.fn("InstanceStore.dispose")(function* (ctx: InstanceContext) {
|
||||
const entry = cache.get(ctx.directory)
|
||||
@@ -178,9 +168,6 @@ export const layer: Layer.Layer<Service, never, Project.Service> = Layer.effect(
|
||||
return yield* cachedDisposeAll
|
||||
})
|
||||
|
||||
const provide = <A, E, R, R2>(input: LoadInput<R2>, effect: Effect.Effect<A, E, R>): Effect.Effect<A, E, R | R2> =>
|
||||
load(input).pipe(Effect.flatMap((ctx) => effect.pipe(Effect.provideService(InstanceRef, ctx))))
|
||||
|
||||
yield* Effect.addFinalizer(() => disposeAll().pipe(Effect.ignore))
|
||||
|
||||
return Service.of({
|
||||
@@ -188,7 +175,6 @@ export const layer: Layer.Layer<Service, never, Project.Service> = Layer.effect(
|
||||
reload,
|
||||
dispose,
|
||||
disposeAll,
|
||||
provide,
|
||||
})
|
||||
}),
|
||||
)
|
||||
@@ -197,11 +183,4 @@ export const defaultLayer = layer.pipe(Layer.provide(Project.defaultLayer))
|
||||
|
||||
export const runtime = makeRuntime(Service, defaultLayer)
|
||||
|
||||
// Promise-returning helpers for callers without an Effect runtime in scope.
|
||||
// They route through `runtime` (not a yielded Service from a fresh runtime)
|
||||
// so they share the cache that `Instance.provide` populates.
|
||||
export const disposeInstance = (ctx: InstanceContext) => runtime.runPromise((store) => store.dispose(ctx))
|
||||
export const disposeAllInstances = () => runtime.runPromise((store) => store.disposeAll())
|
||||
export const reloadInstance = (input: LoadInput) => runtime.runPromise((store) => store.reload(input))
|
||||
|
||||
export * as InstanceStore from "./instance-store"
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { Effect } from "effect"
|
||||
import { AppFileSystem } from "@opencode-ai/core/filesystem"
|
||||
import * as Project from "./project"
|
||||
import { context, type InstanceContext } from "./instance-context"
|
||||
import { InstanceStore } from "./instance-store"
|
||||
|
||||
@@ -6,11 +7,14 @@ export type { InstanceContext } from "./instance-context"
|
||||
export type { LoadInput } from "./instance-store"
|
||||
|
||||
export const Instance = {
|
||||
async provide<R>(input: { directory: string; init?: Effect.Effect<void>; fn: () => R }): Promise<R> {
|
||||
const ctx = await InstanceStore.runtime.runPromise((store) =>
|
||||
store.load({ directory: input.directory, init: input.init }),
|
||||
load(input: InstanceStore.LoadInput): Promise<InstanceContext> {
|
||||
return InstanceStore.runtime.runPromise((store) => store.load(input))
|
||||
},
|
||||
async provide<R>(input: { directory: string; init?: () => Promise<any>; fn: () => R }): Promise<R> {
|
||||
return context.provide(
|
||||
await Instance.load({ directory: input.directory, init: input.init }),
|
||||
async () => input.fn(),
|
||||
)
|
||||
return context.provide(ctx, async () => input.fn())
|
||||
},
|
||||
get current() {
|
||||
return context.use()
|
||||
@@ -25,6 +29,19 @@ export const Instance = {
|
||||
return context.use().project
|
||||
},
|
||||
|
||||
/**
|
||||
* Check if a path is within the project boundary.
|
||||
* Returns true if path is inside Instance.directory OR Instance.worktree.
|
||||
* Paths within the worktree but outside the working directory should not trigger external_directory permission.
|
||||
*/
|
||||
containsPath(filepath: string, ctx?: InstanceContext) {
|
||||
const instance = ctx ?? Instance
|
||||
if (AppFileSystem.contains(instance.directory, filepath)) return true
|
||||
// Non-git projects set worktree to "/" which would match ANY absolute path.
|
||||
// Skip worktree check in this case to preserve external_directory permissions.
|
||||
if (instance.worktree === "/") return false
|
||||
return AppFileSystem.contains(instance.worktree, filepath)
|
||||
},
|
||||
/**
|
||||
* Captures the current instance ALS context and returns a wrapper that
|
||||
* restores it when called. Use this for callbacks that fire outside the
|
||||
@@ -42,4 +59,13 @@ export const Instance = {
|
||||
restore<R>(ctx: InstanceContext, fn: () => R): R {
|
||||
return context.provide(ctx, fn)
|
||||
},
|
||||
async reload(input: { directory: string; init?: () => Promise<any>; project?: Project.Info; worktree?: string }) {
|
||||
return InstanceStore.runtime.runPromise((store) => store.reload(input))
|
||||
},
|
||||
async dispose() {
|
||||
return InstanceStore.runtime.runPromise((store) => store.dispose(Instance.current))
|
||||
},
|
||||
async disposeAll() {
|
||||
return InstanceStore.runtime.runPromise((store) => store.disposeAll())
|
||||
},
|
||||
}
|
||||
|
||||
@@ -1,13 +1,25 @@
|
||||
import { Global } from "@opencode-ai/core/global"
|
||||
import * as Log from "@opencode-ai/core/util/log"
|
||||
import path from "path"
|
||||
import { Context, Duration, Effect, Layer, Option, Schedule, Schema } from "effect"
|
||||
import { FetchHttpClient, HttpClient, HttpClientRequest } from "effect/unstable/http"
|
||||
import { Schema } from "effect"
|
||||
import { Installation } from "../installation"
|
||||
import { Flag } from "@opencode-ai/core/flag/flag"
|
||||
import { lazy } from "@/util/lazy"
|
||||
import { Filesystem } from "@/util/filesystem"
|
||||
import { Flock } from "@opencode-ai/core/util/flock"
|
||||
import { Hash } from "@opencode-ai/core/util/hash"
|
||||
import { AppFileSystem } from "@opencode-ai/core/filesystem"
|
||||
import { withTransientReadRetry } from "@/util/effect-http-client"
|
||||
|
||||
// Try to import bundled snapshot (generated at build time)
|
||||
// Falls back to undefined in dev mode when snapshot doesn't exist
|
||||
/* @ts-ignore */
|
||||
|
||||
const log = Log.create({ service: "models.dev" })
|
||||
const source = url()
|
||||
const filepath = path.join(
|
||||
Global.Path.cache,
|
||||
source === "https://models.dev" ? "models.json" : `models-${Hash.fast(source)}.json`,
|
||||
)
|
||||
const ttl = 5 * 60 * 1000
|
||||
|
||||
const Cost = Schema.Struct({
|
||||
input: Schema.Finite,
|
||||
@@ -89,110 +101,76 @@ export const Provider = Schema.Struct({
|
||||
|
||||
export type Provider = Schema.Schema.Type<typeof Provider>
|
||||
|
||||
export interface Interface {
|
||||
readonly get: () => Effect.Effect<Record<string, Provider>>
|
||||
readonly refresh: (force?: boolean) => Effect.Effect<void>
|
||||
function url() {
|
||||
return Flag.OPENCODE_MODELS_URL || "https://models.dev"
|
||||
}
|
||||
|
||||
export class Service extends Context.Service<Service, Interface>()("@opencode/ModelsDev") {}
|
||||
function fresh() {
|
||||
return Date.now() - Number(Filesystem.stat(filepath)?.mtimeMs ?? 0) < ttl
|
||||
}
|
||||
|
||||
export const layer: Layer.Layer<Service, never, AppFileSystem.Service | HttpClient.HttpClient> = Layer.effect(
|
||||
Service,
|
||||
Effect.gen(function* () {
|
||||
const fs = yield* AppFileSystem.Service
|
||||
const http = HttpClient.filterStatusOk(withTransientReadRetry(yield* HttpClient.HttpClient))
|
||||
function skip(force: boolean) {
|
||||
return !force && fresh()
|
||||
}
|
||||
|
||||
const source = Flag.OPENCODE_MODELS_URL || "https://models.dev"
|
||||
const filepath = path.join(
|
||||
Global.Path.cache,
|
||||
source === "https://models.dev" ? "models.json" : `models-${Hash.fast(source)}.json`,
|
||||
)
|
||||
const ttl = Duration.minutes(5)
|
||||
const lockKey = `models-dev:${filepath}`
|
||||
const fetchApi = async () => {
|
||||
const result = await fetch(`${url()}/api.json`, {
|
||||
headers: { "User-Agent": Installation.USER_AGENT },
|
||||
signal: AbortSignal.timeout(10000),
|
||||
})
|
||||
return { ok: result.ok, text: await result.text() }
|
||||
}
|
||||
|
||||
const fresh = Effect.fnUntraced(function* () {
|
||||
const stat = yield* fs.stat(filepath).pipe(Effect.catch(() => Effect.succeed(undefined)))
|
||||
if (!stat) return false
|
||||
const mtime = Option.getOrElse(stat.mtime, () => new Date(0)).getTime()
|
||||
return Date.now() - mtime < Duration.toMillis(ttl)
|
||||
})
|
||||
|
||||
const fetchApi = Effect.fn("ModelsDev.fetchApi")(function* () {
|
||||
return yield* HttpClientRequest.get(`${source}/api.json`).pipe(
|
||||
HttpClientRequest.setHeader("User-Agent", Installation.USER_AGENT),
|
||||
http.execute,
|
||||
Effect.flatMap((res) => res.text),
|
||||
Effect.timeout("10 seconds"),
|
||||
)
|
||||
})
|
||||
|
||||
const loadFromDisk = fs.readJson(Flag.OPENCODE_MODELS_PATH ?? filepath).pipe(
|
||||
Effect.catch(() => Effect.succeed(undefined)),
|
||||
Effect.map((v) => v as Record<string, Provider> | undefined),
|
||||
)
|
||||
|
||||
// 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()
|
||||
yield* fs.writeWithDirs(filepath, text)
|
||||
return text
|
||||
})
|
||||
|
||||
const populate = Effect.gen(function* () {
|
||||
const fromDisk = yield* loadFromDisk
|
||||
if (fromDisk) return fromDisk
|
||||
const snapshot = yield* loadSnapshot
|
||||
if (snapshot) return snapshot
|
||||
if (Flag.OPENCODE_DISABLE_MODELS_FETCH) return {}
|
||||
// Flock is cross-process: concurrent opencode CLIs can race on this cache file.
|
||||
const text = yield* Effect.scoped(
|
||||
Effect.gen(function* () {
|
||||
yield* Flock.effect(lockKey)
|
||||
return yield* fetchAndWrite()
|
||||
}),
|
||||
)
|
||||
return JSON.parse(text) as Record<string, Provider>
|
||||
}).pipe(Effect.withSpan("ModelsDev.populate"), Effect.orDie)
|
||||
|
||||
const [cachedGet, invalidate] = yield* Effect.cachedInvalidateWithTTL(populate, Duration.infinity)
|
||||
|
||||
const get = (): Effect.Effect<Record<string, Provider>> => cachedGet
|
||||
|
||||
const refresh = Effect.fn("ModelsDev.refresh")(function* (force = false) {
|
||||
if (!force && (yield* fresh())) return
|
||||
yield* Effect.scoped(
|
||||
Effect.gen(function* () {
|
||||
yield* Flock.effect(lockKey)
|
||||
// Re-check under the lock: another process may have refreshed between
|
||||
// our outer check and lock acquisition.
|
||||
if (!force && (yield* fresh())) return
|
||||
yield* fetchAndWrite()
|
||||
yield* invalidate
|
||||
}),
|
||||
).pipe(
|
||||
Effect.tapCause((cause) => Effect.logError("Failed to fetch models.dev", { cause })),
|
||||
Effect.ignore,
|
||||
)
|
||||
})
|
||||
|
||||
if (!Flag.OPENCODE_DISABLE_MODELS_FETCH && !process.argv.includes("--get-yargs-completions")) {
|
||||
// Schedule.spaced runs the effect once, then waits between completions.
|
||||
yield* Effect.forkScoped(refresh().pipe(Effect.repeat(Schedule.spaced("60 minutes")), Effect.ignore))
|
||||
export const Data = lazy(async () => {
|
||||
const result = await Filesystem.readJson(Flag.OPENCODE_MODELS_PATH ?? filepath).catch(() => {})
|
||||
if (result) return result
|
||||
// @ts-ignore
|
||||
const snapshot = await import("./models-snapshot.js")
|
||||
.then((m) => m.snapshot as Record<string, unknown>)
|
||||
.catch(() => undefined)
|
||||
if (snapshot) return snapshot
|
||||
if (Flag.OPENCODE_DISABLE_MODELS_FETCH) return {}
|
||||
return Flock.withLock(`models-dev:${filepath}`, async () => {
|
||||
const result = await Filesystem.readJson(Flag.OPENCODE_MODELS_PATH ?? filepath).catch(() => {})
|
||||
if (result) return result
|
||||
const result2 = await fetchApi()
|
||||
if (result2.ok) {
|
||||
await Filesystem.write(filepath, result2.text).catch((e) => {
|
||||
log.error("Failed to write models cache", { error: e })
|
||||
})
|
||||
}
|
||||
return JSON.parse(result2.text)
|
||||
})
|
||||
})
|
||||
|
||||
return Service.of({ get, refresh })
|
||||
}),
|
||||
)
|
||||
export async function get() {
|
||||
const result = await Data()
|
||||
return result as Record<string, Provider>
|
||||
}
|
||||
|
||||
export const defaultLayer: Layer.Layer<Service> = layer.pipe(
|
||||
Layer.provide(FetchHttpClient.layer),
|
||||
Layer.provide(AppFileSystem.defaultLayer),
|
||||
)
|
||||
export async function refresh(force = false) {
|
||||
if (skip(force)) return Data.reset()
|
||||
await Flock.withLock(`models-dev:${filepath}`, async () => {
|
||||
if (skip(force)) return Data.reset()
|
||||
const result = await fetchApi()
|
||||
if (!result.ok) return
|
||||
await Filesystem.write(filepath, result.text)
|
||||
Data.reset()
|
||||
}).catch((e) => {
|
||||
log.error("Failed to fetch models.dev", {
|
||||
error: e,
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
if (!Flag.OPENCODE_DISABLE_MODELS_FETCH && !process.argv.includes("--get-yargs-completions")) {
|
||||
void refresh()
|
||||
setInterval(
|
||||
async () => {
|
||||
await refresh()
|
||||
},
|
||||
60 * 1000 * 60,
|
||||
).unref()
|
||||
}
|
||||
|
||||
export * as ModelsDev from "./models"
|
||||
|
||||
@@ -1074,7 +1074,7 @@ export function fromModelsDevProvider(provider: ModelsDev.Provider): Info {
|
||||
const layer: Layer.Layer<
|
||||
Service,
|
||||
never,
|
||||
Config.Service | Auth.Service | Plugin.Service | AppFileSystem.Service | Env.Service | ModelsDev.Service
|
||||
Config.Service | Auth.Service | Plugin.Service | AppFileSystem.Service | Env.Service
|
||||
> = Layer.effect(
|
||||
Service,
|
||||
Effect.gen(function* () {
|
||||
@@ -1083,14 +1083,13 @@ const layer: Layer.Layer<
|
||||
const auth = yield* Auth.Service
|
||||
const env = yield* Env.Service
|
||||
const plugin = yield* Plugin.Service
|
||||
const modelsDevSvc = yield* ModelsDev.Service
|
||||
|
||||
const state = yield* InstanceState.make<State>(() =>
|
||||
Effect.gen(function* () {
|
||||
using _ = log.time("state")
|
||||
const bridge = yield* EffectBridge.make()
|
||||
const cfg = yield* config.get()
|
||||
const modelsDev = yield* modelsDevSvc.get()
|
||||
const modelsDev = yield* Effect.promise(() => ModelsDev.get())
|
||||
const database = mapValues(modelsDev, fromModelsDevProvider)
|
||||
|
||||
const providers: Record<ProviderID, Info> = {} as Record<ProviderID, Info>
|
||||
@@ -1723,7 +1722,6 @@ export const defaultLayer = Layer.suspend(() =>
|
||||
Layer.provide(Config.defaultLayer),
|
||||
Layer.provide(Auth.defaultLayer),
|
||||
Layer.provide(Plugin.defaultLayer),
|
||||
Layer.provide(ModelsDev.defaultLayer),
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
@@ -8,7 +8,7 @@ import { SyncEvent } from "@/sync"
|
||||
import { GlobalBus } from "@/bus/global"
|
||||
import { AppRuntime } from "@/effect/app-runtime"
|
||||
import { AsyncQueue } from "@/util/queue"
|
||||
import { InstanceStore } from "../../project/instance-store"
|
||||
import { Instance } from "../../project/instance"
|
||||
import { Installation } from "@/installation"
|
||||
import { InstallationVersion } from "@opencode-ai/core/installation/version"
|
||||
import * as Log from "@opencode-ai/core/util/log"
|
||||
@@ -200,7 +200,7 @@ export const GlobalRoutes = lazy(() =>
|
||||
},
|
||||
}),
|
||||
async (c) => {
|
||||
await InstanceStore.disposeAllInstances()
|
||||
await Instance.disposeAll()
|
||||
GlobalBus.emit("event", {
|
||||
directory: "global",
|
||||
payload: {
|
||||
|
||||
@@ -1,8 +0,0 @@
|
||||
# Instance Route Parity
|
||||
|
||||
This directory contains the legacy Hono instance routes and the experimental Effect HttpApi implementation under `httpapi/`. Keep them behaviorally aligned.
|
||||
|
||||
- When adding, removing, or changing a legacy Hono route, update the matching Effect HttpApi group and handler in `httpapi/` in the same change unless the route is intentionally unsupported.
|
||||
- When changing an Effect HttpApi route, verify the legacy Hono route has the same public behavior, request shape, response shape, status codes, and instance/workspace routing semantics.
|
||||
- Keep OpenAPI/SDK-visible schemas aligned. If a difference is only an OpenAPI generation artifact, prefer fixing the source schema first; use `httpapi/public.ts` normalization only for compatibility shims that cannot be represented cleanly in the source schema.
|
||||
- Add or update parity coverage in `test/server/httpapi-bridge.test.ts` or the focused HttpApi tests when behavior or schema parity could regress.
|
||||
@@ -1,5 +1,6 @@
|
||||
import { AppRuntime } from "@/effect/app-runtime"
|
||||
import * as InstanceState from "@/effect/instance-state"
|
||||
import { InstanceBootstrap } from "@/project/bootstrap"
|
||||
import { Project } from "@/project/project"
|
||||
import { ProjectID } from "@/project/schema"
|
||||
import { Effect } from "effect"
|
||||
@@ -28,6 +29,7 @@ export const projectHandlers = HttpApiBuilder.group(InstanceHttpApi, "project",
|
||||
directory: ctx.directory,
|
||||
worktree: ctx.directory,
|
||||
project: next,
|
||||
init: () => AppRuntime.runPromise(InstanceBootstrap),
|
||||
})
|
||||
return next
|
||||
})
|
||||
|
||||
@@ -17,7 +17,7 @@ export const providerHandlers = HttpApiBuilder.group(InstanceHttpApi, "provider"
|
||||
|
||||
const list = Effect.fn("ProviderHttpApi.list")(function* () {
|
||||
const config = yield* cfg.get()
|
||||
const all = yield* ModelsDev.Service.use((s) => s.get())
|
||||
const all = yield* Effect.promise(() => ModelsDev.get())
|
||||
const disabled = new Set(config.disabled_providers ?? [])
|
||||
const enabled = config.enabled_providers ? new Set(config.enabled_providers) : undefined
|
||||
const filtered: Record<string, (typeof all)[string]> = {}
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
import { WorkspaceRef } from "@/effect/instance-ref"
|
||||
import { InstanceRef, WorkspaceRef } from "@/effect/instance-ref"
|
||||
import { AppRuntime } from "@/effect/app-runtime"
|
||||
import { InstanceBootstrap } from "@/project/bootstrap"
|
||||
import type { InstanceContext } from "@/project/instance"
|
||||
import { InstanceStore } from "@/project/instance-store"
|
||||
import { Effect, Layer } from "effect"
|
||||
import { HttpRouter, HttpServerResponse } from "effect/unstable/http"
|
||||
@@ -21,16 +23,26 @@ function decode(input: string): string {
|
||||
}
|
||||
}
|
||||
|
||||
function makeInstanceContext(
|
||||
store: InstanceStore.Interface,
|
||||
directory: string,
|
||||
): Effect.Effect<InstanceContext> {
|
||||
return store.load({
|
||||
directory: decode(directory),
|
||||
init: () => AppRuntime.runPromise(InstanceBootstrap),
|
||||
})
|
||||
}
|
||||
|
||||
function provideInstanceContext<E>(
|
||||
effect: Effect.Effect<HttpServerResponse.HttpServerResponse, E>,
|
||||
store: InstanceStore.Interface,
|
||||
bootstrap: InstanceBootstrap.Interface,
|
||||
): Effect.Effect<HttpServerResponse.HttpServerResponse, E, WorkspaceRouteContext> {
|
||||
return Effect.gen(function* () {
|
||||
const route = yield* WorkspaceRouteContext
|
||||
return yield* store.provide(
|
||||
{ directory: decode(route.directory), init: bootstrap.run },
|
||||
effect.pipe(Effect.provideService(WorkspaceRef, route.workspaceID)),
|
||||
const ctx = yield* makeInstanceContext(store, route.directory)
|
||||
return yield* effect.pipe(
|
||||
Effect.provideService(InstanceRef, ctx),
|
||||
Effect.provideService(WorkspaceRef, route.workspaceID),
|
||||
)
|
||||
})
|
||||
}
|
||||
@@ -39,15 +51,13 @@ export const instanceContextLayer = Layer.effect(
|
||||
InstanceContextMiddleware,
|
||||
Effect.gen(function* () {
|
||||
const store = yield* InstanceStore.Service
|
||||
const bootstrap = yield* InstanceBootstrap.Service
|
||||
return InstanceContextMiddleware.of((effect) => provideInstanceContext(effect, store, bootstrap))
|
||||
return InstanceContextMiddleware.of((effect) => provideInstanceContext(effect, store))
|
||||
}),
|
||||
)
|
||||
|
||||
export const instanceRouterMiddleware = HttpRouter.middleware()(
|
||||
Effect.gen(function* () {
|
||||
const store = yield* InstanceStore.Service
|
||||
const bootstrap = yield* InstanceBootstrap.Service
|
||||
return (effect) => provideInstanceContext(effect, store, bootstrap)
|
||||
return (effect) => provideInstanceContext(effect, store)
|
||||
}),
|
||||
)
|
||||
|
||||
@@ -2,7 +2,7 @@ import { getAdapter } from "@/control-plane/adapters"
|
||||
import { WorkspaceID } from "@/control-plane/schema"
|
||||
import type { Target } from "@/control-plane/types"
|
||||
import { Workspace } from "@/control-plane/workspace"
|
||||
import { EffectBridge } from "@/effect/bridge"
|
||||
import { Instance } from "@/project/instance"
|
||||
import { Session } from "@/session/session"
|
||||
import { HttpApiProxy } from "./proxy"
|
||||
import * as Fence from "@/server/fence"
|
||||
@@ -43,6 +43,14 @@ export class WorkspaceRoutingMiddleware extends HttpApiMiddleware.Service<
|
||||
}
|
||||
>()("@opencode/ExperimentalHttpApiWorkspaceRouting") {}
|
||||
|
||||
function currentDirectory(): string {
|
||||
try {
|
||||
return Instance.directory
|
||||
} catch {
|
||||
return process.cwd()
|
||||
}
|
||||
}
|
||||
|
||||
function requestURL(request: HttpServerRequest.HttpServerRequest): URL {
|
||||
return new URL(request.url, "http://localhost")
|
||||
}
|
||||
@@ -57,7 +65,7 @@ function selectedWorkspaceID(url: URL, sessionWorkspaceID?: WorkspaceID): Worksp
|
||||
}
|
||||
|
||||
function defaultDirectory(request: HttpServerRequest.HttpServerRequest, url: URL): string {
|
||||
return url.searchParams.get("directory") || request.headers["x-opencode-directory"] || process.cwd()
|
||||
return url.searchParams.get("directory") || request.headers["x-opencode-directory"] || currentDirectory()
|
||||
}
|
||||
|
||||
function shouldStayOnControlPlane(request: HttpServerRequest.HttpServerRequest, url: URL): boolean {
|
||||
@@ -80,8 +88,10 @@ function missingWorkspaceResponse(id: WorkspaceID): HttpServerResponse.HttpServe
|
||||
}
|
||||
|
||||
function resolveTarget(workspace: Workspace.Info): Effect.Effect<Target> {
|
||||
const adapter = getAdapter(workspace.projectID, workspace.type)
|
||||
return EffectBridge.fromPromise(() => adapter.target(workspace))
|
||||
return Effect.gen(function* () {
|
||||
const adapter = yield* Effect.sync(() => getAdapter(workspace.projectID, workspace.type))
|
||||
return yield* Effect.promise(() => Promise.resolve(adapter.target(workspace)))
|
||||
})
|
||||
}
|
||||
|
||||
function proxyRemote(
|
||||
|
||||
@@ -39,7 +39,6 @@ type OpenApiSchema = {
|
||||
maximum?: number
|
||||
minimum?: number
|
||||
oneOf?: OpenApiSchema[]
|
||||
pattern?: string
|
||||
prefixItems?: OpenApiSchema[]
|
||||
properties?: Record<string, OpenApiSchema>
|
||||
required?: string[]
|
||||
@@ -75,18 +74,9 @@ const QueryNumberParameters = new Set(["start", "cursor", "limit", "method"])
|
||||
const QueryBooleanParameters = new Set(["roots", "archived"])
|
||||
const QueryParameterSchemas = {
|
||||
"GET /find/file limit": { type: "integer", minimum: 1, maximum: 200 },
|
||||
"GET /session/{sessionID}/diff messageID": { type: "string", pattern: "^msg.*" },
|
||||
"GET /session/{sessionID}/message limit": { type: "integer", minimum: 0, maximum: Number.MAX_SAFE_INTEGER },
|
||||
} satisfies Record<string, OpenApiSchema>
|
||||
|
||||
const PathParameterSchemas = {
|
||||
sessionID: { type: "string", pattern: "^ses.*" },
|
||||
messageID: { type: "string", pattern: "^msg.*" },
|
||||
partID: { type: "string", pattern: "^prt.*" },
|
||||
permissionID: { type: "string", pattern: "^per.*" },
|
||||
ptyID: { type: "string", pattern: "^pty.*" },
|
||||
} satisfies Record<string, OpenApiSchema>
|
||||
|
||||
const LegacyComponentDescriptions = {
|
||||
LogLevel: "Log level",
|
||||
ServerConfig: "Server configuration for opencode serve and web commands",
|
||||
@@ -438,11 +428,6 @@ function fixSelfReferencingComponents(spec: OpenApiSpec) {
|
||||
|
||||
/** Strip `{type:"null"}` arms that Effect's `Schema.optional` adds to OpenAPI unions. */
|
||||
function stripOptionalNull(schema: OpenApiSchema): OpenApiSchema {
|
||||
if (schema.allOf?.length === 1) {
|
||||
const [constraint] = schema.allOf
|
||||
delete schema.allOf
|
||||
return stripOptionalNull({ ...schema, ...constraint })
|
||||
}
|
||||
if (isEmptyObjectUnion(schema)) return { type: "object", properties: {} }
|
||||
const options = flattenOptions(schema.anyOf ?? schema.oneOf)
|
||||
if (options) {
|
||||
@@ -491,40 +476,25 @@ function flattenOptions(options: OpenApiSchema[] | undefined): OpenApiSchema[] |
|
||||
}
|
||||
|
||||
function normalizeParameter(param: OpenApiParameter, route: string) {
|
||||
if (!param.schema || typeof param.schema !== "object") return
|
||||
if (param.in === "path") {
|
||||
param.schema = pathParameterSchema(route, param.name) ?? stripOptionalNull(param.schema)
|
||||
if (param.in !== "query" || !param.schema || typeof param.schema !== "object") return
|
||||
const override = QueryParameterSchemas[`${route} ${param.name}` as keyof typeof QueryParameterSchemas]
|
||||
if (override) {
|
||||
param.schema = override
|
||||
return
|
||||
}
|
||||
if (param.in === "query") {
|
||||
const override = QueryParameterSchemas[`${route} ${param.name}` as keyof typeof QueryParameterSchemas]
|
||||
if (override) {
|
||||
param.schema = override
|
||||
return
|
||||
}
|
||||
if (QueryNumberParameters.has(param.name)) {
|
||||
param.schema = { type: "number" }
|
||||
return
|
||||
}
|
||||
if (QueryBooleanParameters.has(param.name)) {
|
||||
param.schema = {
|
||||
anyOf: [{ type: "boolean" }, { type: "string", enum: ["true", "false"] }],
|
||||
}
|
||||
return
|
||||
if (QueryNumberParameters.has(param.name)) {
|
||||
param.schema = { type: "number" }
|
||||
return
|
||||
}
|
||||
if (QueryBooleanParameters.has(param.name)) {
|
||||
param.schema = {
|
||||
anyOf: [{ type: "boolean" }, { type: "string", enum: ["true", "false"] }],
|
||||
}
|
||||
return
|
||||
}
|
||||
param.schema = stripOptionalNull(param.schema)
|
||||
}
|
||||
|
||||
function pathParameterSchema(route: string, name: string) {
|
||||
if (name in PathParameterSchemas) return PathParameterSchemas[name as keyof typeof PathParameterSchemas]
|
||||
if (name === "id" && route.startsWith("DELETE /experimental/workspace/")) return { type: "string", pattern: "^wrk.*" }
|
||||
if (name === "id" && route.startsWith("POST /experimental/workspace/")) return { type: "string", pattern: "^wrk.*" }
|
||||
if (name === "requestID" && route.startsWith("POST /permission/")) return { type: "string", pattern: "^per.*" }
|
||||
if (name === "requestID" && route.startsWith("POST /question/")) return { type: "string", pattern: "^que.*" }
|
||||
return undefined
|
||||
}
|
||||
|
||||
export const PublicApi = OpenCodeHttpApi.annotateMerge(
|
||||
OpenApi.annotations({
|
||||
title: "opencode",
|
||||
|
||||
@@ -11,19 +11,15 @@ import { Config } from "@/config/config"
|
||||
import { Command } from "@/command"
|
||||
import * as Observability from "@opencode-ai/core/effect/observability"
|
||||
import { File } from "@/file"
|
||||
import { FileWatcher } from "@/file/watcher"
|
||||
import { Ripgrep } from "@/file/ripgrep"
|
||||
import { Format } from "@/format"
|
||||
import { LSP } from "@/lsp/lsp"
|
||||
import { MCP } from "@/mcp"
|
||||
import { Permission } from "@/permission"
|
||||
import { Installation } from "@/installation"
|
||||
import { InstanceBootstrap } from "@/project/bootstrap"
|
||||
import { InstanceStore } from "@/project/instance-store"
|
||||
import { Plugin } from "@/plugin"
|
||||
import { Project } from "@/project/project"
|
||||
import { ProviderAuth } from "@/provider/auth"
|
||||
import { ModelsDev } from "@/provider/models"
|
||||
import { Provider } from "@/provider/provider"
|
||||
import { Pty } from "@/pty"
|
||||
import { Question } from "@/question"
|
||||
@@ -36,9 +32,7 @@ import { SessionStatus } from "@/session/status"
|
||||
import { SessionSummary } from "@/session/summary"
|
||||
import { Todo } from "@/session/todo"
|
||||
import { SessionShare } from "@/share/session"
|
||||
import { ShareNext } from "@/share/share-next"
|
||||
import { Skill } from "@/skill"
|
||||
import { Snapshot } from "@/snapshot"
|
||||
import { SyncEvent } from "@/sync"
|
||||
import { ToolRegistry } from "@/tool/registry"
|
||||
import { lazy } from "@/util/lazy"
|
||||
@@ -149,16 +143,12 @@ export function createRoutes(corsOptions?: CorsOptions) {
|
||||
Command.defaultLayer,
|
||||
Config.defaultLayer,
|
||||
File.defaultLayer,
|
||||
FileWatcher.defaultLayer,
|
||||
Format.defaultLayer,
|
||||
LSP.defaultLayer,
|
||||
Installation.defaultLayer,
|
||||
InstanceBootstrap.defaultLayer,
|
||||
InstanceStore.defaultLayer,
|
||||
MCP.defaultLayer,
|
||||
ModelsDev.defaultLayer,
|
||||
Permission.defaultLayer,
|
||||
Plugin.defaultLayer,
|
||||
Project.defaultLayer,
|
||||
ProviderAuth.defaultLayer,
|
||||
Provider.defaultLayer,
|
||||
@@ -173,8 +163,6 @@ export function createRoutes(corsOptions?: CorsOptions) {
|
||||
SessionRunState.defaultLayer,
|
||||
SessionStatus.defaultLayer,
|
||||
SessionSummary.defaultLayer,
|
||||
ShareNext.defaultLayer,
|
||||
Snapshot.defaultLayer,
|
||||
SyncEvent.defaultLayer,
|
||||
Skill.defaultLayer,
|
||||
Todo.defaultLayer,
|
||||
|
||||
@@ -6,7 +6,6 @@ import z from "zod"
|
||||
import { Format } from "@/format"
|
||||
import { TuiRoutes } from "./tui"
|
||||
import { Instance } from "@/project/instance"
|
||||
import { InstanceStore } from "@/project/instance-store"
|
||||
import { Vcs } from "@/project/vcs"
|
||||
import { Agent } from "@/agent/agent"
|
||||
import { Skill } from "@/skill"
|
||||
@@ -63,7 +62,7 @@ export const InstanceRoutes = (upgrade: UpgradeWebSocket): Hono => {
|
||||
},
|
||||
}),
|
||||
async (c) => {
|
||||
await InstanceStore.disposeInstance(Instance.current)
|
||||
await Instance.dispose()
|
||||
return c.json(true)
|
||||
},
|
||||
)
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import type { MiddlewareHandler } from "hono"
|
||||
import { Instance } from "@/project/instance"
|
||||
import { getBootstrapRunEffect } from "@/effect/app-runtime"
|
||||
import { InstanceBootstrap } from "@/project/bootstrap"
|
||||
import { AppRuntime } from "@/effect/app-runtime"
|
||||
import { AppFileSystem } from "@opencode-ai/core/filesystem"
|
||||
import { WorkspaceContext } from "@/control-plane/workspace-context"
|
||||
import { WorkspaceID } from "@/control-plane/schema"
|
||||
@@ -23,7 +24,7 @@ export function InstanceMiddleware(workspaceID?: WorkspaceID): MiddlewareHandler
|
||||
async fn() {
|
||||
return Instance.provide({
|
||||
directory,
|
||||
init: await getBootstrapRunEffect(),
|
||||
init: () => AppRuntime.runPromise(InstanceBootstrap),
|
||||
async fn() {
|
||||
return next()
|
||||
},
|
||||
|
||||
@@ -2,13 +2,13 @@ import { Hono } from "hono"
|
||||
import { describeRoute, validator } from "hono-openapi"
|
||||
import { resolver } from "hono-openapi"
|
||||
import { Instance } from "@/project/instance"
|
||||
import { InstanceStore } from "@/project/instance-store"
|
||||
import { Project } from "@/project/project"
|
||||
import z from "zod"
|
||||
import { ProjectID } from "@/project/schema"
|
||||
import { errors } from "../../error"
|
||||
import { lazy } from "@/util/lazy"
|
||||
import { getBootstrapRunEffect } from "@/effect/app-runtime"
|
||||
import { InstanceBootstrap } from "@/project/bootstrap"
|
||||
import { AppRuntime } from "@/effect/app-runtime"
|
||||
import { jsonRequest, runRequest } from "./trace"
|
||||
|
||||
export const ProjectRoutes = lazy(() =>
|
||||
@@ -82,7 +82,12 @@ export const ProjectRoutes = lazy(() =>
|
||||
Project.Service.use((svc) => svc.initGit({ directory: dir, project: prev })),
|
||||
)
|
||||
if (next.id === prev.id && next.vcs === prev.vcs && next.worktree === prev.worktree) return c.json(next)
|
||||
await InstanceStore.reloadInstance({ directory: dir, worktree: dir, project: next, init: await getBootstrapRunEffect() })
|
||||
await Instance.reload({
|
||||
directory: dir,
|
||||
worktree: dir,
|
||||
project: next,
|
||||
init: () => AppRuntime.runPromise(InstanceBootstrap),
|
||||
})
|
||||
return c.json(next)
|
||||
},
|
||||
)
|
||||
|
||||
@@ -36,7 +36,7 @@ export const ProviderRoutes = lazy(() =>
|
||||
const svc = yield* Provider.Service
|
||||
const cfg = yield* Config.Service
|
||||
const config = yield* cfg.get()
|
||||
const all = yield* ModelsDev.Service.use((s) => s.get())
|
||||
const all = yield* Effect.promise(() => ModelsDev.get())
|
||||
const disabled = new Set(config.disabled_providers ?? [])
|
||||
const enabled = config.enabled_providers ? new Set(config.enabled_providers) : undefined
|
||||
const filtered: Record<string, (typeof all)[string]> = {}
|
||||
|
||||
@@ -5,10 +5,11 @@ import { WorkspaceID } from "@/control-plane/schema"
|
||||
import { WorkspaceContext } from "@/control-plane/workspace-context"
|
||||
import { Workspace } from "@/control-plane/workspace"
|
||||
import { Flag } from "@opencode-ai/core/flag/flag"
|
||||
import { getBootstrapRunEffect, AppRuntime } from "@/effect/app-runtime"
|
||||
import { InstanceBootstrap } from "@/project/bootstrap"
|
||||
import { Instance } from "@/project/instance"
|
||||
import { Session } from "@/session/session"
|
||||
import { SessionID } from "@/session/schema"
|
||||
import { AppRuntime } from "@/effect/app-runtime"
|
||||
import { Effect } from "effect"
|
||||
import * as Log from "@opencode-ai/core/util/log"
|
||||
import { ServerProxy } from "./proxy"
|
||||
@@ -94,13 +95,12 @@ export function WorkspaceRouterMiddleware(upgrade: UpgradeWebSocket): Middleware
|
||||
const target = await adapter.target(workspace)
|
||||
|
||||
if (target.type === "local") {
|
||||
const init = await getBootstrapRunEffect()
|
||||
return WorkspaceContext.provide({
|
||||
workspaceID: WorkspaceID.make(workspaceID),
|
||||
fn: () =>
|
||||
Instance.provide({
|
||||
directory: target.directory,
|
||||
init,
|
||||
init: () => AppRuntime.runPromise(InstanceBootstrap),
|
||||
async fn() {
|
||||
return next()
|
||||
},
|
||||
|
||||
@@ -464,18 +464,9 @@ NOTE: At any point in time through this workflow you should feel free to ask the
|
||||
{ tool: key, sessionID: ctx.sessionID, callID: opts.toolCallId },
|
||||
{ args },
|
||||
)
|
||||
const result: Awaited<ReturnType<NonNullable<typeof execute>>> = yield* Effect.gen(function* () {
|
||||
yield* ctx.ask({ permission: key, metadata: {}, patterns: ["*"], always: ["*"] })
|
||||
return yield* Effect.promise(() => execute(args, opts))
|
||||
}).pipe(
|
||||
Effect.withSpan("Tool.execute", {
|
||||
attributes: {
|
||||
"tool.name": key,
|
||||
"tool.call_id": opts.toolCallId,
|
||||
"session.id": ctx.sessionID,
|
||||
"message.id": input.processor.message.id,
|
||||
},
|
||||
}),
|
||||
yield* ctx.ask({ permission: key, metadata: {}, patterns: ["*"], always: ["*"] })
|
||||
const result: Awaited<ReturnType<NonNullable<typeof execute>>> = yield* Effect.promise(() =>
|
||||
execute(args, opts),
|
||||
)
|
||||
yield* plugin.trigger(
|
||||
"tool.execute.after",
|
||||
|
||||
@@ -6,7 +6,7 @@ import * as Tool from "./tool"
|
||||
import path from "path"
|
||||
import DESCRIPTION from "./bash.txt"
|
||||
import * as Log from "@opencode-ai/core/util/log"
|
||||
import { containsPath, type InstanceContext } from "../project/instance-context"
|
||||
import { Instance, type InstanceContext } from "../project/instance"
|
||||
import { lazy } from "@/util/lazy"
|
||||
import { Language, type Node } from "web-tree-sitter"
|
||||
|
||||
@@ -386,7 +386,7 @@ export const BashTool = Tool.define(
|
||||
for (const arg of pathArgs(command, ps)) {
|
||||
const resolved = yield* argPath(arg, cwd, ps, shell)
|
||||
log.info("resolved path", { arg, resolved })
|
||||
if (!resolved || containsPath(resolved, instance)) continue
|
||||
if (!resolved || Instance.containsPath(resolved, instance)) continue
|
||||
const dir = (yield* fs.isDir(resolved)) ? resolved : path.dirname(resolved)
|
||||
scan.dirs.add(dir)
|
||||
}
|
||||
@@ -612,7 +612,7 @@ export const BashTool = Tool.define(
|
||||
Effect.sync(() => tree.delete()),
|
||||
)
|
||||
const scan = yield* collect(tree.rootNode, cwd, ps, shell, executeInstance)
|
||||
if (!containsPath(cwd, executeInstance)) scan.dirs.add(cwd)
|
||||
if (!Instance.containsPath(cwd, executeInstance)) scan.dirs.add(cwd)
|
||||
yield* ask(ctx, scan)
|
||||
}),
|
||||
)
|
||||
|
||||
@@ -3,7 +3,7 @@ import { Effect } from "effect"
|
||||
import * as EffectLogger from "@opencode-ai/core/effect/logger"
|
||||
import { InstanceState } from "@/effect/instance-state"
|
||||
import type * as Tool from "./tool"
|
||||
import { containsPath } from "../project/instance-context"
|
||||
import { Instance } from "../project/instance"
|
||||
import { AppFileSystem } from "@opencode-ai/core/filesystem"
|
||||
|
||||
type Kind = "file" | "directory"
|
||||
@@ -24,7 +24,7 @@ export const assertExternalDirectoryEffect = Effect.fn("Tool.assertExternalDirec
|
||||
|
||||
const ins = yield* InstanceState.context
|
||||
const full = process.platform === "win32" ? AppFileSystem.normalizePath(target) : target
|
||||
if (containsPath(full, ins)) return
|
||||
if (Instance.containsPath(full, ins)) return
|
||||
|
||||
const kind = options?.kind ?? "file"
|
||||
const dir = kind === "directory" ? full : path.dirname(full)
|
||||
|
||||
@@ -10,7 +10,7 @@ import DESCRIPTION from "./read.txt"
|
||||
import { InstanceState } from "@/effect/instance-state"
|
||||
import { assertExternalDirectoryEffect } from "./external-directory"
|
||||
import { Instruction } from "../session/instruction"
|
||||
import { isPdfAttachment, sniffAttachmentMime } from "@/util/media"
|
||||
import { isImageAttachment, isPdfAttachment, sniffAttachmentMime } from "@/util/media"
|
||||
|
||||
const DEFAULT_READ_LIMIT = 2000
|
||||
const MAX_LINE_LENGTH = 2000
|
||||
@@ -18,7 +18,6 @@ const MAX_LINE_SUFFIX = `... (line truncated to ${MAX_LINE_LENGTH} chars)`
|
||||
const MAX_BYTES = 50 * 1024
|
||||
const MAX_BYTES_LABEL = `${MAX_BYTES / 1024} KB`
|
||||
const SAMPLE_BYTES = 4096
|
||||
const SUPPORTED_IMAGE_MIMES = new Set(["image/jpeg", "image/png", "image/gif", "image/webp"])
|
||||
|
||||
// `offset` and `limit` were originally `z.coerce.number()` — the runtime
|
||||
// coercion was useful when the tool was called from a shell but serves no
|
||||
@@ -154,6 +153,10 @@ export const ReadTool = Tool.define(
|
||||
params: Schema.Schema.Type<typeof Parameters>,
|
||||
ctx: Tool.Context,
|
||||
) {
|
||||
if (params.offset !== undefined && params.offset < 1) {
|
||||
return yield* Effect.fail(new Error("offset must be greater than or equal to 1"))
|
||||
}
|
||||
|
||||
const instance = yield* InstanceState.context
|
||||
let filepath = params.filePath
|
||||
if (!path.isAbsolute(filepath)) {
|
||||
@@ -188,7 +191,7 @@ export const ReadTool = Tool.define(
|
||||
if (stat.type === "Directory") {
|
||||
const items = yield* list(filepath)
|
||||
const limit = params.limit ?? DEFAULT_READ_LIMIT
|
||||
const offset = params.offset || 1
|
||||
const offset = params.offset ?? 1
|
||||
const start = offset - 1
|
||||
const sliced = items.slice(start, start + limit)
|
||||
const truncated = start + sliced.length < items.length
|
||||
@@ -217,9 +220,7 @@ export const ReadTool = Tool.define(
|
||||
const sample = yield* readSample(filepath, Number(stat.size), SAMPLE_BYTES)
|
||||
|
||||
const mime = sniffAttachmentMime(sample, AppFileSystem.mimeType(filepath))
|
||||
const isImage = SUPPORTED_IMAGE_MIMES.has(mime)
|
||||
|
||||
if (isImage || isPdfAttachment(mime)) {
|
||||
if (isImageAttachment(mime) || isPdfAttachment(mime)) {
|
||||
const bytes = yield* fs.readFile(filepath)
|
||||
const msg = isPdfAttachment(mime) ? "PDF read successfully" : "Image read successfully"
|
||||
return {
|
||||
@@ -245,7 +246,7 @@ export const ReadTool = Tool.define(
|
||||
}
|
||||
|
||||
const file = yield* Effect.promise(() =>
|
||||
lines(filepath, { limit: params.limit ?? DEFAULT_READ_LIMIT, offset: params.offset || 1 }),
|
||||
lines(filepath, { limit: params.limit ?? DEFAULT_READ_LIMIT, offset: params.offset ?? 1 }),
|
||||
)
|
||||
if (file.count < file.offset && !(file.count === 0 && file.offset === 1)) {
|
||||
return yield* Effect.fail(
|
||||
|
||||
@@ -154,16 +154,7 @@ export const layer: Layer.Layer<
|
||||
...(out.truncated && { outputPath: out.outputPath }),
|
||||
},
|
||||
}
|
||||
}).pipe(
|
||||
Effect.withSpan("Tool.execute", {
|
||||
attributes: {
|
||||
"tool.name": id,
|
||||
"session.id": toolCtx.sessionID,
|
||||
"message.id": toolCtx.messageID,
|
||||
...(toolCtx.callID ? { "tool.call_id": toolCtx.callID } : {}),
|
||||
},
|
||||
}),
|
||||
),
|
||||
}),
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -2,6 +2,7 @@ import z from "zod"
|
||||
import { NamedError } from "@opencode-ai/core/util/error"
|
||||
import { Global } from "@opencode-ai/core/global"
|
||||
import { Instance } from "../project/instance"
|
||||
import { InstanceBootstrap } from "../project/bootstrap"
|
||||
import { Project } from "@/project/project"
|
||||
import { Database } from "@/storage/db"
|
||||
import { eq } from "drizzle-orm"
|
||||
@@ -254,6 +255,7 @@ export const layer: Layer.Layer<
|
||||
const booted = yield* Effect.promise(() =>
|
||||
Instance.provide({
|
||||
directory: info.directory,
|
||||
init: () => BootstrapRuntime.runPromise(InstanceBootstrap),
|
||||
fn: () => undefined,
|
||||
})
|
||||
.then(() => true)
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { afterEach, test, expect } from "bun:test"
|
||||
import { Effect } from "effect"
|
||||
import path from "path"
|
||||
import { disposeAllInstances, provideInstance, tmpdir } from "../fixture/fixture"
|
||||
import { provideInstance, tmpdir } from "../fixture/fixture"
|
||||
import { Instance } from "../../src/project/instance"
|
||||
import { Agent } from "../../src/agent/agent"
|
||||
import { Permission } from "../../src/permission"
|
||||
@@ -18,7 +18,7 @@ function load<A>(dir: string, fn: (svc: Agent.Interface) => Effect.Effect<A>) {
|
||||
}
|
||||
|
||||
afterEach(async () => {
|
||||
await disposeAllInstances()
|
||||
await Instance.disposeAll()
|
||||
})
|
||||
|
||||
test("returns default native agents when no config", async () => {
|
||||
|
||||
@@ -4,7 +4,7 @@ import { Bus } from "../../src/bus"
|
||||
import { BusEvent } from "../../src/bus/bus-event"
|
||||
import { Instance } from "../../src/project/instance"
|
||||
import { CrossSpawnSpawner } from "@opencode-ai/core/cross-spawn-spawner"
|
||||
import { disposeAllInstances, provideInstance, provideTmpdirInstance, tmpdirScoped } from "../fixture/fixture"
|
||||
import { provideInstance, provideTmpdirInstance, tmpdirScoped } from "../fixture/fixture"
|
||||
import { testEffect } from "../lib/effect"
|
||||
|
||||
const TestEvent = {
|
||||
@@ -151,7 +151,7 @@ describe("Bus (Effect-native)", () => {
|
||||
}).pipe(provideInstance(dir))
|
||||
|
||||
// Dispose from OUTSIDE the instance scope
|
||||
yield* Effect.promise(disposeAllInstances)
|
||||
yield* Effect.promise(() => Instance.disposeAll())
|
||||
yield* Deferred.await(disposed).pipe(Effect.timeout("2 seconds"))
|
||||
|
||||
expect(types).toContain("test.effect.ping")
|
||||
|
||||
@@ -3,7 +3,7 @@ import { Schema } from "effect"
|
||||
import { Bus } from "../../src/bus"
|
||||
import { BusEvent } from "../../src/bus/bus-event"
|
||||
import { Instance } from "../../src/project/instance"
|
||||
import { disposeAllInstances, tmpdir } from "../fixture/fixture"
|
||||
import { tmpdir } from "../fixture/fixture"
|
||||
|
||||
const TestEvent = BusEvent.define("test.integration", Schema.Struct({ value: Schema.Number }))
|
||||
|
||||
@@ -12,7 +12,7 @@ function withInstance(directory: string, fn: () => Promise<void>) {
|
||||
}
|
||||
|
||||
describe("Bus integration: acquireRelease subscriber pattern", () => {
|
||||
afterEach(() => disposeAllInstances())
|
||||
afterEach(() => Instance.disposeAll())
|
||||
|
||||
test("subscriber via callback facade receives events and cleans up on unsub", async () => {
|
||||
await using tmp = await tmpdir()
|
||||
@@ -78,7 +78,7 @@ describe("Bus integration: acquireRelease subscriber pattern", () => {
|
||||
await Bun.sleep(10)
|
||||
})
|
||||
|
||||
await disposeAllInstances()
|
||||
await Instance.disposeAll()
|
||||
await Bun.sleep(50)
|
||||
|
||||
expect(received).toEqual([1])
|
||||
|
||||
@@ -3,7 +3,7 @@ import { Schema } from "effect"
|
||||
import { Bus } from "../../src/bus"
|
||||
import { BusEvent } from "../../src/bus/bus-event"
|
||||
import { Instance } from "../../src/project/instance"
|
||||
import { disposeAllInstances, tmpdir } from "../fixture/fixture"
|
||||
import { tmpdir } from "../fixture/fixture"
|
||||
|
||||
const TestEvent = {
|
||||
Ping: BusEvent.define("test.ping", Schema.Struct({ value: Schema.Number })),
|
||||
@@ -15,7 +15,7 @@ function withInstance(directory: string, fn: () => Promise<void>) {
|
||||
}
|
||||
|
||||
describe("Bus", () => {
|
||||
afterEach(() => disposeAllInstances())
|
||||
afterEach(() => Instance.disposeAll())
|
||||
|
||||
describe("publish + subscribe", () => {
|
||||
test("subscriber is live immediately after subscribe returns", async () => {
|
||||
@@ -208,8 +208,8 @@ describe("Bus", () => {
|
||||
await Bun.sleep(10)
|
||||
})
|
||||
|
||||
// disposeAllInstances triggers the finalizer which publishes InstanceDisposed
|
||||
await disposeAllInstances()
|
||||
// Instance.disposeAll triggers the finalizer which publishes InstanceDisposed
|
||||
await Instance.disposeAll()
|
||||
await Bun.sleep(50)
|
||||
|
||||
expect(received).toContain("test.ping")
|
||||
|
||||
@@ -1,38 +0,0 @@
|
||||
import { describe, expect, test } from "bun:test"
|
||||
import { computePromptTraits } from "../../../../src/cli/cmd/tui/component/prompt/traits"
|
||||
|
||||
describe("computePromptTraits", () => {
|
||||
test("normal mode without autocomplete only captures tab", () => {
|
||||
const traits = computePromptTraits({ mode: "normal", disabled: false, autocompleteVisible: false })
|
||||
expect(traits.capture).toEqual(["tab"])
|
||||
expect(traits.suspend).toBe(false)
|
||||
expect(traits.status).toBeUndefined()
|
||||
})
|
||||
|
||||
test("normal mode with autocomplete captures navigation keys", () => {
|
||||
const traits = computePromptTraits({ mode: "normal", disabled: false, autocompleteVisible: true })
|
||||
expect(traits.capture).toEqual(["escape", "navigate", "submit", "tab"])
|
||||
expect(traits.suspend).toBe(false)
|
||||
expect(traits.status).toBeUndefined()
|
||||
})
|
||||
|
||||
test("shell mode does not suspend the textarea", () => {
|
||||
// Suspending the textarea would gate every keybinding action
|
||||
// (backspace, delete-word-backward, arrow movement, etc.) — see
|
||||
// @opentui/core 0.2.x TextareaRenderable.handleKeyPress. Shell mode is
|
||||
// an active editing mode, so suspend must stay off.
|
||||
const traits = computePromptTraits({ mode: "shell", disabled: false, autocompleteVisible: false })
|
||||
expect(traits.suspend).toBe(false)
|
||||
})
|
||||
|
||||
test("shell mode disables capture and labels the prompt", () => {
|
||||
const traits = computePromptTraits({ mode: "shell", disabled: false, autocompleteVisible: false })
|
||||
expect(traits.capture).toBeUndefined()
|
||||
expect(traits.status).toBe("SHELL")
|
||||
})
|
||||
|
||||
test("disabled suspends regardless of mode", () => {
|
||||
expect(computePromptTraits({ mode: "normal", disabled: true, autocompleteVisible: false }).suspend).toBe(true)
|
||||
expect(computePromptTraits({ mode: "shell", disabled: true, autocompleteVisible: false }).suspend).toBe(true)
|
||||
})
|
||||
})
|
||||
@@ -12,7 +12,7 @@ import { Account } from "../../src/account/account"
|
||||
import { AccessToken, AccountID, OrgID } from "../../src/account/schema"
|
||||
import { AppFileSystem } from "@opencode-ai/core/filesystem"
|
||||
import { Env } from "../../src/env"
|
||||
import { disposeAllInstances, provideTmpdirInstance } from "../fixture/fixture"
|
||||
import { provideTmpdirInstance } from "../fixture/fixture"
|
||||
import { tmpdir } from "../fixture/fixture"
|
||||
import { CrossSpawnSpawner } from "@opencode-ai/core/cross-spawn-spawner"
|
||||
import { testEffect } from "../lib/effect"
|
||||
@@ -108,7 +108,7 @@ async function check(map: (dir: string) => string) {
|
||||
},
|
||||
})
|
||||
} finally {
|
||||
await disposeAllInstances()
|
||||
await Instance.disposeAll()
|
||||
;(Global.Path as { config: string }).config = prev
|
||||
await clear()
|
||||
}
|
||||
|
||||
@@ -21,7 +21,7 @@ import { ModelID, ProviderID } from "@/provider/schema"
|
||||
import { SyncEvent } from "@/sync"
|
||||
import { EventSequenceTable, EventTable } from "@/sync/event.sql"
|
||||
import { resetDatabase } from "../fixture/db"
|
||||
import { disposeAllInstances, provideTmpdirInstance, tmpdir } from "../fixture/fixture"
|
||||
import { provideTmpdirInstance, tmpdir } from "../fixture/fixture"
|
||||
import { testEffect } from "../lib/effect"
|
||||
import { registerAdapter } from "../../src/control-plane/adapters"
|
||||
import { WorkspaceID } from "../../src/control-plane/schema"
|
||||
@@ -93,7 +93,7 @@ beforeEach(() => {
|
||||
|
||||
afterEach(async () => {
|
||||
mock.restore()
|
||||
await disposeAllInstances()
|
||||
await Instance.disposeAll()
|
||||
Flag.OPENCODE_EXPERIMENTAL_WORKSPACES = originalWorkspacesFlag
|
||||
restoreEnv()
|
||||
await resetDatabase()
|
||||
|
||||
@@ -3,9 +3,8 @@ import { CrossSpawnSpawner } from "@opencode-ai/core/cross-spawn-spawner"
|
||||
import { $ } from "bun"
|
||||
import { Context, Deferred, Duration, Effect, Exit, Fiber, Layer } from "effect"
|
||||
import { InstanceState } from "@/effect/instance-state"
|
||||
import { InstanceStore } from "../../src/project/instance-store"
|
||||
import { Instance } from "../../src/project/instance"
|
||||
import { disposeAllInstances, provideInstance, tmpdirScoped } from "../fixture/fixture"
|
||||
import { provideInstance, tmpdirScoped } from "../fixture/fixture"
|
||||
import { testEffect } from "../lib/effect"
|
||||
|
||||
const it = testEffect(CrossSpawnSpawner.defaultLayer)
|
||||
@@ -20,7 +19,7 @@ const tmpdirGitScoped = Effect.gen(function* () {
|
||||
})
|
||||
|
||||
afterEach(async () => {
|
||||
await disposeAllInstances()
|
||||
await Instance.disposeAll()
|
||||
})
|
||||
|
||||
it.live("InstanceState caches values per directory", () =>
|
||||
@@ -70,7 +69,7 @@ it.live("InstanceState invalidates on reload", () =>
|
||||
)
|
||||
|
||||
const a = yield* access(state, dir)
|
||||
yield* Effect.promise(() => InstanceStore.reloadInstance({ directory: dir }))
|
||||
yield* Effect.promise(() => Instance.reload({ directory: dir }))
|
||||
const b = yield* access(state, dir)
|
||||
|
||||
expect(a).not.toBe(b)
|
||||
@@ -95,7 +94,7 @@ it.live("InstanceState invalidates on disposeAll", () =>
|
||||
|
||||
yield* access(state, one)
|
||||
yield* access(state, two)
|
||||
yield* Effect.promise(disposeAllInstances)
|
||||
yield* Effect.promise(() => Instance.disposeAll())
|
||||
|
||||
expect(seen.sort()).toEqual([one, two].sort())
|
||||
}),
|
||||
@@ -270,7 +269,7 @@ it.live("InstanceState correct after interleaved init and dispose", () =>
|
||||
|
||||
const [, b] = yield* Effect.all(
|
||||
[
|
||||
Effect.promise(() => InstanceStore.reloadInstance({ directory: one })),
|
||||
Effect.promise(() => Instance.reload({ directory: one })),
|
||||
Test.use((svc) => svc.get()).pipe(provideInstance(two)),
|
||||
],
|
||||
{ concurrency: "unbounded" },
|
||||
|
||||
@@ -1,12 +1,9 @@
|
||||
import { expect } from "bun:test"
|
||||
import { Effect, Layer, Context } from "effect"
|
||||
import { InstanceRef } from "../../src/effect/instance-ref"
|
||||
import { makeRuntime } from "../../src/effect/run-service"
|
||||
import { ProjectID } from "../../src/project/schema"
|
||||
import { it } from "../lib/effect"
|
||||
|
||||
class Shared extends Context.Service<Shared, { readonly id: number }>()("@test/Shared") {}
|
||||
const testDirectory = "/tmp/opencode-test"
|
||||
|
||||
it.live("makeRuntime shares dependent layers through the shared memo map", () =>
|
||||
Effect.gen(function* () {
|
||||
@@ -50,40 +47,3 @@ it.live("makeRuntime shares dependent layers through the shared memo map", () =>
|
||||
expect(n).toBe(1)
|
||||
}),
|
||||
)
|
||||
|
||||
it.live("makeRuntime inherits InstanceRef from the current fiber", () =>
|
||||
Effect.gen(function* () {
|
||||
class NeedsInstance extends Context.Service<
|
||||
NeedsInstance,
|
||||
{ readonly directory: () => Effect.Effect<string | undefined> }
|
||||
>()("@test/NeedsInstance") {}
|
||||
|
||||
const runtime = makeRuntime(
|
||||
NeedsInstance,
|
||||
Layer.succeed(
|
||||
NeedsInstance,
|
||||
NeedsInstance.of({
|
||||
directory: () =>
|
||||
Effect.gen(function* () {
|
||||
return (yield* InstanceRef)?.directory
|
||||
}),
|
||||
}),
|
||||
),
|
||||
)
|
||||
|
||||
const actual = yield* Effect.promise(() => runtime.runPromise((svc) => svc.directory()))
|
||||
|
||||
expect(actual).toBe(testDirectory)
|
||||
}).pipe(
|
||||
Effect.provideService(InstanceRef, {
|
||||
directory: testDirectory,
|
||||
worktree: testDirectory,
|
||||
project: {
|
||||
id: ProjectID.global,
|
||||
worktree: testDirectory,
|
||||
time: { created: 0, updated: 0 },
|
||||
sandboxes: [],
|
||||
},
|
||||
}),
|
||||
),
|
||||
)
|
||||
|
||||
@@ -6,10 +6,10 @@ import fs from "fs/promises"
|
||||
import { File } from "../../src/file"
|
||||
import { Instance } from "../../src/project/instance"
|
||||
import { Filesystem } from "@/util/filesystem"
|
||||
import { disposeAllInstances, provideInstance, tmpdir } from "../fixture/fixture"
|
||||
import { provideInstance, tmpdir } from "../fixture/fixture"
|
||||
|
||||
afterEach(async () => {
|
||||
await disposeAllInstances()
|
||||
await Instance.disposeAll()
|
||||
})
|
||||
|
||||
const init = () => run(File.Service.use((svc) => svc.init()))
|
||||
@@ -936,7 +936,7 @@ describe("file/index Filesystem patterns", () => {
|
||||
},
|
||||
})
|
||||
|
||||
await disposeAllInstances()
|
||||
await Instance.disposeAll()
|
||||
|
||||
await fs.writeFile(path.join(tmp.path, "after.ts"), "after", "utf-8")
|
||||
await fs.rm(path.join(tmp.path, "before.ts"))
|
||||
|
||||
@@ -5,7 +5,6 @@ import fs from "fs/promises"
|
||||
import { Filesystem } from "@/util/filesystem"
|
||||
import { File } from "../../src/file"
|
||||
import { Instance } from "../../src/project/instance"
|
||||
import { containsPath } from "../../src/project/instance-context"
|
||||
import { provideInstance, tmpdir } from "../fixture/fixture"
|
||||
|
||||
const run = <A, E>(eff: Effect.Effect<A, E, File.Service>) =>
|
||||
@@ -122,15 +121,15 @@ describe("File.list path traversal protection", () => {
|
||||
})
|
||||
})
|
||||
|
||||
describe("containsPath", () => {
|
||||
describe("Instance.containsPath", () => {
|
||||
test("returns true for path inside directory", async () => {
|
||||
await using tmp = await tmpdir({ git: true })
|
||||
|
||||
await Instance.provide({
|
||||
directory: tmp.path,
|
||||
fn: () => {
|
||||
expect(containsPath(path.join(tmp.path, "foo.txt"), Instance.current)).toBe(true)
|
||||
expect(containsPath(path.join(tmp.path, "src", "file.ts"), Instance.current)).toBe(true)
|
||||
expect(Instance.containsPath(path.join(tmp.path, "foo.txt"))).toBe(true)
|
||||
expect(Instance.containsPath(path.join(tmp.path, "src", "file.ts"))).toBe(true)
|
||||
},
|
||||
})
|
||||
})
|
||||
@@ -144,11 +143,11 @@ describe("containsPath", () => {
|
||||
directory: subdir,
|
||||
fn: () => {
|
||||
// .opencode at worktree root, but we're running from packages/lib
|
||||
expect(containsPath(path.join(tmp.path, ".opencode", "state"), Instance.current)).toBe(true)
|
||||
expect(Instance.containsPath(path.join(tmp.path, ".opencode", "state"))).toBe(true)
|
||||
// sibling package should also be accessible
|
||||
expect(containsPath(path.join(tmp.path, "packages", "other", "file.ts"), Instance.current)).toBe(true)
|
||||
expect(Instance.containsPath(path.join(tmp.path, "packages", "other", "file.ts"))).toBe(true)
|
||||
// worktree root itself
|
||||
expect(containsPath(tmp.path, Instance.current)).toBe(true)
|
||||
expect(Instance.containsPath(tmp.path)).toBe(true)
|
||||
},
|
||||
})
|
||||
})
|
||||
@@ -159,8 +158,8 @@ describe("containsPath", () => {
|
||||
await Instance.provide({
|
||||
directory: tmp.path,
|
||||
fn: () => {
|
||||
expect(containsPath("/etc/passwd", Instance.current)).toBe(false)
|
||||
expect(containsPath("/tmp/other-project", Instance.current)).toBe(false)
|
||||
expect(Instance.containsPath("/etc/passwd")).toBe(false)
|
||||
expect(Instance.containsPath("/tmp/other-project")).toBe(false)
|
||||
},
|
||||
})
|
||||
})
|
||||
@@ -171,7 +170,7 @@ describe("containsPath", () => {
|
||||
await Instance.provide({
|
||||
directory: tmp.path,
|
||||
fn: () => {
|
||||
expect(containsPath(path.join(tmp.path, "..", "escape.txt"), Instance.current)).toBe(false)
|
||||
expect(Instance.containsPath(path.join(tmp.path, "..", "escape.txt"))).toBe(false)
|
||||
},
|
||||
})
|
||||
})
|
||||
@@ -183,8 +182,8 @@ describe("containsPath", () => {
|
||||
directory: tmp.path,
|
||||
fn: () => {
|
||||
expect(Instance.directory).toBe(Instance.worktree)
|
||||
expect(containsPath(path.join(tmp.path, "file.txt"), Instance.current)).toBe(true)
|
||||
expect(containsPath("/etc/passwd", Instance.current)).toBe(false)
|
||||
expect(Instance.containsPath(path.join(tmp.path, "file.txt"))).toBe(true)
|
||||
expect(Instance.containsPath("/etc/passwd")).toBe(false)
|
||||
},
|
||||
})
|
||||
})
|
||||
@@ -196,9 +195,9 @@ describe("containsPath", () => {
|
||||
directory: tmp.path,
|
||||
fn: () => {
|
||||
// worktree is "/" for non-git projects, but containsPath should NOT allow all paths
|
||||
expect(containsPath(path.join(tmp.path, "file.txt"), Instance.current)).toBe(true)
|
||||
expect(containsPath("/etc/passwd", Instance.current)).toBe(false)
|
||||
expect(containsPath("/tmp/other", Instance.current)).toBe(false)
|
||||
expect(Instance.containsPath(path.join(tmp.path, "file.txt"))).toBe(true)
|
||||
expect(Instance.containsPath("/etc/passwd")).toBe(false)
|
||||
expect(Instance.containsPath("/tmp/other")).toBe(false)
|
||||
},
|
||||
})
|
||||
})
|
||||
|
||||
@@ -3,7 +3,7 @@ import { afterEach, describe, expect, test } from "bun:test"
|
||||
import fs from "fs/promises"
|
||||
import path from "path"
|
||||
import { ConfigProvider, Deferred, Effect, Layer, ManagedRuntime, Option } from "effect"
|
||||
import { disposeAllInstances, tmpdir } from "../fixture/fixture"
|
||||
import { tmpdir } from "../fixture/fixture"
|
||||
import { Bus } from "../../src/bus"
|
||||
import { Config } from "@/config/config"
|
||||
import { FileWatcher } from "../../src/file/watcher"
|
||||
@@ -147,7 +147,7 @@ function ready(directory: string) {
|
||||
|
||||
describeWatcher("FileWatcher", () => {
|
||||
afterEach(async () => {
|
||||
await disposeAllInstances()
|
||||
await Instance.disposeAll()
|
||||
})
|
||||
|
||||
test("publishes root create, update, and delete events", async () => {
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import { rm } from "fs/promises"
|
||||
import { Instance } from "../../src/project/instance"
|
||||
import { Database } from "@/storage/db"
|
||||
import { disposeAllInstances } from "./fixture"
|
||||
|
||||
export async function resetDatabase() {
|
||||
await disposeAllInstances().catch(() => undefined)
|
||||
await Instance.disposeAll().catch(() => undefined)
|
||||
Database.close()
|
||||
await rm(Database.Path, { force: true }).catch(() => undefined)
|
||||
await rm(`${Database.Path}-wal`, { force: true }).catch(() => undefined)
|
||||
|
||||
@@ -8,14 +8,9 @@ import type * as Scope from "effect/Scope"
|
||||
import { ChildProcess, ChildProcessSpawner } from "effect/unstable/process"
|
||||
import type { Config } from "@/config/config"
|
||||
import { InstanceRef } from "../../src/effect/instance-ref"
|
||||
import { InstanceStore } from "../../src/project/instance-store"
|
||||
import { Instance } from "../../src/project/instance"
|
||||
import { TestLLMServer } from "../lib/llm-server"
|
||||
|
||||
// Re-export for test ergonomics. The implementation lives next to the runtime
|
||||
// it consumes; see `InstanceStore.disposeAllInstances` for the rationale.
|
||||
export { disposeAllInstances } from "../../src/project/instance-store"
|
||||
|
||||
// Strip null bytes from paths (defensive fix for CI environment issues)
|
||||
function sanitizePath(p: string): string {
|
||||
return p.replace(/\0/g, "")
|
||||
@@ -150,7 +145,7 @@ export function provideTmpdirInstance<A, E, R>(
|
||||
? Effect.promise(() =>
|
||||
Instance.provide({
|
||||
directory: path,
|
||||
fn: () => InstanceStore.disposeInstance(Instance.current),
|
||||
fn: () => Instance.dispose(),
|
||||
}),
|
||||
).pipe(Effect.ignore)
|
||||
: Effect.void,
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import { test, expect, mock, beforeEach } from "bun:test"
|
||||
import { InstanceStore } from "../../src/project/instance-store"
|
||||
import { Effect } from "effect"
|
||||
import type { MCP as MCPNS } from "../../src/mcp/index"
|
||||
|
||||
@@ -198,7 +197,7 @@ function withInstance(
|
||||
fn: async () => {
|
||||
await Effect.runPromise(MCP.Service.use(fn).pipe(Effect.provide(MCP.defaultLayer)))
|
||||
// dispose instance to clean up state between tests
|
||||
await InstanceStore.disposeInstance(Instance.current)
|
||||
await Instance.dispose()
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
@@ -2,13 +2,13 @@ import { afterEach, describe, test, expect } from "bun:test"
|
||||
import { Permission } from "../src/permission"
|
||||
import { Config } from "@/config/config"
|
||||
import { Instance } from "../src/project/instance"
|
||||
import { disposeAllInstances, tmpdir } from "./fixture/fixture"
|
||||
import { tmpdir } from "./fixture/fixture"
|
||||
import { AppRuntime } from "../src/effect/app-runtime"
|
||||
|
||||
const load = () => AppRuntime.runPromise(Config.Service.use((svc) => svc.get()))
|
||||
|
||||
afterEach(async () => {
|
||||
await disposeAllInstances()
|
||||
await Instance.disposeAll()
|
||||
})
|
||||
|
||||
describe("Permission.evaluate for permission.task", () => {
|
||||
|
||||
@@ -6,8 +6,7 @@ import { CrossSpawnSpawner } from "@opencode-ai/core/cross-spawn-spawner"
|
||||
import { Permission } from "../../src/permission"
|
||||
import { PermissionID } from "../../src/permission/schema"
|
||||
import { Instance } from "../../src/project/instance"
|
||||
import { InstanceStore } from "../../src/project/instance-store"
|
||||
import { disposeAllInstances, provideInstance, provideTmpdirInstance, tmpdirScoped } from "../fixture/fixture"
|
||||
import { provideInstance, provideTmpdirInstance, tmpdirScoped } from "../fixture/fixture"
|
||||
import { testEffect } from "../lib/effect"
|
||||
import { MessageID, SessionID } from "../../src/session/schema"
|
||||
|
||||
@@ -16,7 +15,7 @@ const env = Layer.mergeAll(Permission.layer.pipe(Layer.provide(bus)), bus, Cross
|
||||
const it = testEffect(env)
|
||||
|
||||
afterEach(async () => {
|
||||
await disposeAllInstances()
|
||||
await Instance.disposeAll()
|
||||
})
|
||||
|
||||
const rejectAll = (message?: string) =>
|
||||
@@ -999,9 +998,7 @@ it.live("pending permission rejects on instance dispose", () =>
|
||||
}).pipe(run, Effect.forkScoped)
|
||||
|
||||
expect(yield* waitForPending(1).pipe(run)).toHaveLength(1)
|
||||
yield* Effect.promise(() =>
|
||||
Instance.provide({ directory: dir, fn: () => void InstanceStore.disposeInstance(Instance.current) }),
|
||||
)
|
||||
yield* Effect.promise(() => Instance.provide({ directory: dir, fn: () => void Instance.dispose() }))
|
||||
|
||||
const exit = yield* Fiber.await(fiber)
|
||||
expect(Exit.isFailure(exit)).toBe(true)
|
||||
@@ -1024,7 +1021,7 @@ it.live("pending permission rejects on instance reload", () =>
|
||||
}).pipe(run, Effect.forkScoped)
|
||||
|
||||
expect(yield* waitForPending(1).pipe(run)).toHaveLength(1)
|
||||
yield* Effect.promise(() => InstanceStore.reloadInstance({ directory: dir }))
|
||||
yield* Effect.promise(() => Instance.reload({ directory: dir }))
|
||||
|
||||
const exit = yield* Fiber.await(fiber)
|
||||
expect(Exit.isFailure(exit)).toBe(true)
|
||||
@@ -1118,7 +1115,7 @@ it.live("ask - abort should clear pending request", () =>
|
||||
|
||||
const pending = yield* waitForPending(1).pipe(run)
|
||||
expect(pending).toHaveLength(1)
|
||||
yield* Effect.promise(() => InstanceStore.reloadInstance({ directory: dir }))
|
||||
yield* Effect.promise(() => Instance.reload({ directory: dir }))
|
||||
|
||||
const exit = yield* Fiber.await(fiber)
|
||||
expect(Exit.isFailure(exit)).toBe(true)
|
||||
|
||||
@@ -3,7 +3,7 @@ import { Effect } from "effect"
|
||||
import fs from "fs/promises"
|
||||
import path from "path"
|
||||
import { pathToFileURL } from "url"
|
||||
import { disposeAllInstances, tmpdir } from "../fixture/fixture"
|
||||
import { tmpdir } from "../fixture/fixture"
|
||||
import { Filesystem } from "@/util/filesystem"
|
||||
|
||||
const disableDefault = process.env.OPENCODE_DISABLE_DEFAULT_PLUGINS
|
||||
@@ -24,7 +24,7 @@ afterAll(() => {
|
||||
})
|
||||
|
||||
afterEach(async () => {
|
||||
await disposeAllInstances()
|
||||
await Instance.disposeAll()
|
||||
})
|
||||
|
||||
async function load(dir: string) {
|
||||
|
||||
@@ -3,7 +3,7 @@ import { Effect, Layer } from "effect"
|
||||
import { CrossSpawnSpawner } from "@opencode-ai/core/cross-spawn-spawner"
|
||||
import path from "path"
|
||||
import { pathToFileURL } from "url"
|
||||
import { disposeAllInstances, provideTmpdirInstance } from "../fixture/fixture"
|
||||
import { provideTmpdirInstance } from "../fixture/fixture"
|
||||
import { testEffect } from "../lib/effect"
|
||||
|
||||
const disableDefault = process.env.OPENCODE_DISABLE_DEFAULT_PLUGINS
|
||||
@@ -20,7 +20,7 @@ const experimental = Flag.OPENCODE_EXPERIMENTAL_WORKSPACES
|
||||
Flag.OPENCODE_EXPERIMENTAL_WORKSPACES = true
|
||||
|
||||
afterEach(async () => {
|
||||
await disposeAllInstances()
|
||||
await Instance.disposeAll()
|
||||
})
|
||||
|
||||
afterAll(() => {
|
||||
|
||||
@@ -1,17 +1,16 @@
|
||||
import { afterEach, describe, expect } from "bun:test"
|
||||
import { CrossSpawnSpawner } from "@opencode-ai/core/cross-spawn-spawner"
|
||||
import { Effect, Fiber, Layer } from "effect"
|
||||
import { InstanceRef } from "../../src/effect/instance-ref"
|
||||
import { registerDisposer } from "../../src/effect/instance-registry"
|
||||
import { Instance } from "../../src/project/instance"
|
||||
import { InstanceStore } from "../../src/project/instance-store"
|
||||
import { disposeAllInstances, tmpdirScoped } from "../fixture/fixture"
|
||||
import { tmpdirScoped } from "../fixture/fixture"
|
||||
import { testEffect } from "../lib/effect"
|
||||
|
||||
const it = testEffect(Layer.mergeAll(InstanceStore.defaultLayer, CrossSpawnSpawner.defaultLayer))
|
||||
|
||||
afterEach(async () => {
|
||||
await disposeAllInstances()
|
||||
await Instance.disposeAll()
|
||||
})
|
||||
|
||||
describe("InstanceStore", () => {
|
||||
@@ -27,7 +26,7 @@ describe("InstanceStore", () => {
|
||||
}),
|
||||
)
|
||||
|
||||
it.live("runs load init with InstanceRef provided", () =>
|
||||
it.live("runs load init inside the loaded legacy instance context", () =>
|
||||
Effect.gen(function* () {
|
||||
const dir = yield* tmpdirScoped({ git: true })
|
||||
const store = yield* InstanceStore.Service
|
||||
@@ -35,9 +34,9 @@ describe("InstanceStore", () => {
|
||||
|
||||
yield* store.load({
|
||||
directory: dir,
|
||||
init: Effect.gen(function* () {
|
||||
initializedDirectory = (yield* InstanceRef)?.directory
|
||||
}),
|
||||
init: async () => {
|
||||
initializedDirectory = Instance.directory
|
||||
},
|
||||
})
|
||||
|
||||
expect(initializedDirectory).toBe(dir)
|
||||
@@ -53,15 +52,15 @@ describe("InstanceStore", () => {
|
||||
|
||||
const first = yield* store.load({
|
||||
directory: dir,
|
||||
init: Effect.sync(() => {
|
||||
init: async () => {
|
||||
initialized++
|
||||
}),
|
||||
},
|
||||
})
|
||||
const second = yield* store.load({
|
||||
directory: dir,
|
||||
init: Effect.sync(() => {
|
||||
init: async () => {
|
||||
initialized++
|
||||
}),
|
||||
},
|
||||
})
|
||||
|
||||
expect(second).toBe(first)
|
||||
@@ -80,11 +79,11 @@ describe("InstanceStore", () => {
|
||||
const first = yield* store
|
||||
.load({
|
||||
directory: dir,
|
||||
init: Effect.promise(async () => {
|
||||
init: async () => {
|
||||
initialized++
|
||||
started.resolve()
|
||||
await release.promise
|
||||
}),
|
||||
},
|
||||
})
|
||||
.pipe(Effect.forkScoped)
|
||||
|
||||
@@ -93,9 +92,9 @@ describe("InstanceStore", () => {
|
||||
const second = yield* store
|
||||
.load({
|
||||
directory: dir,
|
||||
init: Effect.sync(() => {
|
||||
init: async () => {
|
||||
initialized++
|
||||
}),
|
||||
},
|
||||
})
|
||||
.pipe(Effect.forkScoped)
|
||||
|
||||
@@ -117,10 +116,10 @@ describe("InstanceStore", () => {
|
||||
const failed = yield* store
|
||||
.load({
|
||||
directory: dir,
|
||||
init: Effect.sync(() => {
|
||||
init: async () => {
|
||||
attempts++
|
||||
throw new Error("init failed")
|
||||
}),
|
||||
},
|
||||
})
|
||||
.pipe(
|
||||
Effect.as(false),
|
||||
@@ -131,9 +130,9 @@ describe("InstanceStore", () => {
|
||||
|
||||
const ctx = yield* store.load({
|
||||
directory: dir,
|
||||
init: Effect.sync(() => {
|
||||
init: async () => {
|
||||
attempts++
|
||||
}),
|
||||
},
|
||||
})
|
||||
|
||||
expect(ctx.directory).toBe(dir)
|
||||
@@ -171,10 +170,10 @@ describe("InstanceStore", () => {
|
||||
const reload = yield* store
|
||||
.reload({
|
||||
directory: dir,
|
||||
init: Effect.promise(async () => {
|
||||
init: async () => {
|
||||
reloading.resolve()
|
||||
await releaseReload.promise
|
||||
}),
|
||||
},
|
||||
})
|
||||
.pipe(Effect.forkScoped)
|
||||
|
||||
@@ -252,22 +251,4 @@ describe("InstanceStore", () => {
|
||||
expect(() => Instance.current).toThrow()
|
||||
}),
|
||||
)
|
||||
|
||||
it.live("does not install legacy ALS around Effect init", () =>
|
||||
Effect.gen(function* () {
|
||||
const dir = yield* tmpdirScoped()
|
||||
|
||||
const directory = yield* Effect.promise(() =>
|
||||
Instance.provide({
|
||||
directory: dir,
|
||||
init: Effect.sync(() => {
|
||||
expect(() => Instance.current).toThrow()
|
||||
}),
|
||||
fn: () => Instance.directory,
|
||||
}),
|
||||
)
|
||||
|
||||
expect(directory).toBe(dir)
|
||||
}),
|
||||
)
|
||||
})
|
||||
|
||||
@@ -3,7 +3,7 @@ import { afterEach, describe, expect, test } from "bun:test"
|
||||
import { Effect } from "effect"
|
||||
import fs from "fs/promises"
|
||||
import path from "path"
|
||||
import { disposeAllInstances, tmpdir } from "../fixture/fixture"
|
||||
import { tmpdir } from "../fixture/fixture"
|
||||
import { AppRuntime } from "../../src/effect/app-runtime"
|
||||
import { FileWatcher } from "../../src/file/watcher"
|
||||
import { Instance } from "../../src/project/instance"
|
||||
@@ -85,7 +85,7 @@ function nextBranchUpdate(directory: string, timeout = 10_000) {
|
||||
|
||||
describeVcs("Vcs", () => {
|
||||
afterEach(async () => {
|
||||
await disposeAllInstances()
|
||||
await Instance.disposeAll()
|
||||
})
|
||||
|
||||
test("branch() returns current branch name", async () => {
|
||||
@@ -158,7 +158,7 @@ describeVcs("Vcs", () => {
|
||||
|
||||
describe("Vcs diff", () => {
|
||||
afterEach(async () => {
|
||||
await disposeAllInstances()
|
||||
await Instance.disposeAll()
|
||||
})
|
||||
|
||||
test("defaultBranch() falls back to main", async () => {
|
||||
|
||||
@@ -5,9 +5,8 @@ import path from "path"
|
||||
import { Cause, Effect, Exit, Layer } from "effect"
|
||||
import { CrossSpawnSpawner } from "@opencode-ai/core/cross-spawn-spawner"
|
||||
import { Instance } from "../../src/project/instance"
|
||||
import { InstanceStore } from "../../src/project/instance-store"
|
||||
import { Worktree } from "../../src/worktree"
|
||||
import { disposeAllInstances, provideInstance, provideTmpdirInstance } from "../fixture/fixture"
|
||||
import { provideInstance, provideTmpdirInstance } from "../fixture/fixture"
|
||||
import { testEffect } from "../lib/effect"
|
||||
|
||||
const it = testEffect(Layer.mergeAll(Worktree.defaultLayer, CrossSpawnSpawner.defaultLayer))
|
||||
@@ -38,7 +37,7 @@ async function waitReady() {
|
||||
}
|
||||
|
||||
describe("Worktree", () => {
|
||||
afterEach(() => disposeAllInstances())
|
||||
afterEach(() => Instance.disposeAll())
|
||||
|
||||
describe("makeWorktreeInfo", () => {
|
||||
it.live("returns info with name, branch, and directory", () =>
|
||||
@@ -137,11 +136,7 @@ describe("Worktree", () => {
|
||||
expect(props.name).toBe(info.name)
|
||||
expect(props.branch).toBe(info.branch)
|
||||
|
||||
yield* Effect.promise(() =>
|
||||
InstanceStore.runtime.runPromise((s) =>
|
||||
s.load({ directory: info.directory }).pipe(Effect.flatMap(s.dispose)),
|
||||
),
|
||||
)
|
||||
yield* Effect.promise(() => Instance.dispose()).pipe(provideInstance(info.directory))
|
||||
yield* Effect.promise(() => Bun.sleep(100))
|
||||
yield* svc.remove({ directory: info.directory })
|
||||
}),
|
||||
@@ -161,11 +156,7 @@ describe("Worktree", () => {
|
||||
expect(info.branch).toBe("opencode/test-workspace")
|
||||
|
||||
yield* Effect.promise(() => ready)
|
||||
yield* Effect.promise(() =>
|
||||
InstanceStore.runtime.runPromise((s) =>
|
||||
s.load({ directory: info.directory }).pipe(Effect.flatMap(s.dispose)),
|
||||
),
|
||||
)
|
||||
yield* Effect.promise(() => Instance.dispose()).pipe(provideInstance(info.directory))
|
||||
yield* Effect.promise(() => Bun.sleep(100))
|
||||
yield* svc.remove({ directory: info.directory })
|
||||
}),
|
||||
|
||||
@@ -45,10 +45,10 @@ test("Bedrock: config region takes precedence over AWS_REGION env var", async ()
|
||||
})
|
||||
await Instance.provide({
|
||||
directory: tmp.path,
|
||||
init: Effect.promise(async () => {
|
||||
init: async () => {
|
||||
set("AWS_REGION", "us-east-1")
|
||||
set("AWS_PROFILE", "default")
|
||||
}).pipe(Effect.asVoid),
|
||||
},
|
||||
fn: async () => {
|
||||
const providers = await list()
|
||||
expect(providers[ProviderID.amazonBedrock]).toBeDefined()
|
||||
@@ -70,10 +70,10 @@ test("Bedrock: falls back to AWS_REGION env var when no config region", async ()
|
||||
})
|
||||
await Instance.provide({
|
||||
directory: tmp.path,
|
||||
init: Effect.promise(async () => {
|
||||
init: async () => {
|
||||
set("AWS_REGION", "eu-west-1")
|
||||
set("AWS_PROFILE", "default")
|
||||
}).pipe(Effect.asVoid),
|
||||
},
|
||||
fn: async () => {
|
||||
const providers = await list()
|
||||
expect(providers[ProviderID.amazonBedrock]).toBeDefined()
|
||||
@@ -125,11 +125,11 @@ test("Bedrock: loads when bearer token from auth.json is present", async () => {
|
||||
|
||||
await Instance.provide({
|
||||
directory: tmp.path,
|
||||
init: Effect.promise(async () => {
|
||||
init: async () => {
|
||||
set("AWS_PROFILE", "")
|
||||
set("AWS_ACCESS_KEY_ID", "")
|
||||
set("AWS_BEARER_TOKEN_BEDROCK", "")
|
||||
}).pipe(Effect.asVoid),
|
||||
},
|
||||
fn: async () => {
|
||||
const providers = await list()
|
||||
expect(providers[ProviderID.amazonBedrock]).toBeDefined()
|
||||
@@ -171,10 +171,10 @@ test("Bedrock: config profile takes precedence over AWS_PROFILE env var", async
|
||||
})
|
||||
await Instance.provide({
|
||||
directory: tmp.path,
|
||||
init: Effect.promise(async () => {
|
||||
init: async () => {
|
||||
set("AWS_PROFILE", "default")
|
||||
set("AWS_ACCESS_KEY_ID", "test-key-id")
|
||||
}).pipe(Effect.asVoid),
|
||||
},
|
||||
fn: async () => {
|
||||
const providers = await list()
|
||||
expect(providers[ProviderID.amazonBedrock]).toBeDefined()
|
||||
@@ -203,9 +203,9 @@ test("Bedrock: includes custom endpoint in options when specified", async () =>
|
||||
})
|
||||
await Instance.provide({
|
||||
directory: tmp.path,
|
||||
init: Effect.promise(async () => {
|
||||
init: async () => {
|
||||
set("AWS_PROFILE", "default")
|
||||
}).pipe(Effect.asVoid),
|
||||
},
|
||||
fn: async () => {
|
||||
const providers = await list()
|
||||
expect(providers[ProviderID.amazonBedrock]).toBeDefined()
|
||||
@@ -236,12 +236,12 @@ test("Bedrock: autoloads when AWS_WEB_IDENTITY_TOKEN_FILE is present", async ()
|
||||
})
|
||||
await Instance.provide({
|
||||
directory: tmp.path,
|
||||
init: Effect.promise(async () => {
|
||||
init: async () => {
|
||||
set("AWS_WEB_IDENTITY_TOKEN_FILE", "/var/run/secrets/eks.amazonaws.com/serviceaccount/token")
|
||||
set("AWS_ROLE_ARN", "arn:aws:iam::123456789012:role/my-eks-role")
|
||||
set("AWS_PROFILE", "")
|
||||
set("AWS_ACCESS_KEY_ID", "")
|
||||
}).pipe(Effect.asVoid),
|
||||
},
|
||||
fn: async () => {
|
||||
const providers = await list()
|
||||
expect(providers[ProviderID.amazonBedrock]).toBeDefined()
|
||||
@@ -279,9 +279,9 @@ test("Bedrock: model with us. prefix should not be double-prefixed", async () =>
|
||||
})
|
||||
await Instance.provide({
|
||||
directory: tmp.path,
|
||||
init: Effect.promise(async () => {
|
||||
init: async () => {
|
||||
set("AWS_PROFILE", "default")
|
||||
}).pipe(Effect.asVoid),
|
||||
},
|
||||
fn: async () => {
|
||||
const providers = await list()
|
||||
expect(providers[ProviderID.amazonBedrock]).toBeDefined()
|
||||
@@ -316,9 +316,9 @@ test("Bedrock: model with global. prefix should not be prefixed", async () => {
|
||||
})
|
||||
await Instance.provide({
|
||||
directory: tmp.path,
|
||||
init: Effect.promise(async () => {
|
||||
init: async () => {
|
||||
set("AWS_PROFILE", "default")
|
||||
}).pipe(Effect.asVoid),
|
||||
},
|
||||
fn: async () => {
|
||||
const providers = await list()
|
||||
expect(providers[ProviderID.amazonBedrock]).toBeDefined()
|
||||
@@ -352,9 +352,9 @@ test("Bedrock: model with eu. prefix should not be double-prefixed", async () =>
|
||||
})
|
||||
await Instance.provide({
|
||||
directory: tmp.path,
|
||||
init: Effect.promise(async () => {
|
||||
init: async () => {
|
||||
set("AWS_PROFILE", "default")
|
||||
}).pipe(Effect.asVoid),
|
||||
},
|
||||
fn: async () => {
|
||||
const providers = await list()
|
||||
expect(providers[ProviderID.amazonBedrock]).toBeDefined()
|
||||
@@ -388,9 +388,9 @@ test("Bedrock: model without prefix in US region should get us. prefix added", a
|
||||
})
|
||||
await Instance.provide({
|
||||
directory: tmp.path,
|
||||
init: Effect.promise(async () => {
|
||||
init: async () => {
|
||||
set("AWS_PROFILE", "default")
|
||||
}).pipe(Effect.asVoid),
|
||||
},
|
||||
fn: async () => {
|
||||
const providers = await list()
|
||||
expect(providers[ProviderID.amazonBedrock]).toBeDefined()
|
||||
|
||||
@@ -1,259 +0,0 @@
|
||||
import { describe, expect, beforeAll, beforeEach, afterAll } from "bun:test"
|
||||
import { Effect, Layer, Ref } from "effect"
|
||||
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 "../../src/provider/models"
|
||||
import { it } from "../lib/effect"
|
||||
import { rm, writeFile, utimes, mkdir } from "fs/promises"
|
||||
import path from "path"
|
||||
|
||||
// test/preload.ts pins OPENCODE_MODELS_PATH to a fixture so other tests can
|
||||
// resolve providers without network. These tests need to drive the on-disk
|
||||
// cache themselves and silence the eager refresh fork. Save/restore around
|
||||
// the suite — never leak the mutation to subsequent test files in the same
|
||||
// bun process.
|
||||
const ORIGINAL_MODELS_PATH = Flag.OPENCODE_MODELS_PATH
|
||||
const ORIGINAL_DISABLE_FETCH = Flag.OPENCODE_DISABLE_MODELS_FETCH
|
||||
beforeAll(() => {
|
||||
Flag.OPENCODE_MODELS_PATH = undefined
|
||||
Flag.OPENCODE_DISABLE_MODELS_FETCH = true
|
||||
})
|
||||
afterAll(() => {
|
||||
Flag.OPENCODE_MODELS_PATH = ORIGINAL_MODELS_PATH
|
||||
Flag.OPENCODE_DISABLE_MODELS_FETCH = ORIGINAL_DISABLE_FETCH
|
||||
})
|
||||
|
||||
const cacheFile = path.join(Global.Path.cache, "models.json")
|
||||
|
||||
const fixture: Record<string, ModelsDev.Provider> = {
|
||||
acme: {
|
||||
id: "acme",
|
||||
name: "Acme",
|
||||
env: ["ACME_API_KEY"],
|
||||
models: {
|
||||
"acme-1": {
|
||||
id: "acme-1",
|
||||
name: "Acme One",
|
||||
release_date: "2026-01-01",
|
||||
attachment: false,
|
||||
reasoning: false,
|
||||
temperature: true,
|
||||
tool_call: true,
|
||||
limit: { context: 128000, output: 8192 },
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
const fixture2: Record<string, ModelsDev.Provider> = {
|
||||
beta: {
|
||||
id: "beta",
|
||||
name: "Beta",
|
||||
env: ["BETA_API_KEY"],
|
||||
models: {
|
||||
"beta-1": {
|
||||
id: "beta-1",
|
||||
name: "Beta One",
|
||||
release_date: "2026-02-01",
|
||||
attachment: false,
|
||||
reasoning: true,
|
||||
temperature: false,
|
||||
tool_call: false,
|
||||
limit: { context: 64000, output: 4096 },
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
interface MockState {
|
||||
body: string
|
||||
status: number
|
||||
calls: Array<{ url: string }>
|
||||
}
|
||||
|
||||
const makeMockClient = (state: Ref.Ref<MockState>) =>
|
||||
HttpClient.make((request) =>
|
||||
Effect.gen(function* () {
|
||||
yield* Ref.update(state, (s) => ({ ...s, calls: [...s.calls, { url: request.url }] }))
|
||||
const s = yield* Ref.get(state)
|
||||
return HttpClientResponse.fromWeb(request, new Response(s.body, { status: s.status }))
|
||||
}),
|
||||
)
|
||||
|
||||
const buildLayer = (state: Ref.Ref<MockState>) =>
|
||||
// Layer.fresh is required: ModelsDev.layer is a module-level Layer constant,
|
||||
// and Effect.provide uses a process-global MemoMap by default — without fresh,
|
||||
// every test would reuse the cachedInvalidateWithTTL state from the first run.
|
||||
Layer.fresh(ModelsDev.layer).pipe(
|
||||
Layer.provide(Layer.succeed(HttpClient.HttpClient, makeMockClient(state))),
|
||||
Layer.provide(AppFileSystem.defaultLayer),
|
||||
)
|
||||
|
||||
const writeCache = (data: object, mtimeMs?: number) =>
|
||||
Effect.promise(async () => {
|
||||
await mkdir(Global.Path.cache, { recursive: true })
|
||||
await writeFile(cacheFile, JSON.stringify(data))
|
||||
if (mtimeMs !== undefined) {
|
||||
const t = mtimeMs / 1000
|
||||
await utimes(cacheFile, t, t)
|
||||
}
|
||||
})
|
||||
|
||||
const provided = <A, E>(state: Ref.Ref<MockState>, eff: Effect.Effect<A, E, ModelsDev.Service>) =>
|
||||
eff.pipe(Effect.provide(buildLayer(state)))
|
||||
|
||||
beforeEach(async () => {
|
||||
await rm(cacheFile, { force: true })
|
||||
})
|
||||
|
||||
afterAll(async () => {
|
||||
await rm(cacheFile, { force: true })
|
||||
})
|
||||
|
||||
const initialState: MockState = {
|
||||
body: JSON.stringify(fixture),
|
||||
status: 200,
|
||||
calls: [],
|
||||
}
|
||||
|
||||
describe("ModelsDev Service", () => {
|
||||
it.live("get() returns providers from disk when cache file exists", () =>
|
||||
Effect.gen(function* () {
|
||||
yield* writeCache(fixture)
|
||||
const state = yield* Ref.make(initialState)
|
||||
const result = yield* provided(
|
||||
state,
|
||||
ModelsDev.Service.use((s) => s.get()),
|
||||
)
|
||||
expect(result).toEqual(fixture)
|
||||
const final = yield* Ref.get(state)
|
||||
expect(final.calls).toEqual([])
|
||||
}),
|
||||
)
|
||||
|
||||
it.live("get() returns {} 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({})
|
||||
const final = yield* Ref.get(state)
|
||||
expect(final.calls).toEqual([])
|
||||
}),
|
||||
)
|
||||
|
||||
it.live("get() is single-flight under concurrent calls", () =>
|
||||
Effect.gen(function* () {
|
||||
yield* writeCache(fixture)
|
||||
const state = yield* Ref.make(initialState)
|
||||
const results = yield* provided(
|
||||
state,
|
||||
Effect.gen(function* () {
|
||||
const svc = yield* ModelsDev.Service
|
||||
return yield* Effect.all([svc.get(), svc.get(), svc.get(), svc.get(), svc.get()], {
|
||||
concurrency: "unbounded",
|
||||
})
|
||||
}),
|
||||
)
|
||||
for (const result of results) expect(result).toEqual(fixture)
|
||||
}),
|
||||
)
|
||||
|
||||
it.live("get() caches across calls (later disk writes are ignored until invalidate)", () =>
|
||||
Effect.gen(function* () {
|
||||
yield* writeCache(fixture)
|
||||
const state = yield* Ref.make(initialState)
|
||||
const first = yield* provided(
|
||||
state,
|
||||
Effect.gen(function* () {
|
||||
const svc = yield* ModelsDev.Service
|
||||
const a = yield* svc.get()
|
||||
// mutate disk between calls — cache should mask the change
|
||||
yield* writeCache(fixture2)
|
||||
const b = yield* svc.get()
|
||||
return { a, b }
|
||||
}),
|
||||
)
|
||||
expect(first.a).toEqual(fixture)
|
||||
expect(first.b).toEqual(fixture)
|
||||
}),
|
||||
)
|
||||
|
||||
it.live("refresh(true) fetches via HttpClient and updates the cache", () =>
|
||||
Effect.gen(function* () {
|
||||
yield* writeCache(fixture)
|
||||
const state = yield* Ref.make({ ...initialState, body: JSON.stringify(fixture2) })
|
||||
const result = yield* provided(
|
||||
state,
|
||||
Effect.gen(function* () {
|
||||
const svc = yield* ModelsDev.Service
|
||||
const before = yield* svc.get()
|
||||
yield* svc.refresh(true)
|
||||
const after = yield* svc.get()
|
||||
return { before, after }
|
||||
}),
|
||||
)
|
||||
expect(result.before).toEqual(fixture)
|
||||
expect(result.after).toEqual(fixture2)
|
||||
const final = yield* Ref.get(state)
|
||||
expect(final.calls.length).toBe(1)
|
||||
expect(final.calls[0].url).toContain("/api.json")
|
||||
}),
|
||||
)
|
||||
|
||||
it.live("refresh(false) skips fetch when on-disk file is fresh", () =>
|
||||
Effect.gen(function* () {
|
||||
// Fresh: mtime within the 5-minute TTL.
|
||||
yield* writeCache(fixture, Date.now() - 1000)
|
||||
const state = yield* Ref.make({ ...initialState, body: JSON.stringify(fixture2) })
|
||||
yield* provided(
|
||||
state,
|
||||
ModelsDev.Service.use((s) => s.refresh(false)),
|
||||
)
|
||||
const final = yield* Ref.get(state)
|
||||
expect(final.calls).toEqual([])
|
||||
}),
|
||||
)
|
||||
|
||||
it.live("refresh(false) fetches when on-disk file is stale", () =>
|
||||
Effect.gen(function* () {
|
||||
// Stale: mtime 10 minutes ago, beyond the 5-minute TTL.
|
||||
yield* writeCache(fixture, Date.now() - 10 * 60 * 1000)
|
||||
const state = yield* Ref.make({ ...initialState, body: JSON.stringify(fixture2) })
|
||||
const after = yield* provided(
|
||||
state,
|
||||
Effect.gen(function* () {
|
||||
const svc = yield* ModelsDev.Service
|
||||
yield* svc.refresh(false)
|
||||
return yield* svc.get()
|
||||
}),
|
||||
)
|
||||
const final = yield* Ref.get(state)
|
||||
expect(final.calls.length).toBe(1)
|
||||
expect(after).toEqual(fixture2)
|
||||
}),
|
||||
)
|
||||
|
||||
it.live("refresh swallows HTTP errors and leaves cache intact", () =>
|
||||
Effect.gen(function* () {
|
||||
yield* writeCache(fixture)
|
||||
const state = yield* Ref.make({ ...initialState, status: 500, body: "boom" })
|
||||
const result = yield* provided(
|
||||
state,
|
||||
Effect.gen(function* () {
|
||||
const svc = yield* ModelsDev.Service
|
||||
yield* svc.refresh(true)
|
||||
return yield* svc.get()
|
||||
}),
|
||||
)
|
||||
expect(result).toEqual(fixture)
|
||||
// withTransientReadRetry retries 5xx, so calls may be > 1.
|
||||
const final = yield* Ref.get(state)
|
||||
expect(final.calls.length).toBeGreaterThanOrEqual(1)
|
||||
}),
|
||||
)
|
||||
})
|
||||
@@ -2,7 +2,7 @@ import { test, expect } from "bun:test"
|
||||
import { mkdir, unlink } from "fs/promises"
|
||||
import path from "path"
|
||||
|
||||
import { disposeAllInstances, tmpdir } from "../fixture/fixture"
|
||||
import { tmpdir } from "../fixture/fixture"
|
||||
import { Global } from "@opencode-ai/core/global"
|
||||
import { Instance } from "../../src/project/instance"
|
||||
import { Plugin } from "../../src/plugin/index"
|
||||
@@ -82,9 +82,9 @@ test("provider loaded from env variable", async () => {
|
||||
})
|
||||
await Instance.provide({
|
||||
directory: tmp.path,
|
||||
init: Effect.promise(async () => {
|
||||
init: async () => {
|
||||
set("ANTHROPIC_API_KEY", "test-api-key")
|
||||
}).pipe(Effect.asVoid),
|
||||
},
|
||||
fn: async () => {
|
||||
const providers = await list()
|
||||
expect(providers[ProviderID.anthropic]).toBeDefined()
|
||||
@@ -137,9 +137,9 @@ test("disabled_providers excludes provider", async () => {
|
||||
})
|
||||
await Instance.provide({
|
||||
directory: tmp.path,
|
||||
init: Effect.promise(async () => {
|
||||
init: async () => {
|
||||
set("ANTHROPIC_API_KEY", "test-api-key")
|
||||
}).pipe(Effect.asVoid),
|
||||
},
|
||||
fn: async () => {
|
||||
const providers = await list()
|
||||
expect(providers[ProviderID.anthropic]).toBeUndefined()
|
||||
@@ -161,10 +161,10 @@ test("enabled_providers restricts to only listed providers", async () => {
|
||||
})
|
||||
await Instance.provide({
|
||||
directory: tmp.path,
|
||||
init: Effect.promise(async () => {
|
||||
init: async () => {
|
||||
set("ANTHROPIC_API_KEY", "test-api-key")
|
||||
set("OPENAI_API_KEY", "test-openai-key")
|
||||
}).pipe(Effect.asVoid),
|
||||
},
|
||||
fn: async () => {
|
||||
const providers = await list()
|
||||
expect(providers[ProviderID.anthropic]).toBeDefined()
|
||||
@@ -191,9 +191,9 @@ test("model whitelist filters models for provider", async () => {
|
||||
})
|
||||
await Instance.provide({
|
||||
directory: tmp.path,
|
||||
init: Effect.promise(async () => {
|
||||
init: async () => {
|
||||
set("ANTHROPIC_API_KEY", "test-api-key")
|
||||
}).pipe(Effect.asVoid),
|
||||
},
|
||||
fn: async () => {
|
||||
const providers = await list()
|
||||
expect(providers[ProviderID.anthropic]).toBeDefined()
|
||||
@@ -222,9 +222,9 @@ test("model blacklist excludes specific models", async () => {
|
||||
})
|
||||
await Instance.provide({
|
||||
directory: tmp.path,
|
||||
init: Effect.promise(async () => {
|
||||
init: async () => {
|
||||
set("ANTHROPIC_API_KEY", "test-api-key")
|
||||
}).pipe(Effect.asVoid),
|
||||
},
|
||||
fn: async () => {
|
||||
const providers = await list()
|
||||
expect(providers[ProviderID.anthropic]).toBeDefined()
|
||||
@@ -257,9 +257,9 @@ test("custom model alias via config", async () => {
|
||||
})
|
||||
await Instance.provide({
|
||||
directory: tmp.path,
|
||||
init: Effect.promise(async () => {
|
||||
init: async () => {
|
||||
set("ANTHROPIC_API_KEY", "test-api-key")
|
||||
}).pipe(Effect.asVoid),
|
||||
},
|
||||
fn: async () => {
|
||||
const providers = await list()
|
||||
expect(providers[ProviderID.anthropic]).toBeDefined()
|
||||
@@ -394,9 +394,9 @@ test("env variable takes precedence, config merges options", async () => {
|
||||
})
|
||||
await Instance.provide({
|
||||
directory: tmp.path,
|
||||
init: Effect.promise(async () => {
|
||||
init: async () => {
|
||||
set("ANTHROPIC_API_KEY", "env-api-key")
|
||||
}).pipe(Effect.asVoid),
|
||||
},
|
||||
fn: async () => {
|
||||
const providers = await list()
|
||||
expect(providers[ProviderID.anthropic]).toBeDefined()
|
||||
@@ -420,9 +420,9 @@ test("getModel returns model for valid provider/model", async () => {
|
||||
})
|
||||
await Instance.provide({
|
||||
directory: tmp.path,
|
||||
init: Effect.promise(async () => {
|
||||
init: async () => {
|
||||
set("ANTHROPIC_API_KEY", "test-api-key")
|
||||
}).pipe(Effect.asVoid),
|
||||
},
|
||||
fn: async () => {
|
||||
const model = await getModel(ProviderID.anthropic, ModelID.make("claude-sonnet-4-20250514"))
|
||||
expect(model).toBeDefined()
|
||||
@@ -447,9 +447,9 @@ test("getModel throws ModelNotFoundError for invalid model", async () => {
|
||||
})
|
||||
await Instance.provide({
|
||||
directory: tmp.path,
|
||||
init: Effect.promise(async () => {
|
||||
init: async () => {
|
||||
set("ANTHROPIC_API_KEY", "test-api-key")
|
||||
}).pipe(Effect.asVoid),
|
||||
},
|
||||
fn: async () => {
|
||||
expect(getModel(ProviderID.anthropic, ModelID.make("nonexistent-model"))).rejects.toThrow()
|
||||
},
|
||||
@@ -500,9 +500,9 @@ test("defaultModel returns first available model when no config set", async () =
|
||||
})
|
||||
await Instance.provide({
|
||||
directory: tmp.path,
|
||||
init: Effect.promise(async () => {
|
||||
init: async () => {
|
||||
set("ANTHROPIC_API_KEY", "test-api-key")
|
||||
}).pipe(Effect.asVoid),
|
||||
},
|
||||
fn: async () => {
|
||||
const model = await defaultModel()
|
||||
expect(model.providerID).toBeDefined()
|
||||
@@ -525,9 +525,9 @@ test("defaultModel respects config model setting", async () => {
|
||||
})
|
||||
await Instance.provide({
|
||||
directory: tmp.path,
|
||||
init: Effect.promise(async () => {
|
||||
init: async () => {
|
||||
set("ANTHROPIC_API_KEY", "test-api-key")
|
||||
}).pipe(Effect.asVoid),
|
||||
},
|
||||
fn: async () => {
|
||||
const model = await defaultModel()
|
||||
expect(String(model.providerID)).toBe("anthropic")
|
||||
@@ -640,9 +640,9 @@ test("model options are merged from existing model", async () => {
|
||||
})
|
||||
await Instance.provide({
|
||||
directory: tmp.path,
|
||||
init: Effect.promise(async () => {
|
||||
init: async () => {
|
||||
set("ANTHROPIC_API_KEY", "test-api-key")
|
||||
}).pipe(Effect.asVoid),
|
||||
},
|
||||
fn: async () => {
|
||||
const providers = await list()
|
||||
const model = providers[ProviderID.anthropic].models["claude-sonnet-4-20250514"]
|
||||
@@ -669,9 +669,9 @@ test("provider removed when all models filtered out", async () => {
|
||||
})
|
||||
await Instance.provide({
|
||||
directory: tmp.path,
|
||||
init: Effect.promise(async () => {
|
||||
init: async () => {
|
||||
set("ANTHROPIC_API_KEY", "test-api-key")
|
||||
}).pipe(Effect.asVoid),
|
||||
},
|
||||
fn: async () => {
|
||||
const providers = await list()
|
||||
expect(providers[ProviderID.anthropic]).toBeUndefined()
|
||||
@@ -692,9 +692,9 @@ test("closest finds model by partial match", async () => {
|
||||
})
|
||||
await Instance.provide({
|
||||
directory: tmp.path,
|
||||
init: Effect.promise(async () => {
|
||||
init: async () => {
|
||||
set("ANTHROPIC_API_KEY", "test-api-key")
|
||||
}).pipe(Effect.asVoid),
|
||||
},
|
||||
fn: async () => {
|
||||
const result = await closest(ProviderID.anthropic, ["sonnet-4"])
|
||||
expect(result).toBeDefined()
|
||||
@@ -747,9 +747,9 @@ test("getModel uses realIdByKey for aliased models", async () => {
|
||||
})
|
||||
await Instance.provide({
|
||||
directory: tmp.path,
|
||||
init: Effect.promise(async () => {
|
||||
init: async () => {
|
||||
set("ANTHROPIC_API_KEY", "test-api-key")
|
||||
}).pipe(Effect.asVoid),
|
||||
},
|
||||
fn: async () => {
|
||||
const providers = await list()
|
||||
expect(providers[ProviderID.anthropic].models["my-sonnet"]).toBeDefined()
|
||||
@@ -862,9 +862,9 @@ test("model inherits properties from existing database model", async () => {
|
||||
})
|
||||
await Instance.provide({
|
||||
directory: tmp.path,
|
||||
init: Effect.promise(async () => {
|
||||
init: async () => {
|
||||
set("ANTHROPIC_API_KEY", "test-api-key")
|
||||
}).pipe(Effect.asVoid),
|
||||
},
|
||||
fn: async () => {
|
||||
const providers = await list()
|
||||
const model = providers[ProviderID.anthropic].models["claude-sonnet-4-20250514"]
|
||||
@@ -890,9 +890,9 @@ test("disabled_providers prevents loading even with env var", async () => {
|
||||
})
|
||||
await Instance.provide({
|
||||
directory: tmp.path,
|
||||
init: Effect.promise(async () => {
|
||||
init: async () => {
|
||||
set("OPENAI_API_KEY", "test-openai-key")
|
||||
}).pipe(Effect.asVoid),
|
||||
},
|
||||
fn: async () => {
|
||||
const providers = await list()
|
||||
expect(providers[ProviderID.openai]).toBeUndefined()
|
||||
@@ -914,10 +914,10 @@ test("enabled_providers with empty array allows no providers", async () => {
|
||||
})
|
||||
await Instance.provide({
|
||||
directory: tmp.path,
|
||||
init: Effect.promise(async () => {
|
||||
init: async () => {
|
||||
set("ANTHROPIC_API_KEY", "test-api-key")
|
||||
set("OPENAI_API_KEY", "test-openai-key")
|
||||
}).pipe(Effect.asVoid),
|
||||
},
|
||||
fn: async () => {
|
||||
const providers = await list()
|
||||
expect(Object.keys(providers).length).toBe(0)
|
||||
@@ -944,9 +944,9 @@ test("whitelist and blacklist can be combined", async () => {
|
||||
})
|
||||
await Instance.provide({
|
||||
directory: tmp.path,
|
||||
init: Effect.promise(async () => {
|
||||
init: async () => {
|
||||
set("ANTHROPIC_API_KEY", "test-api-key")
|
||||
}).pipe(Effect.asVoid),
|
||||
},
|
||||
fn: async () => {
|
||||
const providers = await list()
|
||||
expect(providers[ProviderID.anthropic]).toBeDefined()
|
||||
@@ -1053,9 +1053,9 @@ test("getSmallModel returns appropriate small model", async () => {
|
||||
})
|
||||
await Instance.provide({
|
||||
directory: tmp.path,
|
||||
init: Effect.promise(async () => {
|
||||
init: async () => {
|
||||
set("ANTHROPIC_API_KEY", "test-api-key")
|
||||
}).pipe(Effect.asVoid),
|
||||
},
|
||||
fn: async () => {
|
||||
const model = await getSmallModel(ProviderID.anthropic)
|
||||
expect(model).toBeDefined()
|
||||
@@ -1078,9 +1078,9 @@ test("getSmallModel respects config small_model override", async () => {
|
||||
})
|
||||
await Instance.provide({
|
||||
directory: tmp.path,
|
||||
init: Effect.promise(async () => {
|
||||
init: async () => {
|
||||
set("ANTHROPIC_API_KEY", "test-api-key")
|
||||
}).pipe(Effect.asVoid),
|
||||
},
|
||||
fn: async () => {
|
||||
const model = await getSmallModel(ProviderID.anthropic)
|
||||
expect(model).toBeDefined()
|
||||
@@ -1126,10 +1126,10 @@ test("multiple providers can be configured simultaneously", async () => {
|
||||
})
|
||||
await Instance.provide({
|
||||
directory: tmp.path,
|
||||
init: Effect.promise(async () => {
|
||||
init: async () => {
|
||||
set("ANTHROPIC_API_KEY", "test-anthropic-key")
|
||||
set("OPENAI_API_KEY", "test-openai-key")
|
||||
}).pipe(Effect.asVoid),
|
||||
},
|
||||
fn: async () => {
|
||||
const providers = await list()
|
||||
expect(providers[ProviderID.anthropic]).toBeDefined()
|
||||
@@ -1205,9 +1205,9 @@ test("model alias name defaults to alias key when id differs", async () => {
|
||||
})
|
||||
await Instance.provide({
|
||||
directory: tmp.path,
|
||||
init: Effect.promise(async () => {
|
||||
init: async () => {
|
||||
set("ANTHROPIC_API_KEY", "test-api-key")
|
||||
}).pipe(Effect.asVoid),
|
||||
},
|
||||
fn: async () => {
|
||||
const providers = await list()
|
||||
expect(providers[ProviderID.anthropic].models["sonnet"].name).toBe("sonnet")
|
||||
@@ -1245,9 +1245,9 @@ test("provider with multiple env var options only includes apiKey when single en
|
||||
})
|
||||
await Instance.provide({
|
||||
directory: tmp.path,
|
||||
init: Effect.promise(async () => {
|
||||
init: async () => {
|
||||
set("MULTI_ENV_KEY_1", "test-key")
|
||||
}).pipe(Effect.asVoid),
|
||||
},
|
||||
fn: async () => {
|
||||
const providers = await list()
|
||||
expect(providers[ProviderID.make("multi-env")]).toBeDefined()
|
||||
@@ -1287,9 +1287,9 @@ test("provider with single env var includes apiKey automatically", async () => {
|
||||
})
|
||||
await Instance.provide({
|
||||
directory: tmp.path,
|
||||
init: Effect.promise(async () => {
|
||||
init: async () => {
|
||||
set("SINGLE_ENV_KEY", "my-api-key")
|
||||
}).pipe(Effect.asVoid),
|
||||
},
|
||||
fn: async () => {
|
||||
const providers = await list()
|
||||
expect(providers[ProviderID.make("single-env")]).toBeDefined()
|
||||
@@ -1324,9 +1324,9 @@ test("model cost overrides existing cost values", async () => {
|
||||
})
|
||||
await Instance.provide({
|
||||
directory: tmp.path,
|
||||
init: Effect.promise(async () => {
|
||||
init: async () => {
|
||||
set("ANTHROPIC_API_KEY", "test-api-key")
|
||||
}).pipe(Effect.asVoid),
|
||||
},
|
||||
fn: async () => {
|
||||
const providers = await list()
|
||||
const model = providers[ProviderID.anthropic].models["claude-sonnet-4-20250514"]
|
||||
@@ -1403,11 +1403,11 @@ test("disabled_providers and enabled_providers interaction", async () => {
|
||||
})
|
||||
await Instance.provide({
|
||||
directory: tmp.path,
|
||||
init: Effect.promise(async () => {
|
||||
init: async () => {
|
||||
set("ANTHROPIC_API_KEY", "test-anthropic")
|
||||
set("OPENAI_API_KEY", "test-openai")
|
||||
set("GOOGLE_GENERATIVE_AI_API_KEY", "test-google")
|
||||
}).pipe(Effect.asVoid),
|
||||
},
|
||||
fn: async () => {
|
||||
const providers = await list()
|
||||
// anthropic: in enabled, not in disabled = allowed
|
||||
@@ -1561,10 +1561,10 @@ test("provider env fallback - second env var used if first missing", async () =>
|
||||
})
|
||||
await Instance.provide({
|
||||
directory: tmp.path,
|
||||
init: Effect.promise(async () => {
|
||||
init: async () => {
|
||||
// Only set fallback, not primary
|
||||
set("FALLBACK_KEY", "fallback-api-key")
|
||||
}).pipe(Effect.asVoid),
|
||||
},
|
||||
fn: async () => {
|
||||
const providers = await list()
|
||||
// Provider should load because fallback env var is set
|
||||
@@ -1586,9 +1586,9 @@ test("getModel returns consistent results", async () => {
|
||||
})
|
||||
await Instance.provide({
|
||||
directory: tmp.path,
|
||||
init: Effect.promise(async () => {
|
||||
init: async () => {
|
||||
set("ANTHROPIC_API_KEY", "test-api-key")
|
||||
}).pipe(Effect.asVoid),
|
||||
},
|
||||
fn: async () => {
|
||||
const model1 = await getModel(ProviderID.anthropic, ModelID.make("claude-sonnet-4-20250514"))
|
||||
const model2 = await getModel(ProviderID.anthropic, ModelID.make("claude-sonnet-4-20250514"))
|
||||
@@ -1647,9 +1647,9 @@ test("ModelNotFoundError includes suggestions for typos", async () => {
|
||||
})
|
||||
await Instance.provide({
|
||||
directory: tmp.path,
|
||||
init: Effect.promise(async () => {
|
||||
init: async () => {
|
||||
set("ANTHROPIC_API_KEY", "test-api-key")
|
||||
}).pipe(Effect.asVoid),
|
||||
},
|
||||
fn: async () => {
|
||||
try {
|
||||
await getModel(ProviderID.anthropic, ModelID.make("claude-sonet-4")) // typo: sonet instead of sonnet
|
||||
@@ -1675,9 +1675,9 @@ test("ModelNotFoundError for provider includes suggestions", async () => {
|
||||
})
|
||||
await Instance.provide({
|
||||
directory: tmp.path,
|
||||
init: Effect.promise(async () => {
|
||||
init: async () => {
|
||||
set("ANTHROPIC_API_KEY", "test-api-key")
|
||||
}).pipe(Effect.asVoid),
|
||||
},
|
||||
fn: async () => {
|
||||
try {
|
||||
await getModel(ProviderID.make("antropic"), ModelID.make("claude-sonnet-4")) // typo: antropic
|
||||
@@ -1723,9 +1723,9 @@ test("getProvider returns provider info", async () => {
|
||||
})
|
||||
await Instance.provide({
|
||||
directory: tmp.path,
|
||||
init: Effect.promise(async () => {
|
||||
init: async () => {
|
||||
set("ANTHROPIC_API_KEY", "test-api-key")
|
||||
}).pipe(Effect.asVoid),
|
||||
},
|
||||
fn: async () => {
|
||||
const provider = await getProvider(ProviderID.anthropic)
|
||||
expect(provider).toBeDefined()
|
||||
@@ -1747,9 +1747,9 @@ test("closest returns undefined when no partial match found", async () => {
|
||||
})
|
||||
await Instance.provide({
|
||||
directory: tmp.path,
|
||||
init: Effect.promise(async () => {
|
||||
init: async () => {
|
||||
set("ANTHROPIC_API_KEY", "test-api-key")
|
||||
}).pipe(Effect.asVoid),
|
||||
},
|
||||
fn: async () => {
|
||||
const result = await closest(ProviderID.anthropic, ["nonexistent-xyz-model"])
|
||||
expect(result).toBeUndefined()
|
||||
@@ -1770,9 +1770,9 @@ test("closest checks multiple query terms in order", async () => {
|
||||
})
|
||||
await Instance.provide({
|
||||
directory: tmp.path,
|
||||
init: Effect.promise(async () => {
|
||||
init: async () => {
|
||||
set("ANTHROPIC_API_KEY", "test-api-key")
|
||||
}).pipe(Effect.asVoid),
|
||||
},
|
||||
fn: async () => {
|
||||
// First term won't match, second will
|
||||
const result = await closest(ProviderID.anthropic, ["nonexistent", "haiku"])
|
||||
@@ -1842,9 +1842,9 @@ test("provider options are deeply merged", async () => {
|
||||
})
|
||||
await Instance.provide({
|
||||
directory: tmp.path,
|
||||
init: Effect.promise(async () => {
|
||||
init: async () => {
|
||||
set("ANTHROPIC_API_KEY", "test-api-key")
|
||||
}).pipe(Effect.asVoid),
|
||||
},
|
||||
fn: async () => {
|
||||
const providers = await list()
|
||||
// Custom options should be merged
|
||||
@@ -1880,9 +1880,9 @@ test("custom model inherits npm package from models.dev provider config", async
|
||||
})
|
||||
await Instance.provide({
|
||||
directory: tmp.path,
|
||||
init: Effect.promise(async () => {
|
||||
init: async () => {
|
||||
set("OPENAI_API_KEY", "test-api-key")
|
||||
}).pipe(Effect.asVoid),
|
||||
},
|
||||
fn: async () => {
|
||||
const providers = await list()
|
||||
const model = providers[ProviderID.openai].models["my-custom-model"]
|
||||
@@ -1915,9 +1915,9 @@ test("custom model inherits api.url from models.dev provider", async () => {
|
||||
})
|
||||
await Instance.provide({
|
||||
directory: tmp.path,
|
||||
init: Effect.promise(async () => {
|
||||
init: async () => {
|
||||
set("OPENROUTER_API_KEY", "test-api-key")
|
||||
}).pipe(Effect.asVoid),
|
||||
},
|
||||
fn: async () => {
|
||||
const providers = await list()
|
||||
expect(providers[ProviderID.openrouter]).toBeDefined()
|
||||
@@ -2048,9 +2048,9 @@ test("model variants are generated for reasoning models", async () => {
|
||||
})
|
||||
await Instance.provide({
|
||||
directory: tmp.path,
|
||||
init: Effect.promise(async () => {
|
||||
init: async () => {
|
||||
set("ANTHROPIC_API_KEY", "test-api-key")
|
||||
}).pipe(Effect.asVoid),
|
||||
},
|
||||
fn: async () => {
|
||||
const providers = await list()
|
||||
// Claude sonnet 4 has reasoning capability
|
||||
@@ -2086,9 +2086,9 @@ test("model variants can be disabled via config", async () => {
|
||||
})
|
||||
await Instance.provide({
|
||||
directory: tmp.path,
|
||||
init: Effect.promise(async () => {
|
||||
init: async () => {
|
||||
set("ANTHROPIC_API_KEY", "test-api-key")
|
||||
}).pipe(Effect.asVoid),
|
||||
},
|
||||
fn: async () => {
|
||||
const providers = await list()
|
||||
const model = providers[ProviderID.anthropic].models["claude-sonnet-4-20250514"]
|
||||
@@ -2129,9 +2129,9 @@ test("model variants can be customized via config", async () => {
|
||||
})
|
||||
await Instance.provide({
|
||||
directory: tmp.path,
|
||||
init: Effect.promise(async () => {
|
||||
init: async () => {
|
||||
set("ANTHROPIC_API_KEY", "test-api-key")
|
||||
}).pipe(Effect.asVoid),
|
||||
},
|
||||
fn: async () => {
|
||||
const providers = await list()
|
||||
const model = providers[ProviderID.anthropic].models["claude-sonnet-4-20250514"]
|
||||
@@ -2168,9 +2168,9 @@ test("disabled key is stripped from variant config", async () => {
|
||||
})
|
||||
await Instance.provide({
|
||||
directory: tmp.path,
|
||||
init: Effect.promise(async () => {
|
||||
init: async () => {
|
||||
set("ANTHROPIC_API_KEY", "test-api-key")
|
||||
}).pipe(Effect.asVoid),
|
||||
},
|
||||
fn: async () => {
|
||||
const providers = await list()
|
||||
const model = providers[ProviderID.anthropic].models["claude-sonnet-4-20250514"]
|
||||
@@ -2206,9 +2206,9 @@ test("all variants can be disabled via config", async () => {
|
||||
})
|
||||
await Instance.provide({
|
||||
directory: tmp.path,
|
||||
init: Effect.promise(async () => {
|
||||
init: async () => {
|
||||
set("ANTHROPIC_API_KEY", "test-api-key")
|
||||
}).pipe(Effect.asVoid),
|
||||
},
|
||||
fn: async () => {
|
||||
const providers = await list()
|
||||
const model = providers[ProviderID.anthropic].models["claude-sonnet-4-20250514"]
|
||||
@@ -2244,9 +2244,9 @@ test("variant config merges with generated variants", async () => {
|
||||
})
|
||||
await Instance.provide({
|
||||
directory: tmp.path,
|
||||
init: Effect.promise(async () => {
|
||||
init: async () => {
|
||||
set("ANTHROPIC_API_KEY", "test-api-key")
|
||||
}).pipe(Effect.asVoid),
|
||||
},
|
||||
fn: async () => {
|
||||
const providers = await list()
|
||||
const model = providers[ProviderID.anthropic].models["claude-sonnet-4-20250514"]
|
||||
@@ -2282,9 +2282,9 @@ test("variants filtered in second pass for database models", async () => {
|
||||
})
|
||||
await Instance.provide({
|
||||
directory: tmp.path,
|
||||
init: Effect.promise(async () => {
|
||||
init: async () => {
|
||||
set("OPENAI_API_KEY", "test-api-key")
|
||||
}).pipe(Effect.asVoid),
|
||||
},
|
||||
fn: async () => {
|
||||
const providers = await list()
|
||||
const model = providers[ProviderID.openai].models["gpt-5"]
|
||||
@@ -2386,9 +2386,9 @@ test("Google Vertex: retains baseURL for custom proxy", async () => {
|
||||
|
||||
await Instance.provide({
|
||||
directory: tmp.path,
|
||||
init: Effect.promise(async () => {
|
||||
init: async () => {
|
||||
set("GOOGLE_APPLICATION_CREDENTIALS", "test-creds")
|
||||
}).pipe(Effect.asVoid),
|
||||
},
|
||||
fn: async () => {
|
||||
const providers = await list()
|
||||
expect(providers[ProviderID.make("vertex-proxy")]).toBeDefined()
|
||||
@@ -2431,9 +2431,9 @@ test("Google Vertex: supports OpenAI compatible models", async () => {
|
||||
|
||||
await Instance.provide({
|
||||
directory: tmp.path,
|
||||
init: Effect.promise(async () => {
|
||||
init: async () => {
|
||||
set("GOOGLE_APPLICATION_CREDENTIALS", "test-creds")
|
||||
}).pipe(Effect.asVoid),
|
||||
},
|
||||
fn: async () => {
|
||||
const providers = await list()
|
||||
const model = providers[ProviderID.make("vertex-openai")].models["gpt-4"]
|
||||
@@ -2457,11 +2457,11 @@ test("cloudflare-ai-gateway loads with env variables", async () => {
|
||||
})
|
||||
await Instance.provide({
|
||||
directory: tmp.path,
|
||||
init: Effect.promise(async () => {
|
||||
init: async () => {
|
||||
set("CLOUDFLARE_ACCOUNT_ID", "test-account")
|
||||
set("CLOUDFLARE_GATEWAY_ID", "test-gateway")
|
||||
set("CLOUDFLARE_API_TOKEN", "test-token")
|
||||
}).pipe(Effect.asVoid),
|
||||
},
|
||||
fn: async () => {
|
||||
const providers = await list()
|
||||
expect(providers[ProviderID.make("cloudflare-ai-gateway")]).toBeDefined()
|
||||
@@ -2489,11 +2489,11 @@ test("cloudflare-ai-gateway forwards config metadata options", async () => {
|
||||
})
|
||||
await Instance.provide({
|
||||
directory: tmp.path,
|
||||
init: Effect.promise(async () => {
|
||||
init: async () => {
|
||||
set("CLOUDFLARE_ACCOUNT_ID", "test-account")
|
||||
set("CLOUDFLARE_GATEWAY_ID", "test-gateway")
|
||||
set("CLOUDFLARE_API_TOKEN", "test-token")
|
||||
}).pipe(Effect.asVoid),
|
||||
},
|
||||
fn: async () => {
|
||||
const providers = await list()
|
||||
expect(providers[ProviderID.make("cloudflare-ai-gateway")]).toBeDefined()
|
||||
@@ -2557,7 +2557,7 @@ test("plugin config providers persist after instance dispose", async () => {
|
||||
expect(first[ProviderID.make("demo")]).toBeDefined()
|
||||
expect(first[ProviderID.make("demo")].models[ModelID.make("chat")]).toBeDefined()
|
||||
|
||||
await disposeAllInstances()
|
||||
await Instance.disposeAll()
|
||||
|
||||
const second = await Instance.provide({
|
||||
directory: tmp.path,
|
||||
@@ -2592,10 +2592,10 @@ test("plugin config enabled and disabled providers are honored", async () => {
|
||||
|
||||
await Instance.provide({
|
||||
directory: tmp.path,
|
||||
init: Effect.promise(async () => {
|
||||
init: async () => {
|
||||
set("ANTHROPIC_API_KEY", "test-anthropic-key")
|
||||
set("OPENAI_API_KEY", "test-openai-key")
|
||||
}).pipe(Effect.asVoid),
|
||||
},
|
||||
fn: async () => {
|
||||
const providers = await list()
|
||||
expect(providers[ProviderID.anthropic]).toBeDefined()
|
||||
|
||||
@@ -1,9 +1,8 @@
|
||||
import { afterEach, test, expect } from "bun:test"
|
||||
import { Question } from "../../src/question"
|
||||
import { Instance } from "../../src/project/instance"
|
||||
import { InstanceStore } from "../../src/project/instance-store"
|
||||
import { QuestionID } from "../../src/question/schema"
|
||||
import { disposeAllInstances, tmpdir } from "../fixture/fixture"
|
||||
import { tmpdir } from "../fixture/fixture"
|
||||
import { SessionID } from "../../src/session/schema"
|
||||
import { AppRuntime } from "../../src/effect/app-runtime"
|
||||
|
||||
@@ -18,7 +17,7 @@ const reply = (input: { requestID: QuestionID; answers: ReadonlyArray<Question.A
|
||||
const reject = (id: QuestionID) => AppRuntime.runPromise(Question.Service.use((svc) => svc.reject(id)))
|
||||
|
||||
afterEach(async () => {
|
||||
await disposeAllInstances()
|
||||
await Instance.disposeAll()
|
||||
})
|
||||
|
||||
/** Reject all pending questions so dangling Deferred fibers don't hang the test. */
|
||||
@@ -422,7 +421,7 @@ test("pending question rejects on instance dispose", async () => {
|
||||
fn: async () => {
|
||||
const items = await list()
|
||||
expect(items).toHaveLength(1)
|
||||
await InstanceStore.disposeInstance(Instance.current)
|
||||
await Instance.dispose()
|
||||
},
|
||||
})
|
||||
|
||||
@@ -457,7 +456,7 @@ test("pending question rejects on instance reload", async () => {
|
||||
fn: async () => {
|
||||
const items = await list()
|
||||
expect(items).toHaveLength(1)
|
||||
await InstanceStore.reloadInstance({ directory: tmp.path })
|
||||
await Instance.reload({ directory: tmp.path })
|
||||
},
|
||||
})
|
||||
|
||||
|
||||
@@ -12,7 +12,7 @@ import { ConfigProvider, Layer } from "effect"
|
||||
import { HttpRouter } from "effect/unstable/http"
|
||||
import { OpenApi } from "effect/unstable/httpapi"
|
||||
import { resetDatabase } from "../fixture/db"
|
||||
import { disposeAllInstances, tmpdir } from "../fixture/fixture"
|
||||
import { tmpdir } from "../fixture/fixture"
|
||||
|
||||
void Log.init({ print: false })
|
||||
|
||||
@@ -119,23 +119,7 @@ type RequestBody = {
|
||||
function parameterKey(param: unknown): string | undefined {
|
||||
if (!param || typeof param !== "object" || !("in" in param) || !("name" in param)) return undefined
|
||||
if (typeof param.in !== "string" || typeof param.name !== "string") return undefined
|
||||
return `${param.in}:${param.name}:${"required" in param && param.required === true}:${stableSchema(
|
||||
"schema" in param ? param.schema : undefined,
|
||||
)}`
|
||||
}
|
||||
|
||||
function stableSchema(input: unknown): string {
|
||||
return JSON.stringify(sortSchema(input))
|
||||
}
|
||||
|
||||
function sortSchema(input: unknown): unknown {
|
||||
if (Array.isArray(input)) return input.map(sortSchema)
|
||||
if (!input || typeof input !== "object") return input
|
||||
return Object.fromEntries(
|
||||
Object.entries(input)
|
||||
.sort(([left], [right]) => left.localeCompare(right))
|
||||
.map(([key, value]) => [key, sortSchema(value)]),
|
||||
)
|
||||
return `${param.in}:${param.name}:${"required" in param && param.required === true}`
|
||||
}
|
||||
|
||||
function parameterSchema(input: {
|
||||
@@ -208,7 +192,7 @@ afterEach(async () => {
|
||||
Flag.OPENCODE_EXPERIMENTAL_HTTPAPI = original.OPENCODE_EXPERIMENTAL_HTTPAPI
|
||||
Flag.OPENCODE_SERVER_PASSWORD = original.OPENCODE_SERVER_PASSWORD
|
||||
Flag.OPENCODE_SERVER_USERNAME = original.OPENCODE_SERVER_USERNAME
|
||||
await disposeAllInstances()
|
||||
await Instance.disposeAll()
|
||||
await resetDatabase()
|
||||
})
|
||||
|
||||
|
||||
@@ -6,7 +6,7 @@ import { Instance } from "../../src/project/instance"
|
||||
import { Server } from "../../src/server/server"
|
||||
import * as Log from "@opencode-ai/core/util/log"
|
||||
import { resetDatabase } from "../fixture/db"
|
||||
import { disposeAllInstances, tmpdir } from "../fixture/fixture"
|
||||
import { tmpdir } from "../fixture/fixture"
|
||||
|
||||
void Log.init({ print: false })
|
||||
|
||||
@@ -37,7 +37,7 @@ async function waitDisposed(directory: string) {
|
||||
|
||||
afterEach(async () => {
|
||||
Flag.OPENCODE_EXPERIMENTAL_HTTPAPI = original
|
||||
await disposeAllInstances()
|
||||
await Instance.disposeAll()
|
||||
await resetDatabase()
|
||||
})
|
||||
|
||||
|
||||
@@ -5,7 +5,7 @@ import { Server } from "../../src/server/server"
|
||||
import { EventPaths } from "../../src/server/routes/instance/httpapi/event"
|
||||
import * as Log from "@opencode-ai/core/util/log"
|
||||
import { resetDatabase } from "../fixture/db"
|
||||
import { disposeAllInstances, tmpdir } from "../fixture/fixture"
|
||||
import { tmpdir } from "../fixture/fixture"
|
||||
|
||||
void Log.init({ print: false })
|
||||
|
||||
@@ -29,7 +29,7 @@ async function readFirstChunk(response: Response) {
|
||||
|
||||
afterEach(async () => {
|
||||
Flag.OPENCODE_EXPERIMENTAL_HTTPAPI = original
|
||||
await disposeAllInstances()
|
||||
await Instance.disposeAll()
|
||||
await resetDatabase()
|
||||
})
|
||||
|
||||
|
||||
@@ -10,7 +10,7 @@ import { Database } from "@/storage/db"
|
||||
import * as Log from "@opencode-ai/core/util/log"
|
||||
import { Worktree } from "../../src/worktree"
|
||||
import { resetDatabase } from "../fixture/db"
|
||||
import { disposeAllInstances, tmpdir } from "../fixture/fixture"
|
||||
import { tmpdir } from "../fixture/fixture"
|
||||
|
||||
void Log.init({ print: false })
|
||||
|
||||
@@ -50,7 +50,7 @@ async function waitReady(directory: string) {
|
||||
|
||||
afterEach(async () => {
|
||||
Flag.OPENCODE_EXPERIMENTAL_HTTPAPI = original
|
||||
await disposeAllInstances()
|
||||
await Instance.disposeAll()
|
||||
await resetDatabase()
|
||||
})
|
||||
|
||||
|
||||
@@ -6,7 +6,7 @@ import { FilePaths } from "../../src/server/routes/instance/httpapi/groups/file"
|
||||
import { Instance } from "../../src/project/instance"
|
||||
import * as Log from "@opencode-ai/core/util/log"
|
||||
import { resetDatabase } from "../fixture/db"
|
||||
import { disposeAllInstances, tmpdir } from "../fixture/fixture"
|
||||
import { tmpdir } from "../fixture/fixture"
|
||||
|
||||
void Log.init({ print: false })
|
||||
|
||||
@@ -28,7 +28,7 @@ function request(route: string, directory: string, query?: Record<string, string
|
||||
}
|
||||
|
||||
afterEach(async () => {
|
||||
await disposeAllInstances()
|
||||
await Instance.disposeAll()
|
||||
await resetDatabase()
|
||||
})
|
||||
|
||||
|
||||
@@ -11,7 +11,6 @@ import { registerAdapter } from "../../src/control-plane/adapters"
|
||||
import type { WorkspaceAdapter } from "../../src/control-plane/types"
|
||||
import { Workspace } from "../../src/control-plane/workspace"
|
||||
import { InstanceRef, WorkspaceRef } from "../../src/effect/instance-ref"
|
||||
import { InstanceBootstrap } from "../../src/project/bootstrap"
|
||||
import { Instance } from "../../src/project/instance"
|
||||
import { InstanceStore } from "../../src/project/instance-store"
|
||||
import { Project } from "../../src/project/project"
|
||||
@@ -19,7 +18,7 @@ import { disposeMiddleware, markInstanceForDisposal } from "../../src/server/rou
|
||||
import { instanceRouterMiddleware } from "../../src/server/routes/instance/httpapi/middleware/instance-context"
|
||||
import { workspaceRouterMiddleware } from "../../src/server/routes/instance/httpapi/middleware/workspace-routing"
|
||||
import { resetDatabase } from "../fixture/db"
|
||||
import { disposeAllInstances, tmpdirScoped } from "../fixture/fixture"
|
||||
import { tmpdirScoped } from "../fixture/fixture"
|
||||
import { testEffect } from "../lib/effect"
|
||||
|
||||
const testStateLayer = Layer.effectDiscard(
|
||||
@@ -30,7 +29,7 @@ const testStateLayer = Layer.effectDiscard(
|
||||
yield* Effect.addFinalizer(() =>
|
||||
Effect.promise(async () => {
|
||||
Flag.OPENCODE_EXPERIMENTAL_WORKSPACES = originalWorkspaces
|
||||
await disposeAllInstances()
|
||||
await Instance.disposeAll()
|
||||
await resetDatabase()
|
||||
}),
|
||||
)
|
||||
@@ -42,7 +41,6 @@ const it = testEffect(
|
||||
testStateLayer,
|
||||
NodeHttpServer.layerTest,
|
||||
NodeServices.layer,
|
||||
InstanceBootstrap.defaultLayer,
|
||||
InstanceStore.defaultLayer,
|
||||
Project.defaultLayer,
|
||||
Workspace.defaultLayer,
|
||||
|
||||
@@ -6,7 +6,7 @@ import { Server } from "../../src/server/server"
|
||||
import { InstancePaths } from "../../src/server/routes/instance/httpapi/groups/instance"
|
||||
import * as Log from "@opencode-ai/core/util/log"
|
||||
import { resetDatabase } from "../fixture/db"
|
||||
import { disposeAllInstances, tmpdir } from "../fixture/fixture"
|
||||
import { tmpdir } from "../fixture/fixture"
|
||||
|
||||
void Log.init({ print: false })
|
||||
|
||||
@@ -37,7 +37,7 @@ async function waitDisposed(directory: string) {
|
||||
|
||||
afterEach(async () => {
|
||||
Flag.OPENCODE_EXPERIMENTAL_HTTPAPI = original
|
||||
await disposeAllInstances()
|
||||
await Instance.disposeAll()
|
||||
await resetDatabase()
|
||||
})
|
||||
|
||||
|
||||
@@ -7,13 +7,13 @@ import * as Socket from "effect/unstable/socket/Socket"
|
||||
import { InstancePaths } from "../../src/server/routes/instance/httpapi/groups/instance"
|
||||
import { ExperimentalHttpApiServer } from "../../src/server/routes/instance/httpapi/server"
|
||||
import { resetDatabase } from "../fixture/db"
|
||||
import { disposeAllInstances, tmpdirScoped } from "../fixture/fixture"
|
||||
import { tmpdirScoped } from "../fixture/fixture"
|
||||
import { testEffect } from "../lib/effect"
|
||||
|
||||
// Flip the experimental HttpApi flag so backend selection telemetry on the
|
||||
// production routes reports the right backend, and reset the database around
|
||||
// the test so per-instance state does not leak between runs. resetDatabase()
|
||||
// already calls disposeAllInstances(), so we don't repeat it.
|
||||
// already calls Instance.disposeAll(), so we don't repeat it.
|
||||
const testStateLayer = Layer.effectDiscard(
|
||||
Effect.gen(function* () {
|
||||
const originalHttpApi = Flag.OPENCODE_EXPERIMENTAL_HTTPAPI
|
||||
|
||||
@@ -15,7 +15,7 @@ import { MessageID, PartID } from "../../src/session/schema"
|
||||
import { Session } from "@/session/session"
|
||||
import * as Log from "@opencode-ai/core/util/log"
|
||||
import { resetDatabase } from "../fixture/db"
|
||||
import { disposeAllInstances, provideInstance, tmpdir } from "../fixture/fixture"
|
||||
import { provideInstance, tmpdir } from "../fixture/fixture"
|
||||
import { it } from "../lib/effect"
|
||||
|
||||
void Log.init({ print: false })
|
||||
@@ -89,7 +89,7 @@ function expectJsonParity(input: {
|
||||
|
||||
afterEach(async () => {
|
||||
Flag.OPENCODE_EXPERIMENTAL_HTTPAPI = original
|
||||
await disposeAllInstances()
|
||||
await Instance.disposeAll()
|
||||
await resetDatabase()
|
||||
})
|
||||
|
||||
|
||||
@@ -5,11 +5,10 @@ import { Flag } from "@opencode-ai/core/flag/flag"
|
||||
import { ExperimentalHttpApiServer } from "../../src/server/routes/instance/httpapi/server"
|
||||
import { McpPaths } from "../../src/server/routes/instance/httpapi/groups/mcp"
|
||||
import { Instance } from "../../src/project/instance"
|
||||
import { InstanceStore } from "../../src/project/instance-store"
|
||||
import { Server } from "../../src/server/server"
|
||||
import * as Log from "@opencode-ai/core/util/log"
|
||||
import { resetDatabase } from "../fixture/db"
|
||||
import { disposeAllInstances, provideInstance, tmpdir } from "../fixture/fixture"
|
||||
import { provideInstance, tmpdir } from "../fixture/fixture"
|
||||
import { testEffect } from "../lib/effect"
|
||||
|
||||
void Log.init({ print: false })
|
||||
@@ -58,9 +57,7 @@ function withMcpProject<A, E, R>(self: (dir: string) => Effect.Effect<A, E, R>)
|
||||
}),
|
||||
)
|
||||
yield* Effect.addFinalizer(() =>
|
||||
Effect.promise(() =>
|
||||
Instance.provide({ directory: dir, fn: () => InstanceStore.disposeInstance(Instance.current) }),
|
||||
).pipe(Effect.ignore),
|
||||
Effect.promise(() => Instance.provide({ directory: dir, fn: () => Instance.dispose() })).pipe(Effect.ignore),
|
||||
)
|
||||
|
||||
return yield* self(dir).pipe(provideInstance(dir))
|
||||
@@ -79,7 +76,7 @@ const readResponse = Effect.fnUntraced(function* (input: { app: TestApp; path: s
|
||||
|
||||
afterEach(async () => {
|
||||
Flag.OPENCODE_EXPERIMENTAL_HTTPAPI = original
|
||||
await disposeAllInstances()
|
||||
await Instance.disposeAll()
|
||||
await resetDatabase()
|
||||
})
|
||||
|
||||
|
||||
@@ -3,11 +3,10 @@ import { Effect, FileSystem, Layer, Path } from "effect"
|
||||
import { NodeFileSystem, NodePath } from "@effect/platform-node"
|
||||
import { Flag } from "@opencode-ai/core/flag/flag"
|
||||
import { Instance } from "../../src/project/instance"
|
||||
import { InstanceStore } from "../../src/project/instance-store"
|
||||
import { Server } from "../../src/server/server"
|
||||
import * as Log from "@opencode-ai/core/util/log"
|
||||
import { resetDatabase } from "../fixture/db"
|
||||
import { disposeAllInstances, provideInstance } from "../fixture/fixture"
|
||||
import { provideInstance } from "../fixture/fixture"
|
||||
import { testEffect } from "../lib/effect"
|
||||
|
||||
void Log.init({ print: false })
|
||||
@@ -90,9 +89,7 @@ function withProviderProject<A, E, R>(self: (dir: string) => Effect.Effect<A, E,
|
||||
)
|
||||
yield* writeProviderAuthPlugin(dir)
|
||||
yield* Effect.addFinalizer(() =>
|
||||
Effect.promise(() =>
|
||||
Instance.provide({ directory: dir, fn: () => InstanceStore.disposeInstance(Instance.current) }),
|
||||
).pipe(Effect.ignore),
|
||||
Effect.promise(() => Instance.provide({ directory: dir, fn: () => Instance.dispose() })).pipe(Effect.ignore),
|
||||
)
|
||||
|
||||
return yield* self(dir).pipe(provideInstance(dir))
|
||||
@@ -101,7 +98,7 @@ function withProviderProject<A, E, R>(self: (dir: string) => Effect.Effect<A, E,
|
||||
|
||||
afterEach(async () => {
|
||||
Flag.OPENCODE_EXPERIMENTAL_HTTPAPI = original
|
||||
await disposeAllInstances()
|
||||
await Instance.disposeAll()
|
||||
await resetDatabase()
|
||||
})
|
||||
|
||||
|
||||
@@ -7,7 +7,7 @@ import { Server } from "../../src/server/server"
|
||||
import { PtyPaths } from "../../src/server/routes/instance/httpapi/groups/pty"
|
||||
import * as Log from "@opencode-ai/core/util/log"
|
||||
import { resetDatabase } from "../fixture/db"
|
||||
import { disposeAllInstances, tmpdir, tmpdirScoped } from "../fixture/fixture"
|
||||
import { tmpdir, tmpdirScoped } from "../fixture/fixture"
|
||||
import { Config, Effect, Layer, Queue, Schema } from "effect"
|
||||
import { HttpClient, HttpClientRequest, HttpRouter, HttpServer } from "effect/unstable/http"
|
||||
import * as Socket from "effect/unstable/socket/Socket"
|
||||
@@ -63,7 +63,7 @@ const directoryHeader = (dir: string) => HttpClientRequest.setHeader("x-opencode
|
||||
|
||||
afterEach(async () => {
|
||||
Flag.OPENCODE_EXPERIMENTAL_HTTPAPI = original
|
||||
await disposeAllInstances()
|
||||
await Instance.disposeAll()
|
||||
await resetDatabase()
|
||||
})
|
||||
|
||||
|
||||
@@ -8,7 +8,7 @@ import { PtyPaths } from "../../src/server/routes/instance/httpapi/groups/pty"
|
||||
import { ExperimentalHttpApiServer } from "../../src/server/routes/instance/httpapi/server"
|
||||
import { PtyID } from "../../src/pty/schema"
|
||||
import { resetDatabase } from "../fixture/db"
|
||||
import { disposeAllInstances, tmpdir } from "../fixture/fixture"
|
||||
import { tmpdir } from "../fixture/fixture"
|
||||
import * as Log from "@opencode-ai/core/util/log"
|
||||
|
||||
void Log.init({ print: false })
|
||||
@@ -49,7 +49,7 @@ async function cancelBody(response: Response) {
|
||||
|
||||
afterEach(async () => {
|
||||
Flag.OPENCODE_EXPERIMENTAL_HTTPAPI = originalHttpApi
|
||||
await disposeAllInstances()
|
||||
await Instance.disposeAll()
|
||||
await resetDatabase()
|
||||
})
|
||||
|
||||
|
||||
@@ -15,7 +15,7 @@ import { Session as SessionNs } from "@/session/session"
|
||||
import { TestLLMServer } from "../lib/llm-server"
|
||||
import path from "path"
|
||||
import { resetDatabase } from "../fixture/db"
|
||||
import { disposeAllInstances, tmpdir } from "../fixture/fixture"
|
||||
import { tmpdir } from "../fixture/fixture"
|
||||
import { it } from "../lib/effect"
|
||||
|
||||
const original = {
|
||||
@@ -169,7 +169,7 @@ function sessionTitles(value: unknown) {
|
||||
|
||||
function resetState() {
|
||||
return Effect.promise(async () => {
|
||||
await disposeAllInstances()
|
||||
await Instance.disposeAll()
|
||||
await resetDatabase()
|
||||
})
|
||||
}
|
||||
@@ -260,7 +260,7 @@ afterEach(async () => {
|
||||
Flag.OPENCODE_EXPERIMENTAL_HTTPAPI = original.OPENCODE_EXPERIMENTAL_HTTPAPI
|
||||
Flag.OPENCODE_SERVER_PASSWORD = original.OPENCODE_SERVER_PASSWORD
|
||||
Flag.OPENCODE_SERVER_USERNAME = original.OPENCODE_SERVER_USERNAME
|
||||
await disposeAllInstances()
|
||||
await Instance.disposeAll()
|
||||
await resetDatabase()
|
||||
})
|
||||
|
||||
|
||||
@@ -20,7 +20,7 @@ import { SessionTable } from "@/session/session.sql"
|
||||
import * as Log from "@opencode-ai/core/util/log"
|
||||
import { eq } from "drizzle-orm"
|
||||
import { resetDatabase } from "../fixture/db"
|
||||
import { disposeAllInstances, tmpdir } from "../fixture/fixture"
|
||||
import { tmpdir } from "../fixture/fixture"
|
||||
import { it } from "../lib/effect"
|
||||
|
||||
void Log.init({ print: false })
|
||||
@@ -138,7 +138,7 @@ function withTmp<A, E, R>(
|
||||
afterEach(async () => {
|
||||
Flag.OPENCODE_EXPERIMENTAL_HTTPAPI = original
|
||||
Flag.OPENCODE_EXPERIMENTAL_WORKSPACES = originalWorkspaces
|
||||
await disposeAllInstances()
|
||||
await Instance.disposeAll()
|
||||
await resetDatabase()
|
||||
})
|
||||
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user