Compare commits
49
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c13699e2c6 | ||
|
|
1557f31415 | ||
|
|
c7a10ac38b | ||
|
|
baa6976a8d | ||
|
|
73406e786f | ||
|
|
76d490afeb | ||
|
|
5ad32d4e7b | ||
|
|
1477b38ea2 | ||
|
|
0e256a1de1 | ||
|
|
2e1f1c2af1 | ||
|
|
b5f391cd8c | ||
|
|
ed00ae267b | ||
|
|
eebb26aa7e | ||
|
|
895275eb1e | ||
|
|
913321e4b6 | ||
|
|
50d5c57193 | ||
|
|
e07f5fbb5d | ||
|
|
f21aa32367 | ||
|
|
b09b7d28b8 | ||
|
|
31ed4602e1 | ||
|
|
6a76346734 | ||
|
|
78b3000031 | ||
|
|
4c4860fb24 | ||
|
|
5242a1c6b4 | ||
|
|
075f876e6f | ||
|
|
a849812e9f | ||
|
|
d99dde6306 | ||
|
|
becf57ee6a | ||
|
|
f33aec1139 | ||
|
|
1571933096 | ||
|
|
160928a9a9 | ||
|
|
d297c29f22 | ||
|
|
0b498dd448 | ||
|
|
cec9c6122a | ||
|
|
51e310c9ce | ||
|
|
478156456e | ||
|
|
6252412d94 | ||
|
|
c2609cbf04 | ||
|
|
2115df57bf | ||
|
|
29ec07700c | ||
|
|
bcae852d28 | ||
|
|
16ddf5f559 | ||
|
|
8c79c58c4d | ||
|
|
97ed9ba624 | ||
|
|
a6b6395c8a | ||
|
|
21f8027ef7 | ||
|
|
a5aa72bd7d | ||
|
|
563177c6ac | ||
|
|
4eae8ec037 |
@@ -32,6 +32,7 @@ rekram1-node
|
|||||||
-ricardo-m-l
|
-ricardo-m-l
|
||||||
-robinmordasiewicz
|
-robinmordasiewicz
|
||||||
rubdos
|
rubdos
|
||||||
|
-saisharan0103 spamming ai prs
|
||||||
shantur
|
shantur
|
||||||
simonklee
|
simonklee
|
||||||
-spider-yamet clawdbot/llm psychosis, spam pinging the team
|
-spider-yamet clawdbot/llm psychosis, spam pinging the team
|
||||||
|
|||||||
@@ -29,7 +29,7 @@
|
|||||||
},
|
},
|
||||||
"packages/app": {
|
"packages/app": {
|
||||||
"name": "@opencode-ai/app",
|
"name": "@opencode-ai/app",
|
||||||
"version": "1.14.30",
|
"version": "1.14.31",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@kobalte/core": "catalog:",
|
"@kobalte/core": "catalog:",
|
||||||
"@opencode-ai/core": "workspace:*",
|
"@opencode-ai/core": "workspace:*",
|
||||||
@@ -85,7 +85,7 @@
|
|||||||
},
|
},
|
||||||
"packages/console/app": {
|
"packages/console/app": {
|
||||||
"name": "@opencode-ai/console-app",
|
"name": "@opencode-ai/console-app",
|
||||||
"version": "1.14.30",
|
"version": "1.14.31",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@cloudflare/vite-plugin": "1.15.2",
|
"@cloudflare/vite-plugin": "1.15.2",
|
||||||
"@ibm/plex": "6.4.1",
|
"@ibm/plex": "6.4.1",
|
||||||
@@ -119,7 +119,7 @@
|
|||||||
},
|
},
|
||||||
"packages/console/core": {
|
"packages/console/core": {
|
||||||
"name": "@opencode-ai/console-core",
|
"name": "@opencode-ai/console-core",
|
||||||
"version": "1.14.30",
|
"version": "1.14.31",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@aws-sdk/client-sts": "3.782.0",
|
"@aws-sdk/client-sts": "3.782.0",
|
||||||
"@jsx-email/render": "1.1.1",
|
"@jsx-email/render": "1.1.1",
|
||||||
@@ -146,7 +146,7 @@
|
|||||||
},
|
},
|
||||||
"packages/console/function": {
|
"packages/console/function": {
|
||||||
"name": "@opencode-ai/console-function",
|
"name": "@opencode-ai/console-function",
|
||||||
"version": "1.14.30",
|
"version": "1.14.31",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@ai-sdk/anthropic": "3.0.64",
|
"@ai-sdk/anthropic": "3.0.64",
|
||||||
"@ai-sdk/openai": "3.0.48",
|
"@ai-sdk/openai": "3.0.48",
|
||||||
@@ -170,7 +170,7 @@
|
|||||||
},
|
},
|
||||||
"packages/console/mail": {
|
"packages/console/mail": {
|
||||||
"name": "@opencode-ai/console-mail",
|
"name": "@opencode-ai/console-mail",
|
||||||
"version": "1.14.30",
|
"version": "1.14.31",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@jsx-email/all": "2.2.3",
|
"@jsx-email/all": "2.2.3",
|
||||||
"@jsx-email/cli": "1.4.3",
|
"@jsx-email/cli": "1.4.3",
|
||||||
@@ -194,7 +194,7 @@
|
|||||||
},
|
},
|
||||||
"packages/core": {
|
"packages/core": {
|
||||||
"name": "@opencode-ai/core",
|
"name": "@opencode-ai/core",
|
||||||
"version": "1.14.30",
|
"version": "1.14.31",
|
||||||
"bin": {
|
"bin": {
|
||||||
"opencode": "./bin/opencode",
|
"opencode": "./bin/opencode",
|
||||||
},
|
},
|
||||||
@@ -228,7 +228,7 @@
|
|||||||
},
|
},
|
||||||
"packages/desktop": {
|
"packages/desktop": {
|
||||||
"name": "@opencode-ai/desktop",
|
"name": "@opencode-ai/desktop",
|
||||||
"version": "1.14.30",
|
"version": "1.14.31",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@opencode-ai/app": "workspace:*",
|
"@opencode-ai/app": "workspace:*",
|
||||||
"@opencode-ai/ui": "workspace:*",
|
"@opencode-ai/ui": "workspace:*",
|
||||||
@@ -263,7 +263,7 @@
|
|||||||
},
|
},
|
||||||
"packages/desktop-electron": {
|
"packages/desktop-electron": {
|
||||||
"name": "@opencode-ai/desktop-electron",
|
"name": "@opencode-ai/desktop-electron",
|
||||||
"version": "1.14.30",
|
"version": "1.14.31",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"drizzle-orm": "catalog:",
|
"drizzle-orm": "catalog:",
|
||||||
"effect": "catalog:",
|
"effect": "catalog:",
|
||||||
@@ -309,7 +309,7 @@
|
|||||||
},
|
},
|
||||||
"packages/enterprise": {
|
"packages/enterprise": {
|
||||||
"name": "@opencode-ai/enterprise",
|
"name": "@opencode-ai/enterprise",
|
||||||
"version": "1.14.30",
|
"version": "1.14.31",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@opencode-ai/core": "workspace:*",
|
"@opencode-ai/core": "workspace:*",
|
||||||
"@opencode-ai/ui": "workspace:*",
|
"@opencode-ai/ui": "workspace:*",
|
||||||
@@ -338,7 +338,7 @@
|
|||||||
},
|
},
|
||||||
"packages/function": {
|
"packages/function": {
|
||||||
"name": "@opencode-ai/function",
|
"name": "@opencode-ai/function",
|
||||||
"version": "1.14.30",
|
"version": "1.14.31",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@octokit/auth-app": "8.0.1",
|
"@octokit/auth-app": "8.0.1",
|
||||||
"@octokit/rest": "catalog:",
|
"@octokit/rest": "catalog:",
|
||||||
@@ -354,7 +354,7 @@
|
|||||||
},
|
},
|
||||||
"packages/opencode": {
|
"packages/opencode": {
|
||||||
"name": "opencode",
|
"name": "opencode",
|
||||||
"version": "1.14.30",
|
"version": "1.14.31",
|
||||||
"bin": {
|
"bin": {
|
||||||
"opencode": "./bin/opencode",
|
"opencode": "./bin/opencode",
|
||||||
},
|
},
|
||||||
@@ -496,7 +496,7 @@
|
|||||||
},
|
},
|
||||||
"packages/plugin": {
|
"packages/plugin": {
|
||||||
"name": "@opencode-ai/plugin",
|
"name": "@opencode-ai/plugin",
|
||||||
"version": "1.14.30",
|
"version": "1.14.31",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@opencode-ai/sdk": "workspace:*",
|
"@opencode-ai/sdk": "workspace:*",
|
||||||
"effect": "catalog:",
|
"effect": "catalog:",
|
||||||
@@ -511,8 +511,8 @@
|
|||||||
"typescript": "catalog:",
|
"typescript": "catalog:",
|
||||||
},
|
},
|
||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
"@opentui/core": ">=0.2.0",
|
"@opentui/core": ">=0.2.2",
|
||||||
"@opentui/solid": ">=0.2.0",
|
"@opentui/solid": ">=0.2.2",
|
||||||
},
|
},
|
||||||
"optionalPeers": [
|
"optionalPeers": [
|
||||||
"@opentui/core",
|
"@opentui/core",
|
||||||
@@ -531,7 +531,7 @@
|
|||||||
},
|
},
|
||||||
"packages/sdk/js": {
|
"packages/sdk/js": {
|
||||||
"name": "@opencode-ai/sdk",
|
"name": "@opencode-ai/sdk",
|
||||||
"version": "1.14.30",
|
"version": "1.14.31",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"cross-spawn": "catalog:",
|
"cross-spawn": "catalog:",
|
||||||
},
|
},
|
||||||
@@ -546,7 +546,7 @@
|
|||||||
},
|
},
|
||||||
"packages/slack": {
|
"packages/slack": {
|
||||||
"name": "@opencode-ai/slack",
|
"name": "@opencode-ai/slack",
|
||||||
"version": "1.14.30",
|
"version": "1.14.31",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@opencode-ai/sdk": "workspace:*",
|
"@opencode-ai/sdk": "workspace:*",
|
||||||
"@slack/bolt": "^3.17.1",
|
"@slack/bolt": "^3.17.1",
|
||||||
@@ -581,7 +581,7 @@
|
|||||||
},
|
},
|
||||||
"packages/ui": {
|
"packages/ui": {
|
||||||
"name": "@opencode-ai/ui",
|
"name": "@opencode-ai/ui",
|
||||||
"version": "1.14.30",
|
"version": "1.14.31",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@kobalte/core": "catalog:",
|
"@kobalte/core": "catalog:",
|
||||||
"@opencode-ai/core": "workspace:*",
|
"@opencode-ai/core": "workspace:*",
|
||||||
@@ -630,7 +630,7 @@
|
|||||||
},
|
},
|
||||||
"packages/web": {
|
"packages/web": {
|
||||||
"name": "@opencode-ai/web",
|
"name": "@opencode-ai/web",
|
||||||
"version": "1.14.30",
|
"version": "1.14.31",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@astrojs/cloudflare": "12.6.3",
|
"@astrojs/cloudflare": "12.6.3",
|
||||||
"@astrojs/markdown-remark": "6.3.1",
|
"@astrojs/markdown-remark": "6.3.1",
|
||||||
@@ -690,8 +690,8 @@
|
|||||||
"@npmcli/arborist": "9.4.0",
|
"@npmcli/arborist": "9.4.0",
|
||||||
"@octokit/rest": "22.0.0",
|
"@octokit/rest": "22.0.0",
|
||||||
"@openauthjs/openauth": "0.0.0-20250322224806",
|
"@openauthjs/openauth": "0.0.0-20250322224806",
|
||||||
"@opentui/core": "0.2.0",
|
"@opentui/core": "0.2.2",
|
||||||
"@opentui/solid": "0.2.0",
|
"@opentui/solid": "0.2.2",
|
||||||
"@pierre/diffs": "1.1.0-beta.18",
|
"@pierre/diffs": "1.1.0-beta.18",
|
||||||
"@playwright/test": "1.59.1",
|
"@playwright/test": "1.59.1",
|
||||||
"@sentry/solid": "10.36.0",
|
"@sentry/solid": "10.36.0",
|
||||||
@@ -1618,21 +1618,21 @@
|
|||||||
|
|
||||||
"@opentelemetry/semantic-conventions": ["@opentelemetry/semantic-conventions@1.40.0", "", {}, "sha512-cifvXDhcqMwwTlTK04GBNeIe7yyo28Mfby85QXFe1Yk8nmi36Ab/5UQwptOx84SsoGNRg+EVSjwzfSZMy6pmlw=="],
|
"@opentelemetry/semantic-conventions": ["@opentelemetry/semantic-conventions@1.40.0", "", {}, "sha512-cifvXDhcqMwwTlTK04GBNeIe7yyo28Mfby85QXFe1Yk8nmi36Ab/5UQwptOx84SsoGNRg+EVSjwzfSZMy6pmlw=="],
|
||||||
|
|
||||||
"@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": ["@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-darwin-arm64": ["@opentui/core-darwin-arm64@0.2.0", "", { "os": "darwin", "cpu": "arm64" }, "sha512-VVmKwth3hzsQPjAZ7WGJxmzuzx0uCtynd79JJDg26D7QRM9V5beVGbKwwU5SKsDlK74EyQoY85Mv9xFY5E4jrA=="],
|
"@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-x64": ["@opentui/core-darwin-x64@0.2.0", "", { "os": "darwin", "cpu": "x64" }, "sha512-eX+WNdbSNr7Bozdq/MH6p1vXIALGt0SqBHR4YtWyTh6X7KDz9FTtJT3ylxMPqiVRUGBNAiWOxoqKGXW7JLQ0TA=="],
|
"@opentui/core-darwin-x64": ["@opentui/core-darwin-x64@0.2.2", "", { "os": "darwin", "cpu": "x64" }, "sha512-W/R7OnqY30FXcTG0tiP2JkQFmgtYbIte5afQ5PC12TliRoee1RqG3iCG6kY1jxW+3Vg6jge88uiSjUEDpeV2gA=="],
|
||||||
|
|
||||||
"@opentui/core-linux-arm64": ["@opentui/core-linux-arm64@0.2.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-ARZa+ywbN/OV7esT5ZdJMlQW3a4Pr56qLlEI/X65ik88C2sgmDze4Kf2FmqtvJ1hbv1YsMfLHH9MfhLl5twyHQ=="],
|
"@opentui/core-linux-arm64": ["@opentui/core-linux-arm64@0.2.2", "", { "os": "linux", "cpu": "arm64" }, "sha512-1pzTYFEZauYuw6AGycw2TYGtAlZVGjuUtSdxH1fP51kBPS3oVWduUY2j7GKREz3SU5NulvO2Wc6HWsm3feMqwQ=="],
|
||||||
|
|
||||||
"@opentui/core-linux-x64": ["@opentui/core-linux-x64@0.2.0", "", { "os": "linux", "cpu": "x64" }, "sha512-ZjNxrD45P51cdbABoivVQLBakVYwDqAridJbHhkK6T/+EU7YsTrmAu9ae19N9ZGnrlKzLViQF8GOavNUNjAbhw=="],
|
"@opentui/core-linux-x64": ["@opentui/core-linux-x64@0.2.2", "", { "os": "linux", "cpu": "x64" }, "sha512-ucVwUtUYeOYGVFPBLbPoxzbrPdhD0PDyKNQ2X4n1AJ9jlQX4gqBZRcXMEF8hiXDjFxsikZwef7De0ciCcWvAMg=="],
|
||||||
|
|
||||||
"@opentui/core-win32-arm64": ["@opentui/core-win32-arm64@0.2.0", "", { "os": "win32", "cpu": "arm64" }, "sha512-ImMjFPOWE8wcZQ2lUz1D418xonS/5EwnItUF1g5dbp1q9+A0vv2P3bxTenLwMqcYvG4wjO6gKT3n2QLnRd6qKg=="],
|
"@opentui/core-win32-arm64": ["@opentui/core-win32-arm64@0.2.2", "", { "os": "win32", "cpu": "arm64" }, "sha512-MPhYdJNdxmC5Bqsq6sis/+VkjRgkEjm+bQ1Tl++NSKLuiTU32Re0ImcZlgHbe+LZtZoGMZHVSgZlkGd3oYXO2g=="],
|
||||||
|
|
||||||
"@opentui/core-win32-x64": ["@opentui/core-win32-x64@0.2.0", "", { "os": "win32", "cpu": "x64" }, "sha512-6yfYHTtJ4yzbl8kXCW3Pc4eWbZDYVw21GumwdNgkjJJ2JqQAQ861em0riEoucYAa5qPYYTiMUEw7X4Fv8lGwuQ=="],
|
"@opentui/core-win32-x64": ["@opentui/core-win32-x64@0.2.2", "", { "os": "win32", "cpu": "x64" }, "sha512-19BroLfn2h0RDYfJS5o96Fc8kYCDhRBcseIXtHIkoKIsKMxx62KiDLo/byVye6rp+yQRRB7Xkd2uWqsbdiWo9w=="],
|
||||||
|
|
||||||
"@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=="],
|
"@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=="],
|
||||||
|
|
||||||
"@oslojs/asn1": ["@oslojs/asn1@1.0.0", "", { "dependencies": { "@oslojs/binary": "1.0.0" } }, "sha512-zw/wn0sj0j0QKbIXfIlnEcTviaCzYOY3V5rAyjR6YtOByFtJiT574+8p9Wlach0lZH9fddD4yb9laEAIl4vXQA=="],
|
"@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=="],
|
"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.1.2", "", { "peerDependencies": { "typescript": "^5" } }, "sha512-Lh1oQAYHDcnesJauieA4UNkWGXY9hYck7OA5IaRwE3Bp6K2F2pJSNYqq+hIy7P3uOvo3km3oxS8304g5gDMl/w=="],
|
"bun-ffi-structs": ["bun-ffi-structs@0.2.2", "", { "peerDependencies": { "typescript": "^5" } }, "sha512-N/ZWtyN0piZlrXQT7TO0V+q952orYqkfhXRXM1Hcbb+R3QSiBH4vLnib187Mrs1H7pWIYECAmPeapGYDOMCl+w=="],
|
||||||
|
|
||||||
"bun-pty": ["bun-pty@0.4.8", "", {}, "sha512-rO70Mrbr13+jxHHHu2YBkk2pNqrJE5cJn29WE++PUr+GFA0hq/VgtQPZANJ8dJo6d7XImvBk37Innt8GM7O28w=="],
|
"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-types": ["bun-types@1.3.12", "", { "dependencies": { "@types/node": "*" } }, "sha512-HqOLj5PoFajAQciOMRiIZGNoKxDJSr6qigAttOX40vJuSp6DN/CxWp9s3C1Xwm4oH7ybueITwiaOcWXoYVoRkA=="],
|
||||||
|
|
||||||
"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": ["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-darwin-arm64": ["bun-webgpu-darwin-arm64@0.1.7", "", { "os": "darwin", "cpu": "arm64" }, "sha512-mRrFFyHzPWjsTRidAZBRcu808CPQBOUL0P6b4nxLhp+XHcV/mbUHERZMgW9s58tsojQfSdzschiQa8q+JCgRWA=="],
|
"bun-webgpu-darwin-arm64": ["bun-webgpu-darwin-arm64@0.1.6", "", { "os": "darwin", "cpu": "arm64" }, "sha512-lIsDkPzJzPl6yrB5CUOINJFPnTRv6fF/Q8J1mAr43ogSp86WZEg9XZKaT6f3EUJ+9ETogGoMnoj1q0AwHUTbAQ=="],
|
||||||
|
|
||||||
"bun-webgpu-darwin-x64": ["bun-webgpu-darwin-x64@0.1.7", "", { "os": "darwin", "cpu": "x64" }, "sha512-g0NXGNgvaVCSH/jCWWlfdiquOHkbUN6vP4zqzSkIxWKQeLnqm3oADcok7SO3yIgI7v5mKpRc/ks7NDEKNH+jNQ=="],
|
"bun-webgpu-darwin-x64": ["bun-webgpu-darwin-x64@0.1.6", "", { "os": "darwin", "cpu": "x64" }, "sha512-uEddf5U7GvKIkM/BV18rUKtYHL6d0KeqBjNHwfqDH9QgEo9KVSKvJXS5I/sMefk5V5pIYE+8tQhtrREevhocng=="],
|
||||||
|
|
||||||
"bun-webgpu-linux-x64": ["bun-webgpu-linux-x64@0.1.7", "", { "os": "linux", "cpu": "x64" }, "sha512-UEP7UZdEhx9otvkZczjsszL8ZVlrODANQvgl+C88/bNVmxDoFi7w1fWzGi1sZyakiETjmtFDq2/xCLhbSZxjqw=="],
|
"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-win32-x64": ["bun-webgpu-win32-x64@0.1.7", "", { "os": "win32", "cpu": "x64" }, "sha512-KZktiFkBz6sN7PEm1NVdeaLP5Q5X/PlSHZqefY4nNuWtf0LNvh54NhZe7yVv/Plz/nGbv92b0KHMBY3ki/pp6g=="],
|
"bun-webgpu-win32-x64": ["bun-webgpu-win32-x64@0.1.6", "", { "os": "win32", "cpu": "x64" }, "sha512-MHSFAKqizISb+C5NfDrFe3g0Al5Njnu0j/A+oO2Q+bIWX+fUYjBSowiYE1ZXJx65KuryuB+tiM7Qh6cQbVvkEg=="],
|
||||||
|
|
||||||
"bundle-name": ["bundle-name@4.1.0", "", { "dependencies": { "run-applescript": "^7.0.0" } }, "sha512-tjwM5exMg6BGRI+kNmTntNsvdZS1X8BFYS6tnJ2hdH0kVxM6/eVZ2xy+FqStSWvYmtfFMDLIxurorHwDKfDz5Q=="],
|
"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=="],
|
"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@1.0.11", "", {}, "sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw=="],
|
"pako": ["pako@0.2.9", "", {}, "sha512-NUcwaKxUxWrZLpDG+z/xZaCgQITkA/Dv4V/T6bw7VON6l1Xz/VnrBqrYjZQ12TamKHzITTfOEIYUj48y2KXImA=="],
|
||||||
|
|
||||||
"param-case": ["param-case@3.0.4", "", { "dependencies": { "dot-case": "^3.0.4", "tslib": "^2.0.3" } }, "sha512-RXlj7zCYokReqWpOPH9oYivUzLYZ5vAPIfEmCTNViosC78F8F0H9y7T7gG2M39ymgutxF5gcFEsyZQSph9Bp3A=="],
|
"param-case": ["param-case@3.0.4", "", { "dependencies": { "dot-case": "^3.0.4", "tslib": "^2.0.3" } }, "sha512-RXlj7zCYokReqWpOPH9oYivUzLYZ5vAPIfEmCTNViosC78F8F0H9y7T7gG2M39ymgutxF5gcFEsyZQSph9Bp3A=="],
|
||||||
|
|
||||||
@@ -5640,6 +5640,8 @@
|
|||||||
|
|
||||||
"@opencode-ai/web/@shikijs/transformers": ["@shikijs/transformers@3.20.0", "", { "dependencies": { "@shikijs/core": "3.20.0", "@shikijs/types": "3.20.0" } }, "sha512-PrHHMRr3Q5W1qB/42kJW6laqFyWdhrPF2hNR9qjOm1xcSiAO3hAHo7HaVyHE6pMyevmy3i51O8kuGGXC78uK3g=="],
|
"@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=="],
|
"@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=="],
|
"@oslojs/jwt/@oslojs/encoding": ["@oslojs/encoding@0.4.1", "", {}, "sha512-hkjo6MuIK/kQR5CrGNdAPZhS01ZCXuWDRJ187zh6qqF2+yMHZpD9fAYpX8q2bOO6Ryhl3XpCT6kUX76N8hhm4Q=="],
|
||||||
@@ -6122,8 +6124,6 @@
|
|||||||
|
|
||||||
"type-is/mime-types": ["mime-types@2.1.35", "", { "dependencies": { "mime-db": "1.52.0" } }, "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw=="],
|
"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=="],
|
"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=="],
|
"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,6 +6132,8 @@
|
|||||||
|
|
||||||
"uri-js/punycode": ["punycode@2.3.1", "", {}, "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg=="],
|
"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=="],
|
"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=="],
|
"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=="],
|
||||||
@@ -6798,7 +6800,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/@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-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/core/bun-ffi-structs": ["bun-ffi-structs@0.1.2", "", { "peerDependencies": { "typescript": "^5" } }, "sha512-Lh1oQAYHDcnesJauieA4UNkWGXY9hYck7OA5IaRwE3Bp6K2F2pJSNYqq+hIy7P3uOvo3km3oxS8304g5gDMl/w=="],
|
||||||
|
|
||||||
"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=="],
|
"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=="],
|
||||||
|
|
||||||
@@ -7158,16 +7160,6 @@
|
|||||||
|
|
||||||
"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=="],
|
"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=="],
|
"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=="],
|
"ora/bl/buffer/ieee754": ["ieee754@1.2.1", "", {}, "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA=="],
|
||||||
|
|||||||
+4
-4
@@ -1,8 +1,8 @@
|
|||||||
{
|
{
|
||||||
"nodeModules": {
|
"nodeModules": {
|
||||||
"x86_64-linux": "sha256-OtyfKTBEHsJpjzAjN9vCR0PzGzdK6CDHdyU7eZ6Gl1s=",
|
"x86_64-linux": "sha256-SLWRe4uPSRWgU+NPa1BywmrUtNVIC0Oy2mjmxclxk+s=",
|
||||||
"aarch64-linux": "sha256-3eHJs3S/+uDUPAouWPsdBOlEvAOhOYx5bJzahL0tAJk=",
|
"aarch64-linux": "sha256-toHEeIqMzrmThoV0B52juGKm4pa/aJN3gBFFtrSZp2Q=",
|
||||||
"aarch64-darwin": "sha256-rFXzrkhPVb3yM20J8R8m7GqroNNk1vAEz+o/Ks+iAI4=",
|
"aarch64-darwin": "sha256-lYUsUxq5zR2RXjqZTEdjduOncnlwvTlxDJVKWXJuKPY=",
|
||||||
"x86_64-darwin": "sha256-lb1IGgbpxg723Qxj2WVPkxKUUmyOIsFOAhA5LoZ8GwY="
|
"x86_64-darwin": "sha256-77XmuEYqGwb1mkEHfnghq1VtukFTneohA0FW6WDOk1U="
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+2
-2
@@ -34,8 +34,8 @@
|
|||||||
"@types/cross-spawn": "6.0.6",
|
"@types/cross-spawn": "6.0.6",
|
||||||
"@octokit/rest": "22.0.0",
|
"@octokit/rest": "22.0.0",
|
||||||
"@hono/zod-validator": "0.4.2",
|
"@hono/zod-validator": "0.4.2",
|
||||||
"@opentui/core": "0.2.0",
|
"@opentui/core": "0.2.2",
|
||||||
"@opentui/solid": "0.2.0",
|
"@opentui/solid": "0.2.2",
|
||||||
"ulid": "3.0.1",
|
"ulid": "3.0.1",
|
||||||
"@kobalte/core": "0.13.11",
|
"@kobalte/core": "0.13.11",
|
||||||
"@types/luxon": "3.7.1",
|
"@types/luxon": "3.7.1",
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@opencode-ai/app",
|
"name": "@opencode-ai/app",
|
||||||
"version": "1.14.30",
|
"version": "1.14.31",
|
||||||
"description": "",
|
"description": "",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"exports": {
|
"exports": {
|
||||||
|
|||||||
@@ -204,6 +204,9 @@ function createGlobalSync() {
|
|||||||
},
|
},
|
||||||
translate: language.t,
|
translate: language.t,
|
||||||
getSdk: sdkFor,
|
getSdk: sdkFor,
|
||||||
|
global: {
|
||||||
|
provider: globalStore.provider,
|
||||||
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
async function loadSessions(directory: string) {
|
async function loadSessions(directory: string) {
|
||||||
|
|||||||
@@ -260,9 +260,6 @@ export async function bootstrapDirectory(input: {
|
|||||||
const seededPath = input.global.path.directory === input.directory ? input.global.path : undefined
|
const seededPath = input.global.path.directory === input.directory ? input.global.path : undefined
|
||||||
if (seededProject) input.setStore("project", seededProject)
|
if (seededProject) input.setStore("project", seededProject)
|
||||||
if (seededPath) input.setStore("path", seededPath)
|
if (seededPath) input.setStore("path", seededPath)
|
||||||
if (input.store.provider.all.length === 0 && input.global.provider.all.length > 0) {
|
|
||||||
input.setStore("provider", input.global.provider)
|
|
||||||
}
|
|
||||||
if (Object.keys(input.store.config).length === 0 && Object.keys(input.global.config).length > 0) {
|
if (Object.keys(input.store.config).length === 0 && Object.keys(input.global.config).length > 0) {
|
||||||
input.setStore("config", reconcile(input.global.config, { merge: false }))
|
input.setStore("config", reconcile(input.global.config, { merge: false }))
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -23,6 +23,7 @@ describe("createChildStoreManager", () => {
|
|||||||
onDispose() {},
|
onDispose() {},
|
||||||
translate: (key) => key,
|
translate: (key) => key,
|
||||||
getSdk: () => null!,
|
getSdk: () => null!,
|
||||||
|
global: { provider: null! },
|
||||||
})
|
})
|
||||||
|
|
||||||
Array.from({ length: 30 }, (_, index) => `/pinned-${index}`).forEach((directory) => {
|
Array.from({ length: 30 }, (_, index) => `/pinned-${index}`).forEach((directory) => {
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { createRoot, getOwner, onCleanup, runWithOwner, type Owner } from "solid-js"
|
import { createRoot, getOwner, onCleanup, runWithOwner, type Owner } from "solid-js"
|
||||||
import { createStore, type SetStoreFunction, type Store } from "solid-js/store"
|
import { createStore, type SetStoreFunction, type Store } from "solid-js/store"
|
||||||
import { Persist, persisted } from "@/utils/persist"
|
import { Persist, persisted } from "@/utils/persist"
|
||||||
import type { OpencodeClient, VcsInfo } from "@opencode-ai/sdk/v2/client"
|
import type { OpencodeClient, ProviderListResponse, VcsInfo } from "@opencode-ai/sdk/v2/client"
|
||||||
import {
|
import {
|
||||||
DIR_IDLE_TTL_MS,
|
DIR_IDLE_TTL_MS,
|
||||||
MAX_DIR_STORES,
|
MAX_DIR_STORES,
|
||||||
@@ -27,6 +27,9 @@ export function createChildStoreManager(input: {
|
|||||||
onDispose: (directory: string) => void
|
onDispose: (directory: string) => void
|
||||||
translate: (key: string, vars?: Record<string, string | number>) => string
|
translate: (key: string, vars?: Record<string, string | number>) => string
|
||||||
getSdk: (directory: string) => OpencodeClient
|
getSdk: (directory: string) => OpencodeClient
|
||||||
|
global: {
|
||||||
|
provider: ProviderListResponse
|
||||||
|
}
|
||||||
}) {
|
}) {
|
||||||
const children: Record<string, [Store<State>, SetStoreFunction<State>]> = {}
|
const children: Record<string, [Store<State>, SetStoreFunction<State>]> = {}
|
||||||
const vcsCache = new Map<string, VcsCache>()
|
const vcsCache = new Map<string, VcsCache>()
|
||||||
@@ -189,7 +192,13 @@ export function createChildStoreManager(input: {
|
|||||||
get provider_ready() {
|
get provider_ready() {
|
||||||
return !providerQuery.isLoading
|
return !providerQuery.isLoading
|
||||||
},
|
},
|
||||||
provider: { all: [], connected: [], default: {} },
|
get provider() {
|
||||||
|
const EMPTY = { all: [], connected: [], default: {} }
|
||||||
|
if (providerQuery.isLoading) return EMPTY
|
||||||
|
if (providerQuery.data?.all.length === 0 && input.global.provider.all.length > 0)
|
||||||
|
return input.global.provider
|
||||||
|
return providerQuery.data ?? EMPTY
|
||||||
|
},
|
||||||
config: {},
|
config: {},
|
||||||
get path() {
|
get path() {
|
||||||
if (pathQuery.isLoading || !pathQuery.data)
|
if (pathQuery.isLoading || !pathQuery.data)
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@opencode-ai/console-app",
|
"name": "@opencode-ai/console-app",
|
||||||
"version": "1.14.30",
|
"version": "1.14.31",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
|
|||||||
@@ -141,7 +141,10 @@ export async function handler(
|
|||||||
)
|
)
|
||||||
validateModelSettings(billingSource, authInfo)
|
validateModelSettings(billingSource, authInfo)
|
||||||
updateProviderKey(authInfo, providerInfo)
|
updateProviderKey(authInfo, providerInfo)
|
||||||
logger.metric({ provider: providerInfo.id })
|
logger.metric({
|
||||||
|
provider: providerInfo.id,
|
||||||
|
"provider.model": providerInfo.model,
|
||||||
|
})
|
||||||
|
|
||||||
const startTimestamp = Date.now()
|
const startTimestamp = Date.now()
|
||||||
const reqUrl = providerInfo.modifyUrl(providerInfo.api, isStream)
|
const reqUrl = providerInfo.modifyUrl(providerInfo.api, isStream)
|
||||||
@@ -149,12 +152,23 @@ export async function handler(
|
|||||||
providerInfo.modifyBody({
|
providerInfo.modifyBody({
|
||||||
...createBodyConverter(opts.format, providerInfo.format)(body),
|
...createBodyConverter(opts.format, providerInfo.format)(body),
|
||||||
model: providerInfo.model,
|
model: providerInfo.model,
|
||||||
...providerInfo.payloadModifier,
|
...(() => {
|
||||||
...Object.fromEntries(
|
const replacer = (obj: Record<string, any>): Record<string, any> =>
|
||||||
Object.entries(providerInfo.payloadMappings ?? {})
|
Object.fromEntries(
|
||||||
.map(([k, v]) => [k, input.request.headers.get(v)])
|
Object.entries(obj).flatMap(([k, v]) => {
|
||||||
.filter(([_k, v]) => !!v),
|
if (Array.isArray(v)) return [[k, v]]
|
||||||
),
|
if (typeof v === "object") return [[k, replacer(v)]]
|
||||||
|
if (v === "$ip") return [[k, ip]]
|
||||||
|
if (v === "$workspace") return authInfo?.workspaceID ? [[k, authInfo?.workspaceID]] : []
|
||||||
|
if (v.startsWith("$header.")) {
|
||||||
|
const headerValue = input.request.headers.get(v.slice(8))
|
||||||
|
return headerValue ? [[k, headerValue]] : []
|
||||||
|
}
|
||||||
|
return [[k, v]]
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
return replacer(providerInfo.payloadModifier ?? {})
|
||||||
|
})(),
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
logger.debug("REQUEST URL: " + reqUrl)
|
logger.debug("REQUEST URL: " + reqUrl)
|
||||||
@@ -514,7 +528,6 @@ export async function handler(
|
|||||||
reqModel,
|
reqModel,
|
||||||
providerModel: modelProvider.model,
|
providerModel: modelProvider.model,
|
||||||
adjustCacheUsage: providerProps.adjustCacheUsage,
|
adjustCacheUsage: providerProps.adjustCacheUsage,
|
||||||
safetyIdentifier: modelProvider.safetyIdentifier ? ip : undefined,
|
|
||||||
workspaceID: authInfo?.workspaceID,
|
workspaceID: authInfo?.workspaceID,
|
||||||
}
|
}
|
||||||
if (format === "anthropic") return anthropicHelper(opts)
|
if (format === "anthropic") return anthropicHelper(opts)
|
||||||
|
|||||||
@@ -23,7 +23,7 @@ type Usage = {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export const oaCompatHelper: ProviderHelper = ({ adjustCacheUsage, safetyIdentifier }) => ({
|
export const oaCompatHelper: ProviderHelper = ({ adjustCacheUsage }) => ({
|
||||||
format: "oa-compat",
|
format: "oa-compat",
|
||||||
modifyUrl: (providerApi: string) => providerApi + "/chat/completions",
|
modifyUrl: (providerApi: string) => providerApi + "/chat/completions",
|
||||||
modifyHeaders: (headers: Headers, body: Record<string, any>, apiKey: string) => {
|
modifyHeaders: (headers: Headers, body: Record<string, any>, apiKey: string) => {
|
||||||
@@ -34,7 +34,6 @@ export const oaCompatHelper: ProviderHelper = ({ adjustCacheUsage, safetyIdentif
|
|||||||
return {
|
return {
|
||||||
...body,
|
...body,
|
||||||
...(body.stream ? { stream_options: { include_usage: true } } : {}),
|
...(body.stream ? { stream_options: { include_usage: true } } : {}),
|
||||||
...(safetyIdentifier ? { safety_identifier: safetyIdentifier } : {}),
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
createBinaryStreamDecoder: () => undefined,
|
createBinaryStreamDecoder: () => undefined,
|
||||||
|
|||||||
@@ -18,10 +18,7 @@ export const openaiHelper: ProviderHelper = ({ workspaceID }) => ({
|
|||||||
modifyHeaders: (headers: Headers, body: Record<string, any>, apiKey: string) => {
|
modifyHeaders: (headers: Headers, body: Record<string, any>, apiKey: string) => {
|
||||||
headers.set("authorization", `Bearer ${apiKey}`)
|
headers.set("authorization", `Bearer ${apiKey}`)
|
||||||
},
|
},
|
||||||
modifyBody: (body: Record<string, any>) => ({
|
modifyBody: (body: Record<string, any>) => body,
|
||||||
...body,
|
|
||||||
...(workspaceID ? { safety_identifier: workspaceID } : {}),
|
|
||||||
}),
|
|
||||||
createBinaryStreamDecoder: () => undefined,
|
createBinaryStreamDecoder: () => undefined,
|
||||||
streamSeparator: "\n\n",
|
streamSeparator: "\n\n",
|
||||||
createUsageParser: () => {
|
createUsageParser: () => {
|
||||||
|
|||||||
@@ -37,7 +37,6 @@ export type ProviderHelper = (input: {
|
|||||||
reqModel: string
|
reqModel: string
|
||||||
providerModel: string
|
providerModel: string
|
||||||
adjustCacheUsage?: boolean
|
adjustCacheUsage?: boolean
|
||||||
safetyIdentifier?: string
|
|
||||||
workspaceID?: string
|
workspaceID?: string
|
||||||
}) => {
|
}) => {
|
||||||
format: ZenData.Format
|
format: ZenData.Format
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"$schema": "https://json.schemastore.org/package.json",
|
"$schema": "https://json.schemastore.org/package.json",
|
||||||
"name": "@opencode-ai/console-core",
|
"name": "@opencode-ai/console-core",
|
||||||
"version": "1.14.30",
|
"version": "1.14.31",
|
||||||
"private": true,
|
"private": true,
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
|||||||
@@ -40,7 +40,6 @@ export namespace ZenData {
|
|||||||
disabled: z.boolean().optional(),
|
disabled: z.boolean().optional(),
|
||||||
storeModel: z.string().optional(),
|
storeModel: z.string().optional(),
|
||||||
payloadModifier: z.record(z.string(), z.any()).optional(),
|
payloadModifier: z.record(z.string(), z.any()).optional(),
|
||||||
safetyIdentifier: z.boolean().optional(),
|
|
||||||
}),
|
}),
|
||||||
),
|
),
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@opencode-ai/console-function",
|
"name": "@opencode-ai/console-function",
|
||||||
"version": "1.14.30",
|
"version": "1.14.31",
|
||||||
"$schema": "https://json.schemastore.org/package.json",
|
"$schema": "https://json.schemastore.org/package.json",
|
||||||
"private": true,
|
"private": true,
|
||||||
"type": "module",
|
"type": "module",
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@opencode-ai/console-mail",
|
"name": "@opencode-ai/console-mail",
|
||||||
"version": "1.14.30",
|
"version": "1.14.31",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@jsx-email/all": "2.2.3",
|
"@jsx-email/all": "2.2.3",
|
||||||
"@jsx-email/cli": "1.4.3",
|
"@jsx-email/cli": "1.4.3",
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"$schema": "https://json.schemastore.org/package.json",
|
"$schema": "https://json.schemastore.org/package.json",
|
||||||
"version": "1.14.30",
|
"version": "1.14.31",
|
||||||
"name": "@opencode-ai/core",
|
"name": "@opencode-ai/core",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
|||||||
@@ -120,13 +120,17 @@ export const layer = Layer.effect(
|
|||||||
}
|
}
|
||||||
})()
|
})()
|
||||||
|
|
||||||
if (yield* afs.existsSafe(dir)) {
|
if (yield* afs.existsSafe(path.join(dir, "node_modules", name))) {
|
||||||
return resolveEntryPoint(name, path.join(dir, "node_modules", name))
|
return resolveEntryPoint(name, path.join(dir, "node_modules", name))
|
||||||
}
|
}
|
||||||
|
|
||||||
const tree = yield* reify({ dir, add: [pkg] })
|
const tree = yield* reify({ dir, add: [pkg] })
|
||||||
const first = tree.edgesOut.values().next().value?.to
|
const first = tree.edgesOut.values().next().value?.to
|
||||||
if (!first) return yield* new InstallFailedError({ add: [pkg], dir })
|
if (!first) {
|
||||||
|
const result = resolveEntryPoint(name, path.join(dir, "node_modules", name))
|
||||||
|
if (Option.isSome(result.entrypoint)) return result
|
||||||
|
return yield* new InstallFailedError({ add: [pkg], dir })
|
||||||
|
}
|
||||||
return resolveEntryPoint(first.name, first.path)
|
return resolveEntryPoint(first.name, first.path)
|
||||||
}, Effect.scoped)
|
}, Effect.scoped)
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,16 @@
|
|||||||
|
import { describe, expect, test } from "bun:test"
|
||||||
|
import fs from "fs/promises"
|
||||||
|
import os from "os"
|
||||||
|
import path from "path"
|
||||||
|
import { Global } from "@opencode-ai/core/global"
|
||||||
|
|
||||||
|
describe("global paths", () => {
|
||||||
|
test("tmp path is under the system temp directory", () => {
|
||||||
|
expect(Global.Path.tmp).toBe(path.join(os.tmpdir(), "opencode"))
|
||||||
|
expect(Global.make().tmp).toBe(Global.Path.tmp)
|
||||||
|
})
|
||||||
|
|
||||||
|
test("tmp path is created on module load", async () => {
|
||||||
|
expect((await fs.stat(Global.Path.tmp)).isDirectory()).toBe(true)
|
||||||
|
})
|
||||||
|
})
|
||||||
@@ -1,7 +1,12 @@
|
|||||||
import fs from "fs/promises"
|
import fs from "fs/promises"
|
||||||
import path from "path"
|
import path from "path"
|
||||||
import { describe, expect, test } from "bun:test"
|
import { describe, expect, test } from "bun:test"
|
||||||
|
import { NodeFileSystem } from "@effect/platform-node"
|
||||||
|
import { Effect, Layer, Option } from "effect"
|
||||||
|
import { AppFileSystem } from "@opencode-ai/core/filesystem"
|
||||||
|
import { Global } from "@opencode-ai/core/global"
|
||||||
import { Npm } from "@opencode-ai/core/npm"
|
import { Npm } from "@opencode-ai/core/npm"
|
||||||
|
import { EffectFlock } from "@opencode-ai/core/util/effect-flock"
|
||||||
import { tmpdir } from "./fixture/tmpdir"
|
import { tmpdir } from "./fixture/tmpdir"
|
||||||
|
|
||||||
const win = process.platform === "win32"
|
const win = process.platform === "win32"
|
||||||
@@ -15,6 +20,14 @@ const writePackage = (dir: string, pkg: Record<string, unknown>) =>
|
|||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
const npmLayer = (cache: string) =>
|
||||||
|
Npm.layer.pipe(
|
||||||
|
Layer.provide(EffectFlock.layer),
|
||||||
|
Layer.provide(AppFileSystem.layer),
|
||||||
|
Layer.provide(Global.layerWith({ cache, state: path.join(cache, "state") })),
|
||||||
|
Layer.provide(NodeFileSystem.layer),
|
||||||
|
)
|
||||||
|
|
||||||
describe("Npm.sanitize", () => {
|
describe("Npm.sanitize", () => {
|
||||||
test("keeps normal scoped package specs unchanged", () => {
|
test("keeps normal scoped package specs unchanged", () => {
|
||||||
expect(Npm.sanitize("@opencode/acme")).toBe("@opencode/acme")
|
expect(Npm.sanitize("@opencode/acme")).toBe("@opencode/acme")
|
||||||
@@ -29,6 +42,28 @@ describe("Npm.sanitize", () => {
|
|||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
describe("Npm.add", () => {
|
||||||
|
test("reifies when package cache directory exists without the package installed", async () => {
|
||||||
|
await using tmp = await tmpdir()
|
||||||
|
await fs.mkdir(path.join(tmp.path, "fixture-provider"))
|
||||||
|
await writePackage(path.join(tmp.path, "fixture-provider"), {
|
||||||
|
name: "fixture-provider",
|
||||||
|
main: "index.js",
|
||||||
|
})
|
||||||
|
await Bun.write(path.join(tmp.path, "fixture-provider", "index.js"), "export const fixture = true\n")
|
||||||
|
|
||||||
|
const spec = `fixture-provider@file:${path.join(tmp.path, "fixture-provider")}`
|
||||||
|
await fs.mkdir(path.join(tmp.path, "cache", "packages", Npm.sanitize(spec)), { recursive: true })
|
||||||
|
|
||||||
|
const entry = await Effect.gen(function* () {
|
||||||
|
const npm = yield* Npm.Service
|
||||||
|
return yield* npm.add(spec)
|
||||||
|
}).pipe(Effect.scoped, Effect.provide(npmLayer(path.join(tmp.path, "cache"))), Effect.runPromise)
|
||||||
|
|
||||||
|
expect(Option.isSome(entry.entrypoint)).toBe(true)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
describe("Npm.install", () => {
|
describe("Npm.install", () => {
|
||||||
test("respects omit from project .npmrc", async () => {
|
test("respects omit from project .npmrc", async () => {
|
||||||
await using tmp = await tmpdir()
|
await using tmp = await tmpdir()
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"name": "@opencode-ai/desktop-electron",
|
"name": "@opencode-ai/desktop-electron",
|
||||||
"private": true,
|
"private": true,
|
||||||
"version": "1.14.30",
|
"version": "1.14.31",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"homepage": "https://opencode.ai",
|
"homepage": "https://opencode.ai",
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"name": "@opencode-ai/desktop",
|
"name": "@opencode-ai/desktop",
|
||||||
"private": true,
|
"private": true,
|
||||||
"version": "1.14.30",
|
"version": "1.14.31",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@opencode-ai/enterprise",
|
"name": "@opencode-ai/enterprise",
|
||||||
"version": "1.14.30",
|
"version": "1.14.31",
|
||||||
"private": true,
|
"private": true,
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
id = "opencode"
|
id = "opencode"
|
||||||
name = "OpenCode"
|
name = "OpenCode"
|
||||||
description = "The open source coding agent."
|
description = "The open source coding agent."
|
||||||
version = "1.14.30"
|
version = "1.14.31"
|
||||||
schema_version = 1
|
schema_version = 1
|
||||||
authors = ["Anomaly"]
|
authors = ["Anomaly"]
|
||||||
repository = "https://github.com/anomalyco/opencode"
|
repository = "https://github.com/anomalyco/opencode"
|
||||||
@@ -11,26 +11,26 @@ name = "OpenCode"
|
|||||||
icon = "./icons/opencode.svg"
|
icon = "./icons/opencode.svg"
|
||||||
|
|
||||||
[agent_servers.opencode.targets.darwin-aarch64]
|
[agent_servers.opencode.targets.darwin-aarch64]
|
||||||
archive = "https://github.com/anomalyco/opencode/releases/download/v1.14.30/opencode-darwin-arm64.zip"
|
archive = "https://github.com/anomalyco/opencode/releases/download/v1.14.31/opencode-darwin-arm64.zip"
|
||||||
cmd = "./opencode"
|
cmd = "./opencode"
|
||||||
args = ["acp"]
|
args = ["acp"]
|
||||||
|
|
||||||
[agent_servers.opencode.targets.darwin-x86_64]
|
[agent_servers.opencode.targets.darwin-x86_64]
|
||||||
archive = "https://github.com/anomalyco/opencode/releases/download/v1.14.30/opencode-darwin-x64.zip"
|
archive = "https://github.com/anomalyco/opencode/releases/download/v1.14.31/opencode-darwin-x64.zip"
|
||||||
cmd = "./opencode"
|
cmd = "./opencode"
|
||||||
args = ["acp"]
|
args = ["acp"]
|
||||||
|
|
||||||
[agent_servers.opencode.targets.linux-aarch64]
|
[agent_servers.opencode.targets.linux-aarch64]
|
||||||
archive = "https://github.com/anomalyco/opencode/releases/download/v1.14.30/opencode-linux-arm64.tar.gz"
|
archive = "https://github.com/anomalyco/opencode/releases/download/v1.14.31/opencode-linux-arm64.tar.gz"
|
||||||
cmd = "./opencode"
|
cmd = "./opencode"
|
||||||
args = ["acp"]
|
args = ["acp"]
|
||||||
|
|
||||||
[agent_servers.opencode.targets.linux-x86_64]
|
[agent_servers.opencode.targets.linux-x86_64]
|
||||||
archive = "https://github.com/anomalyco/opencode/releases/download/v1.14.30/opencode-linux-x64.tar.gz"
|
archive = "https://github.com/anomalyco/opencode/releases/download/v1.14.31/opencode-linux-x64.tar.gz"
|
||||||
cmd = "./opencode"
|
cmd = "./opencode"
|
||||||
args = ["acp"]
|
args = ["acp"]
|
||||||
|
|
||||||
[agent_servers.opencode.targets.windows-x86_64]
|
[agent_servers.opencode.targets.windows-x86_64]
|
||||||
archive = "https://github.com/anomalyco/opencode/releases/download/v1.14.30/opencode-windows-x64.zip"
|
archive = "https://github.com/anomalyco/opencode/releases/download/v1.14.31/opencode-windows-x64.zip"
|
||||||
cmd = "./opencode.exe"
|
cmd = "./opencode.exe"
|
||||||
args = ["acp"]
|
args = ["acp"]
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@opencode-ai/function",
|
"name": "@opencode-ai/function",
|
||||||
"version": "1.14.30",
|
"version": "1.14.31",
|
||||||
"$schema": "https://json.schemastore.org/package.json",
|
"$schema": "https://json.schemastore.org/package.json",
|
||||||
"private": true,
|
"private": true,
|
||||||
"type": "module",
|
"type": "module",
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"$schema": "https://json.schemastore.org/package.json",
|
"$schema": "https://json.schemastore.org/package.json",
|
||||||
"version": "1.14.30",
|
"version": "1.14.31",
|
||||||
"name": "opencode",
|
"name": "opencode",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -198,7 +198,7 @@ Use raw Effect HTTP routes where `HttpApi` does not fit. The goal is deleting Ho
|
|||||||
| `project` | `bridged` | list, current, git init, update |
|
| `project` | `bridged` | list, current, git init, update |
|
||||||
| `file` | `bridged` partial | find text/file/symbol, list/content/status |
|
| `file` | `bridged` partial | find text/file/symbol, list/content/status |
|
||||||
| `mcp` | `bridged` | status, add, OAuth, connect/disconnect |
|
| `mcp` | `bridged` | status, add, OAuth, connect/disconnect |
|
||||||
| `workspace` | `bridged` | adaptor/list/status/create/remove/session-restore |
|
| `workspace` | `bridged` | adapter/list/status/create/remove/session-restore |
|
||||||
| top-level instance routes | `bridged` | path, vcs, command, agent, skill, lsp, formatter, dispose |
|
| top-level instance routes | `bridged` | path, vcs, command, agent, skill, lsp, formatter, dispose |
|
||||||
| experimental JSON routes | `bridged` | console, tool, worktree list/mutations, global session list, resource list |
|
| experimental JSON routes | `bridged` | console, tool, worktree list/mutations, global session list, resource list |
|
||||||
| `session` | `bridged` | read, lifecycle, prompt, message/part mutations, revert, permission reply |
|
| `session` | `bridged` | read, lifecycle, prompt, message/part mutations, revert, permission reply |
|
||||||
@@ -290,7 +290,7 @@ This checklist tracks bridge parity only. Checked routes are available through t
|
|||||||
|
|
||||||
### Workspace Routes
|
### Workspace Routes
|
||||||
|
|
||||||
- [x] `GET /experimental/workspace/adaptor` - list workspace adaptors.
|
- [x] `GET /experimental/workspace/adapter` - list workspace adapters.
|
||||||
- [x] `POST /experimental/workspace` - create workspace.
|
- [x] `POST /experimental/workspace` - create workspace.
|
||||||
- [x] `GET /experimental/workspace` - list workspaces.
|
- [x] `GET /experimental/workspace` - list workspaces.
|
||||||
- [x] `GET /experimental/workspace/status` - workspace status.
|
- [x] `GET /experimental/workspace/status` - workspace status.
|
||||||
|
|||||||
@@ -353,7 +353,7 @@ piecewise.
|
|||||||
- [ ] `src/cli/cmd/tui/event.ts`
|
- [ ] `src/cli/cmd/tui/event.ts`
|
||||||
- [ ] `src/cli/ui.ts`
|
- [ ] `src/cli/ui.ts`
|
||||||
- [ ] `src/command/index.ts`
|
- [ ] `src/command/index.ts`
|
||||||
- [x] `src/control-plane/adaptors/worktree.ts`
|
- [x] `src/control-plane/adapters/worktree.ts`
|
||||||
- [x] `src/control-plane/types.ts`
|
- [x] `src/control-plane/types.ts`
|
||||||
- [x] `src/control-plane/workspace.ts`
|
- [x] `src/control-plane/workspace.ts`
|
||||||
- [ ] `src/file/index.ts`
|
- [ ] `src/file/index.ts`
|
||||||
|
|||||||
@@ -1,17 +1,15 @@
|
|||||||
import { AppRuntime } from "@/effect/app-runtime"
|
|
||||||
import { InstanceBootstrap } from "../project/bootstrap"
|
|
||||||
import { Instance } from "../project/instance"
|
import { Instance } from "../project/instance"
|
||||||
|
import { InstanceStore } from "../project/instance-store"
|
||||||
|
|
||||||
export async function bootstrap<T>(directory: string, cb: () => Promise<T>) {
|
export async function bootstrap<T>(directory: string, cb: () => Promise<T>) {
|
||||||
return Instance.provide({
|
return Instance.provide({
|
||||||
directory,
|
directory,
|
||||||
init: () => AppRuntime.runPromise(InstanceBootstrap),
|
|
||||||
fn: async () => {
|
fn: async () => {
|
||||||
try {
|
try {
|
||||||
const result = await cb()
|
const result = await cb()
|
||||||
return result
|
return result
|
||||||
} finally {
|
} finally {
|
||||||
await Instance.dispose()
|
await InstanceStore.disposeInstance(Instance.current)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -133,6 +133,8 @@ export function tui(input: {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const renderer = await createCliRenderer(rendererConfig(input.config))
|
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"
|
const mode = (await renderer.waitForThemeMode(1000)) ?? "dark"
|
||||||
|
|
||||||
await render(() => {
|
await render(() => {
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ import { errorMessage } from "@/util/error"
|
|||||||
import { useSDK } from "../context/sdk"
|
import { useSDK } from "../context/sdk"
|
||||||
import { useToast } from "../ui/toast"
|
import { useToast } from "../ui/toast"
|
||||||
|
|
||||||
type Adaptor = {
|
type Adapter = {
|
||||||
type: string
|
type: string
|
||||||
name: string
|
name: string
|
||||||
description: string
|
description: string
|
||||||
@@ -108,26 +108,26 @@ export function DialogWorkspaceCreate(props: { onSelect: (workspaceID: string) =
|
|||||||
const sdk = useSDK()
|
const sdk = useSDK()
|
||||||
const toast = useToast()
|
const toast = useToast()
|
||||||
const [creating, setCreating] = createSignal<string>()
|
const [creating, setCreating] = createSignal<string>()
|
||||||
const [adaptors, setAdaptors] = createSignal<Adaptor[]>()
|
const [adapters, setAdapters] = createSignal<Adapter[]>()
|
||||||
|
|
||||||
onMount(() => {
|
onMount(() => {
|
||||||
dialog.setSize("medium")
|
dialog.setSize("medium")
|
||||||
void (async () => {
|
void (async () => {
|
||||||
const dir = sync.path.directory || sdk.directory
|
const dir = sync.path.directory || sdk.directory
|
||||||
const url = new URL("/experimental/workspace/adaptor", sdk.url)
|
const url = new URL("/experimental/workspace/adapter", sdk.url)
|
||||||
if (dir) url.searchParams.set("directory", dir)
|
if (dir) url.searchParams.set("directory", dir)
|
||||||
const res = await sdk
|
const res = await sdk
|
||||||
.fetch(url)
|
.fetch(url)
|
||||||
.then((x) => x.json() as Promise<Adaptor[]>)
|
.then((x) => x.json() as Promise<Adapter[]>)
|
||||||
.catch(() => undefined)
|
.catch(() => undefined)
|
||||||
if (!res) {
|
if (!res) {
|
||||||
toast.show({
|
toast.show({
|
||||||
message: "Failed to load workspace adaptors",
|
message: "Failed to load workspace adapters",
|
||||||
variant: "error",
|
variant: "error",
|
||||||
})
|
})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
setAdaptors(res)
|
setAdapters(res)
|
||||||
})()
|
})()
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -142,13 +142,13 @@ export function DialogWorkspaceCreate(props: { onSelect: (workspaceID: string) =
|
|||||||
},
|
},
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
const list = adaptors()
|
const list = adapters()
|
||||||
if (!list) {
|
if (!list) {
|
||||||
return [
|
return [
|
||||||
{
|
{
|
||||||
title: "Loading workspaces...",
|
title: "Loading workspaces...",
|
||||||
value: "loading" as const,
|
value: "loading" as const,
|
||||||
description: "Fetching available workspace adaptors",
|
description: "Fetching available workspace adapters",
|
||||||
},
|
},
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import { BoxRenderable, MouseButton, MouseEvent, RGBA, TextAttributes } from "@opentui/core"
|
import { BoxRenderable, MouseButton, MouseEvent, RGBA, TextAttributes } from "@opentui/core"
|
||||||
|
import { useRenderer } from "@opentui/solid"
|
||||||
import { For, createMemo, createSignal, onCleanup, onMount, type JSX } from "solid-js"
|
import { For, createMemo, createSignal, onCleanup, onMount, type JSX } from "solid-js"
|
||||||
import { useTheme, tint } from "@tui/context/theme"
|
import { useTheme, tint } from "@tui/context/theme"
|
||||||
import * as Sound from "@tui/util/sound"
|
import * as Sound from "@tui/util/sound"
|
||||||
@@ -554,6 +555,7 @@ function buildIdleState(t: number, ctx: LogoContext): IdleState {
|
|||||||
export function Logo(props: { shape?: LogoShape; ink?: RGBA; idle?: boolean } = {}) {
|
export function Logo(props: { shape?: LogoShape; ink?: RGBA; idle?: boolean } = {}) {
|
||||||
const ctx = props.shape ? build(props.shape) : DEFAULT
|
const ctx = props.shape ? build(props.shape) : DEFAULT
|
||||||
const { theme } = useTheme()
|
const { theme } = useTheme()
|
||||||
|
const renderer = useRenderer()
|
||||||
const [rings, setRings] = createSignal<Ring[]>([])
|
const [rings, setRings] = createSignal<Ring[]>([])
|
||||||
const [hold, setHold] = createSignal<Hold>()
|
const [hold, setHold] = createSignal<Hold>()
|
||||||
const [release, setRelease] = createSignal<Release>()
|
const [release, setRelease] = createSignal<Release>()
|
||||||
@@ -684,6 +686,7 @@ export function Logo(props: { shape?: LogoShape; ink?: RGBA; idle?: boolean } =
|
|||||||
})
|
})
|
||||||
|
|
||||||
const idleState = createMemo(() => (props.idle ? buildIdleState(frame().t, ctx) : undefined))
|
const idleState = createMemo(() => (props.idle ? buildIdleState(frame().t, ctx) : undefined))
|
||||||
|
const useSubpixelBlocks = () => renderer.capabilities?.rgb === true
|
||||||
|
|
||||||
const renderLine = (
|
const renderLine = (
|
||||||
line: string,
|
line: string,
|
||||||
@@ -789,7 +792,7 @@ export function Logo(props: { shape?: LogoShape; ink?: RGBA; idle?: boolean } =
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Solid █: render as ▀ so the top pixel (fg) and bottom pixel (bg) can carry independent shimmer values
|
// Solid █: render as ▀ so the top pixel (fg) and bottom pixel (bg) can carry independent shimmer values
|
||||||
if (char === "█") {
|
if (char === "█" && useSubpixelBlocks()) {
|
||||||
return (
|
return (
|
||||||
<text
|
<text
|
||||||
fg={shade(inkTop, theme, n + p + e + b)}
|
fg={shade(inkTop, theme, n + p + e + b)}
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ import { MessageID, PartID } from "@/session/schema"
|
|||||||
import { createStore, produce, unwrap } from "solid-js/store"
|
import { createStore, produce, unwrap } from "solid-js/store"
|
||||||
import { useKeybind } from "@tui/context/keybind"
|
import { useKeybind } from "@tui/context/keybind"
|
||||||
import { usePromptHistory, type PromptInfo } from "./history"
|
import { usePromptHistory, type PromptInfo } from "./history"
|
||||||
|
import { computePromptTraits } from "./traits"
|
||||||
import { assign } from "./part"
|
import { assign } from "./part"
|
||||||
import { usePromptStash } from "./stash"
|
import { usePromptStash } from "./stash"
|
||||||
import { DialogStash } from "../dialog-stash"
|
import { DialogStash } from "../dialog-stash"
|
||||||
@@ -557,17 +558,11 @@ export function Prompt(props: PromptProps) {
|
|||||||
|
|
||||||
createEffect(() => {
|
createEffect(() => {
|
||||||
if (!input || input.isDestroyed) return
|
if (!input || input.isDestroyed) return
|
||||||
const capture =
|
input.traits = computePromptTraits({
|
||||||
store.mode === "normal"
|
mode: store.mode,
|
||||||
? auto()?.visible
|
disabled: !!props.disabled,
|
||||||
? (["escape", "navigate", "submit", "tab"] as const)
|
autocompleteVisible: !!auto()?.visible,
|
||||||
: (["tab"] as const)
|
})
|
||||||
: undefined
|
|
||||||
input.traits = {
|
|
||||||
capture,
|
|
||||||
suspend: !!props.disabled || store.mode === "shell",
|
|
||||||
status: store.mode === "shell" ? "SHELL" : undefined,
|
|
||||||
}
|
|
||||||
})
|
})
|
||||||
|
|
||||||
function restoreExtmarksFromParts(parts: PromptInfo["parts"]) {
|
function restoreExtmarksFromParts(parts: PromptInfo["parts"]) {
|
||||||
|
|||||||
@@ -0,0 +1,31 @@
|
|||||||
|
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,12 +416,16 @@ export const { use: useTheme, provider: ThemeProvider } = createSimpleContext({
|
|||||||
|
|
||||||
const values = createMemo(() => {
|
const values = createMemo(() => {
|
||||||
const active = store.themes[store.active]
|
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")
|
const saved = kv.get("theme")
|
||||||
if (typeof saved === "string") {
|
if (typeof saved === "string") {
|
||||||
const theme = store.themes[saved]
|
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)
|
return resolveTheme(store.themes.opencode, store.mode)
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import { Installation } from "@/installation"
|
|||||||
import { Server } from "@/server/server"
|
import { Server } from "@/server/server"
|
||||||
import * as Log from "@opencode-ai/core/util/log"
|
import * as Log from "@opencode-ai/core/util/log"
|
||||||
import { Instance } from "@/project/instance"
|
import { Instance } from "@/project/instance"
|
||||||
import { InstanceBootstrap } from "@/project/bootstrap"
|
import { InstanceStore } from "@/project/instance-store"
|
||||||
import { Rpc } from "@/util/rpc"
|
import { Rpc } from "@/util/rpc"
|
||||||
import { upgrade } from "@/cli/upgrade"
|
import { upgrade } from "@/cli/upgrade"
|
||||||
import { Config } from "@/config/config"
|
import { Config } from "@/config/config"
|
||||||
@@ -77,7 +77,6 @@ export const rpc = {
|
|||||||
async checkUpgrade(input: { directory: string }) {
|
async checkUpgrade(input: { directory: string }) {
|
||||||
await Instance.provide({
|
await Instance.provide({
|
||||||
directory: input.directory,
|
directory: input.directory,
|
||||||
init: () => AppRuntime.runPromise(InstanceBootstrap),
|
|
||||||
fn: async () => {
|
fn: async () => {
|
||||||
await upgrade().catch(() => {})
|
await upgrade().catch(() => {})
|
||||||
},
|
},
|
||||||
@@ -89,7 +88,7 @@ export const rpc = {
|
|||||||
async shutdown() {
|
async shutdown() {
|
||||||
Log.Default.info("worker shutting down")
|
Log.Default.info("worker shutting down")
|
||||||
|
|
||||||
await Instance.disposeAll()
|
await InstanceStore.disposeAllInstances()
|
||||||
if (server) await server.stop(true)
|
if (server) await server.stop(true)
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,7 +11,8 @@ import { Flag } from "@opencode-ai/core/flag/flag"
|
|||||||
import { Auth } from "../auth"
|
import { Auth } from "../auth"
|
||||||
import { Env } from "../env"
|
import { Env } from "../env"
|
||||||
import { applyEdits, modify } from "jsonc-parser"
|
import { applyEdits, modify } from "jsonc-parser"
|
||||||
import { Instance, type InstanceContext } from "../project/instance"
|
import { type InstanceContext } from "../project/instance"
|
||||||
|
import { InstanceStore } from "../project/instance-store"
|
||||||
import { InstallationLocal, InstallationVersion } from "@opencode-ai/core/installation/version"
|
import { InstallationLocal, InstallationVersion } from "@opencode-ai/core/installation/version"
|
||||||
import { existsSync } from "fs"
|
import { existsSync } from "fs"
|
||||||
import { GlobalBus } from "@/bus/global"
|
import { GlobalBus } from "@/bus/global"
|
||||||
@@ -23,7 +24,7 @@ import { AppFileSystem } from "@opencode-ai/core/filesystem"
|
|||||||
import { InstanceState } from "@/effect/instance-state"
|
import { InstanceState } from "@/effect/instance-state"
|
||||||
import { Context, Duration, Effect, Exit, Fiber, Layer, Option, Schema } from "effect"
|
import { Context, Duration, Effect, Exit, Fiber, Layer, Option, Schema } from "effect"
|
||||||
import { EffectFlock } from "@opencode-ai/core/util/effect-flock"
|
import { EffectFlock } from "@opencode-ai/core/util/effect-flock"
|
||||||
import { InstanceRef } from "@/effect/instance-ref"
|
import { containsPath } from "../project/instance-context"
|
||||||
import { zod } from "@/util/effect-zod"
|
import { zod } from "@/util/effect-zod"
|
||||||
import { NonNegativeInt, PositiveInt, withStatics, type DeepMutable } from "@/util/schema"
|
import { NonNegativeInt, PositiveInt, withStatics, type DeepMutable } from "@/util/schema"
|
||||||
import { ConfigAgent } from "./agent"
|
import { ConfigAgent } from "./agent"
|
||||||
@@ -459,7 +460,7 @@ export const layer = Layer.effect(
|
|||||||
const pluginScopeForSource = Effect.fnUntraced(function* (source: string) {
|
const pluginScopeForSource = Effect.fnUntraced(function* (source: string) {
|
||||||
if (source.startsWith("http://") || source.startsWith("https://")) return "global"
|
if (source.startsWith("http://") || source.startsWith("https://")) return "global"
|
||||||
if (source === "OPENCODE_CONFIG_CONTENT") return "local"
|
if (source === "OPENCODE_CONFIG_CONTENT") return "local"
|
||||||
if (yield* InstanceRef.use((ctx) => Effect.succeed(Instance.containsPath(source, ctx)))) return "local"
|
if (containsPath(source, ctx)) return "local"
|
||||||
return "global"
|
return "global"
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -736,12 +737,18 @@ export const layer = Layer.effect(
|
|||||||
yield* fs
|
yield* fs
|
||||||
.writeFileString(file, JSON.stringify(mergeDeep(writable(existing), writable(config)), null, 2))
|
.writeFileString(file, JSON.stringify(mergeDeep(writable(existing), writable(config)), null, 2))
|
||||||
.pipe(Effect.orDie)
|
.pipe(Effect.orDie)
|
||||||
if (options?.dispose !== false) yield* Effect.promise(() => Instance.dispose())
|
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))
|
||||||
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
const invalidate = Effect.fn("Config.invalidate")(function* (wait?: boolean) {
|
const invalidate = Effect.fn("Config.invalidate")(function* (wait?: boolean) {
|
||||||
yield* invalidateGlobal
|
yield* invalidateGlobal
|
||||||
const task = Instance.disposeAll()
|
const task = InstanceStore.disposeAllInstances()
|
||||||
.catch(() => undefined)
|
.catch(() => undefined)
|
||||||
.finally(() =>
|
.finally(() =>
|
||||||
GlobalBus.emit("event", {
|
GlobalBus.emit("event", {
|
||||||
|
|||||||
@@ -0,0 +1,45 @@
|
|||||||
|
import type { ProjectID } from "@/project/schema"
|
||||||
|
import type { WorkspaceAdapter, WorkspaceAdapterEntry } from "../types"
|
||||||
|
import { WorktreeAdapter } from "./worktree"
|
||||||
|
|
||||||
|
const BUILTIN: Record<string, WorkspaceAdapter> = {
|
||||||
|
worktree: WorktreeAdapter,
|
||||||
|
}
|
||||||
|
|
||||||
|
const state = new Map<ProjectID, Map<string, WorkspaceAdapter>>()
|
||||||
|
|
||||||
|
export function getAdapter(projectID: ProjectID, type: string): WorkspaceAdapter {
|
||||||
|
const custom = state.get(projectID)?.get(type)
|
||||||
|
if (custom) return custom
|
||||||
|
|
||||||
|
const builtin = BUILTIN[type]
|
||||||
|
if (builtin) return builtin
|
||||||
|
|
||||||
|
throw new Error(`Unknown workspace adapter: ${type}`)
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function listAdapters(projectID: ProjectID): Promise<WorkspaceAdapterEntry[]> {
|
||||||
|
const builtin = await Promise.all(
|
||||||
|
Object.entries(BUILTIN).map(async ([type, adapter]) => {
|
||||||
|
return {
|
||||||
|
type,
|
||||||
|
name: adapter.name,
|
||||||
|
description: adapter.description,
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
const custom = [...(state.get(projectID)?.entries() ?? [])].map(([type, adapter]) => ({
|
||||||
|
type,
|
||||||
|
name: adapter.name,
|
||||||
|
description: adapter.description,
|
||||||
|
}))
|
||||||
|
return [...builtin, ...custom]
|
||||||
|
}
|
||||||
|
|
||||||
|
// Plugins can be loaded per-project so we need to scope them. If you
|
||||||
|
// want to install a global one pass `ProjectID.global`
|
||||||
|
export function registerAdapter(projectID: ProjectID, type: string, adapter: WorkspaceAdapter) {
|
||||||
|
const adapters = state.get(projectID) ?? new Map<string, WorkspaceAdapter>()
|
||||||
|
adapters.set(type, adapter)
|
||||||
|
state.set(projectID, adapters)
|
||||||
|
}
|
||||||
+2
-2
@@ -1,5 +1,5 @@
|
|||||||
import { Schema } from "effect"
|
import { Schema } from "effect"
|
||||||
import { type WorkspaceAdaptor, WorkspaceInfo } from "../types"
|
import { type WorkspaceAdapter, WorkspaceInfo } from "../types"
|
||||||
|
|
||||||
const WorktreeConfig = Schema.Struct({
|
const WorktreeConfig = Schema.Struct({
|
||||||
name: WorkspaceInfo.fields.name,
|
name: WorkspaceInfo.fields.name,
|
||||||
@@ -13,7 +13,7 @@ async function loadWorktree() {
|
|||||||
return { AppRuntime, Worktree }
|
return { AppRuntime, Worktree }
|
||||||
}
|
}
|
||||||
|
|
||||||
export const WorktreeAdaptor: WorkspaceAdaptor = {
|
export const WorktreeAdapter: WorkspaceAdapter = {
|
||||||
name: "Worktree",
|
name: "Worktree",
|
||||||
description: "Create a git worktree",
|
description: "Create a git worktree",
|
||||||
async configure(info) {
|
async configure(info) {
|
||||||
@@ -1,45 +0,0 @@
|
|||||||
import type { ProjectID } from "@/project/schema"
|
|
||||||
import type { WorkspaceAdaptor, WorkspaceAdaptorEntry } from "../types"
|
|
||||||
import { WorktreeAdaptor } from "./worktree"
|
|
||||||
|
|
||||||
const BUILTIN: Record<string, WorkspaceAdaptor> = {
|
|
||||||
worktree: WorktreeAdaptor,
|
|
||||||
}
|
|
||||||
|
|
||||||
const state = new Map<ProjectID, Map<string, WorkspaceAdaptor>>()
|
|
||||||
|
|
||||||
export function getAdaptor(projectID: ProjectID, type: string): WorkspaceAdaptor {
|
|
||||||
const custom = state.get(projectID)?.get(type)
|
|
||||||
if (custom) return custom
|
|
||||||
|
|
||||||
const builtin = BUILTIN[type]
|
|
||||||
if (builtin) return builtin
|
|
||||||
|
|
||||||
throw new Error(`Unknown workspace adaptor: ${type}`)
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function listAdaptors(projectID: ProjectID): Promise<WorkspaceAdaptorEntry[]> {
|
|
||||||
const builtin = await Promise.all(
|
|
||||||
Object.entries(BUILTIN).map(async ([type, adaptor]) => {
|
|
||||||
return {
|
|
||||||
type,
|
|
||||||
name: adaptor.name,
|
|
||||||
description: adaptor.description,
|
|
||||||
}
|
|
||||||
}),
|
|
||||||
)
|
|
||||||
const custom = [...(state.get(projectID)?.entries() ?? [])].map(([type, adaptor]) => ({
|
|
||||||
type,
|
|
||||||
name: adaptor.name,
|
|
||||||
description: adaptor.description,
|
|
||||||
}))
|
|
||||||
return [...builtin, ...custom]
|
|
||||||
}
|
|
||||||
|
|
||||||
// Plugins can be loaded per-project so we need to scope them. If you
|
|
||||||
// want to install a global one pass `ProjectID.global`
|
|
||||||
export function registerAdaptor(projectID: ProjectID, type: string, adaptor: WorkspaceAdaptor) {
|
|
||||||
const adaptors = state.get(projectID) ?? new Map<string, WorkspaceAdaptor>()
|
|
||||||
adaptors.set(type, adaptor)
|
|
||||||
state.set(projectID, adaptors)
|
|
||||||
}
|
|
||||||
@@ -17,12 +17,12 @@ export const WorkspaceInfo = Schema.Struct({
|
|||||||
.pipe(withStatics((s) => ({ zod: zod(s) })))
|
.pipe(withStatics((s) => ({ zod: zod(s) })))
|
||||||
export type WorkspaceInfo = DeepMutable<Schema.Schema.Type<typeof WorkspaceInfo>>
|
export type WorkspaceInfo = DeepMutable<Schema.Schema.Type<typeof WorkspaceInfo>>
|
||||||
|
|
||||||
export const WorkspaceAdaptorEntry = Schema.Struct({
|
export const WorkspaceAdapterEntry = Schema.Struct({
|
||||||
type: Schema.String,
|
type: Schema.String,
|
||||||
name: Schema.String,
|
name: Schema.String,
|
||||||
description: Schema.String,
|
description: Schema.String,
|
||||||
}).pipe(withStatics((s) => ({ zod: zod(s) })))
|
}).pipe(withStatics((s) => ({ zod: zod(s) })))
|
||||||
export type WorkspaceAdaptorEntry = Schema.Schema.Type<typeof WorkspaceAdaptorEntry>
|
export type WorkspaceAdapterEntry = Schema.Schema.Type<typeof WorkspaceAdapterEntry>
|
||||||
|
|
||||||
export type Target =
|
export type Target =
|
||||||
| {
|
| {
|
||||||
@@ -35,7 +35,7 @@ export type Target =
|
|||||||
headers?: HeadersInit
|
headers?: HeadersInit
|
||||||
}
|
}
|
||||||
|
|
||||||
export type WorkspaceAdaptor = {
|
export type WorkspaceAdapter = {
|
||||||
name: string
|
name: string
|
||||||
description: string
|
description: string
|
||||||
configure(info: WorkspaceInfo): WorkspaceInfo | Promise<WorkspaceInfo>
|
configure(info: WorkspaceInfo): WorkspaceInfo | Promise<WorkspaceInfo>
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ import { Filesystem } from "@/util/filesystem"
|
|||||||
import { ProjectID } from "@/project/schema"
|
import { ProjectID } from "@/project/schema"
|
||||||
import { Slug } from "@opencode-ai/core/util/slug"
|
import { Slug } from "@opencode-ai/core/util/slug"
|
||||||
import { WorkspaceTable } from "./workspace.sql"
|
import { WorkspaceTable } from "./workspace.sql"
|
||||||
import { getAdaptor } from "./adaptors"
|
import { getAdapter } from "./adapters"
|
||||||
import { type WorkspaceInfo, WorkspaceInfo as WorkspaceInfoSchema } from "./types"
|
import { type WorkspaceInfo, WorkspaceInfo as WorkspaceInfoSchema } from "./types"
|
||||||
import { WorkspaceID } from "./schema"
|
import { WorkspaceID } from "./schema"
|
||||||
import { Session } from "@/session/session"
|
import { Session } from "@/session/session"
|
||||||
@@ -25,6 +25,7 @@ import { SessionID } from "@/session/schema"
|
|||||||
import { errorData } from "@/util/error"
|
import { errorData } from "@/util/error"
|
||||||
import { waitEvent } from "./util"
|
import { waitEvent } from "./util"
|
||||||
import { WorkspaceContext } from "./workspace-context"
|
import { WorkspaceContext } from "./workspace-context"
|
||||||
|
import { EffectBridge } from "@/effect/bridge"
|
||||||
import { NonNegativeInt, withStatics } from "@/util/schema"
|
import { NonNegativeInt, withStatics } from "@/util/schema"
|
||||||
import { zod as effectZod, zodObject } from "@/util/effect-zod"
|
import { zod as effectZod, zodObject } from "@/util/effect-zod"
|
||||||
|
|
||||||
@@ -335,8 +336,8 @@ export const layer = Layer.effect(
|
|||||||
})
|
})
|
||||||
|
|
||||||
const syncWorkspaceLoop = Effect.fn("Workspace.syncWorkspaceLoop")(function* (space: Info) {
|
const syncWorkspaceLoop = Effect.fn("Workspace.syncWorkspaceLoop")(function* (space: Info) {
|
||||||
const adaptor = getAdaptor(space.projectID, space.type)
|
const adapter = getAdapter(space.projectID, space.type)
|
||||||
const target = yield* Effect.promise(() => Promise.resolve(adaptor.target(space)))
|
const target = yield* EffectBridge.fromPromise(() => adapter.target(space))
|
||||||
|
|
||||||
if (target.type === "local") return
|
if (target.type === "local") return
|
||||||
|
|
||||||
@@ -419,8 +420,8 @@ export const layer = Layer.effect(
|
|||||||
const startSync = Effect.fn("Workspace.startSync")(function* (space: Info) {
|
const startSync = Effect.fn("Workspace.startSync")(function* (space: Info) {
|
||||||
if (!Flag.OPENCODE_EXPERIMENTAL_WORKSPACES) return
|
if (!Flag.OPENCODE_EXPERIMENTAL_WORKSPACES) return
|
||||||
|
|
||||||
const adaptor = getAdaptor(space.projectID, space.type)
|
const adapter = getAdapter(space.projectID, space.type)
|
||||||
const target = yield* Effect.promise(() => Promise.resolve(adaptor.target(space)))
|
const target = yield* EffectBridge.fromPromise(() => adapter.target(space))
|
||||||
|
|
||||||
if (target.type === "local") {
|
if (target.type === "local") {
|
||||||
setStatus(space.id, (yield* Effect.promise(() => Filesystem.exists(target.directory))) ? "connected" : "error")
|
setStatus(space.id, (yield* Effect.promise(() => Filesystem.exists(target.directory))) ? "connected" : "error")
|
||||||
@@ -458,9 +459,9 @@ export const layer = Layer.effect(
|
|||||||
|
|
||||||
const create = Effect.fn("Workspace.create")(function* (input: CreateInput) {
|
const create = Effect.fn("Workspace.create")(function* (input: CreateInput) {
|
||||||
const id = WorkspaceID.ascending(input.id)
|
const id = WorkspaceID.ascending(input.id)
|
||||||
const adaptor = getAdaptor(input.projectID, input.type)
|
const adapter = getAdapter(input.projectID, input.type)
|
||||||
const config = yield* Effect.promise(() =>
|
const config = yield* EffectBridge.fromPromise(() =>
|
||||||
Promise.resolve(adaptor.configure({ ...input, id, name: Slug.create(), directory: null })),
|
adapter.configure({ ...input, id, name: Slug.create(), directory: null }),
|
||||||
)
|
)
|
||||||
|
|
||||||
const info: Info = {
|
const info: Info = {
|
||||||
@@ -496,7 +497,7 @@ export const layer = Layer.effect(
|
|||||||
OTEL_RESOURCE_ATTRIBUTES: process.env.OTEL_RESOURCE_ATTRIBUTES,
|
OTEL_RESOURCE_ATTRIBUTES: process.env.OTEL_RESOURCE_ATTRIBUTES,
|
||||||
}
|
}
|
||||||
|
|
||||||
yield* Effect.promise(() => adaptor.create(config, env))
|
yield* EffectBridge.fromPromise(() => adapter.create(config, env))
|
||||||
yield* Effect.all(
|
yield* Effect.all(
|
||||||
[
|
[
|
||||||
waitEvent({
|
waitEvent({
|
||||||
@@ -531,8 +532,8 @@ export const layer = Layer.effect(
|
|||||||
workspaceID: input.workspaceID,
|
workspaceID: input.workspaceID,
|
||||||
})
|
})
|
||||||
|
|
||||||
const adaptor = getAdaptor(space.projectID, space.type)
|
const adapter = getAdapter(space.projectID, space.type)
|
||||||
const target = yield* Effect.promise(() => Promise.resolve(adaptor.target(space)))
|
const target = yield* EffectBridge.fromPromise(() => adapter.target(space))
|
||||||
|
|
||||||
yield* sync.run(Session.Event.Updated, {
|
yield* sync.run(Session.Event.Updated, {
|
||||||
sessionID: input.sessionID,
|
sessionID: input.sessionID,
|
||||||
@@ -724,14 +725,14 @@ export const layer = Layer.effect(
|
|||||||
yield* stopSync(id)
|
yield* stopSync(id)
|
||||||
|
|
||||||
const info = fromRow(row)
|
const info = fromRow(row)
|
||||||
yield* Effect.catch(
|
yield* Effect.catchCause(
|
||||||
Effect.gen(function* () {
|
Effect.gen(function* () {
|
||||||
const adaptor = getAdaptor(info.projectID, row.type)
|
const adapter = getAdapter(info.projectID, row.type)
|
||||||
yield* Effect.tryPromise(() => Promise.resolve(adaptor.remove(info)))
|
yield* EffectBridge.fromPromise(() => adapter.remove(info))
|
||||||
}),
|
}),
|
||||||
() =>
|
() =>
|
||||||
Effect.sync(() => {
|
Effect.sync(() => {
|
||||||
log.error("adaptor not available when removing workspace", { type: row.type })
|
log.error("adapter not available when removing workspace", { type: row.type })
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -39,6 +39,7 @@ import { Command } from "@/command"
|
|||||||
import { Truncate } from "@/tool/truncate"
|
import { Truncate } from "@/tool/truncate"
|
||||||
import { ToolRegistry } from "@/tool/registry"
|
import { ToolRegistry } from "@/tool/registry"
|
||||||
import { Format } from "@/format"
|
import { Format } from "@/format"
|
||||||
|
import { InstanceStore } from "@/project/instance-store"
|
||||||
import { Project } from "@/project/project"
|
import { Project } from "@/project/project"
|
||||||
import { Vcs } from "@/project/vcs"
|
import { Vcs } from "@/project/vcs"
|
||||||
import { Workspace } from "@/control-plane/workspace"
|
import { Workspace } from "@/control-plane/workspace"
|
||||||
@@ -90,6 +91,7 @@ export const AppLayer = Layer.mergeAll(
|
|||||||
Truncate.defaultLayer,
|
Truncate.defaultLayer,
|
||||||
ToolRegistry.defaultLayer,
|
ToolRegistry.defaultLayer,
|
||||||
Format.defaultLayer,
|
Format.defaultLayer,
|
||||||
|
InstanceStore.defaultLayer,
|
||||||
Project.defaultLayer,
|
Project.defaultLayer,
|
||||||
Vcs.defaultLayer,
|
Vcs.defaultLayer,
|
||||||
Workspace.defaultLayer,
|
Workspace.defaultLayer,
|
||||||
|
|||||||
@@ -21,6 +21,25 @@ function restore<R>(instance: InstanceContext | undefined, workspace: WorkspaceI
|
|||||||
return fn()
|
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> {
|
export function make(): Effect.Effect<Shape> {
|
||||||
return Effect.gen(function* () {
|
return Effect.gen(function* () {
|
||||||
const ctx = yield* Effect.context()
|
const ctx = yield* Effect.context()
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { Effect, Layer, ManagedRuntime } from "effect"
|
import { Effect, Fiber, Layer, ManagedRuntime } from "effect"
|
||||||
import * as Context from "effect/Context"
|
import * as Context from "effect/Context"
|
||||||
import { Instance } from "@/project/instance"
|
import { Instance } from "@/project/instance"
|
||||||
import { LocalContext } from "@/util/local-context"
|
import { LocalContext } from "@/util/local-context"
|
||||||
@@ -24,15 +24,20 @@ 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> {
|
export function attach<A, E, R>(effect: Effect.Effect<A, E, R>): Effect.Effect<A, E, R> {
|
||||||
|
const workspace = WorkspaceContext.workspaceID
|
||||||
|
const instance = (() => {
|
||||||
try {
|
try {
|
||||||
return attachWith(effect, {
|
return Instance.current
|
||||||
instance: Instance.current,
|
|
||||||
workspace: WorkspaceContext.workspaceID,
|
|
||||||
})
|
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
if (!(err instanceof LocalContext.NotFound)) throw err
|
if (!(err instanceof LocalContext.NotFound)) throw err
|
||||||
}
|
}
|
||||||
return effect
|
})()
|
||||||
|
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),
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
export function makeRuntime<I, S, E>(service: Context.Service<I, S>, layer: Layer.Layer<I, E>) {
|
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 ignore from "ignore"
|
||||||
import path from "path"
|
import path from "path"
|
||||||
import { Global } from "@opencode-ai/core/global"
|
import { Global } from "@opencode-ai/core/global"
|
||||||
import { Instance } from "../project/instance"
|
import { containsPath } from "../project/instance-context"
|
||||||
import * as Log from "@opencode-ai/core/util/log"
|
import * as Log from "@opencode-ai/core/util/log"
|
||||||
import { Protected } from "./protected"
|
import { Protected } from "./protected"
|
||||||
import { Ripgrep } from "./ripgrep"
|
import { Ripgrep } from "./ripgrep"
|
||||||
@@ -507,7 +507,7 @@ export const layer = Layer.effect(
|
|||||||
const ctx = yield* InstanceState.context
|
const ctx = yield* InstanceState.context
|
||||||
const full = path.join(ctx.directory, file)
|
const full = path.join(ctx.directory, file)
|
||||||
|
|
||||||
if (!Instance.containsPath(full, ctx)) {
|
if (!containsPath(full, ctx)) {
|
||||||
throw new Error("Access denied: path escapes project directory")
|
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
|
const resolved = dir ? path.join(ctx.directory, dir) : ctx.directory
|
||||||
if (!Instance.containsPath(resolved, ctx)) {
|
if (!containsPath(resolved, ctx)) {
|
||||||
throw new Error("Access denied: path escapes project directory")
|
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 { spawn as lspspawn } from "./launch"
|
||||||
import { Effect, Layer, Context, Schema } from "effect"
|
import { Effect, Layer, Context, Schema } from "effect"
|
||||||
import { InstanceState } from "@/effect/instance-state"
|
import { InstanceState } from "@/effect/instance-state"
|
||||||
import { AppFileSystem } from "@opencode-ai/core/filesystem"
|
import { containsPath } from "@/project/instance-context"
|
||||||
import { NonNegativeInt, withStatics } from "@/util/schema"
|
import { NonNegativeInt, withStatics } from "@/util/schema"
|
||||||
import { zod, ZodOverride } from "@/util/effect-zod"
|
import { zod, ZodOverride } from "@/util/effect-zod"
|
||||||
|
|
||||||
@@ -221,12 +221,7 @@ export const layer = Layer.effect(
|
|||||||
|
|
||||||
const getClients = Effect.fnUntraced(function* (file: string) {
|
const getClients = Effect.fnUntraced(function* (file: string) {
|
||||||
const ctx = yield* InstanceState.context
|
const ctx = yield* InstanceState.context
|
||||||
if (
|
if (!containsPath(file, ctx)) return [] as LSPClient.Info[]
|
||||||
!AppFileSystem.contains(ctx.directory, file) &&
|
|
||||||
(ctx.worktree === "/" || !AppFileSystem.contains(ctx.worktree, file))
|
|
||||||
) {
|
|
||||||
return [] as LSPClient.Info[]
|
|
||||||
}
|
|
||||||
const s = yield* InstanceState.get(state)
|
const s = yield* InstanceState.get(state)
|
||||||
return yield* Effect.promise(async () => {
|
return yield* Effect.promise(async () => {
|
||||||
const extension = path.parse(file).ext || file
|
const extension = path.parse(file).ext || file
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ import type {
|
|||||||
PluginInput,
|
PluginInput,
|
||||||
Plugin as PluginInstance,
|
Plugin as PluginInstance,
|
||||||
PluginModule,
|
PluginModule,
|
||||||
WorkspaceAdaptor as PluginWorkspaceAdaptor,
|
WorkspaceAdapter as PluginWorkspaceAdapter,
|
||||||
} from "@opencode-ai/plugin"
|
} from "@opencode-ai/plugin"
|
||||||
import { Config } from "@/config/config"
|
import { Config } from "@/config/config"
|
||||||
import { Bus } from "../bus"
|
import { Bus } from "../bus"
|
||||||
@@ -24,8 +24,8 @@ import { InstanceState } from "@/effect/instance-state"
|
|||||||
import { errorMessage } from "@/util/error"
|
import { errorMessage } from "@/util/error"
|
||||||
import { PluginLoader } from "./loader"
|
import { PluginLoader } from "./loader"
|
||||||
import { parsePluginSpecifier, readPluginId, readV1Plugin, resolvePluginId } from "./shared"
|
import { parsePluginSpecifier, readPluginId, readV1Plugin, resolvePluginId } from "./shared"
|
||||||
import { registerAdaptor } from "@/control-plane/adaptors"
|
import { registerAdapter } from "@/control-plane/adapters"
|
||||||
import type { WorkspaceAdaptor } from "@/control-plane/types"
|
import type { WorkspaceAdapter } from "@/control-plane/types"
|
||||||
|
|
||||||
const log = Log.create({ service: "plugin" })
|
const log = Log.create({ service: "plugin" })
|
||||||
|
|
||||||
@@ -138,8 +138,8 @@ export const layer = Layer.effect(
|
|||||||
worktree: ctx.worktree,
|
worktree: ctx.worktree,
|
||||||
directory: ctx.directory,
|
directory: ctx.directory,
|
||||||
experimental_workspace: {
|
experimental_workspace: {
|
||||||
register(type: string, adaptor: PluginWorkspaceAdaptor) {
|
register(type: string, adapter: PluginWorkspaceAdapter) {
|
||||||
registerAdaptor(ctx.project.id, type, adaptor as WorkspaceAdaptor)
|
registerAdapter(ctx.project.id, type, adapter as WorkspaceAdapter)
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
get serverUrl(): URL {
|
get serverUrl(): URL {
|
||||||
|
|||||||
@@ -8,37 +8,71 @@ import * as Vcs from "./vcs"
|
|||||||
import { Bus } from "../bus"
|
import { Bus } from "../bus"
|
||||||
import { Command } from "../command"
|
import { Command } from "../command"
|
||||||
import { InstanceState } from "@/effect/instance-state"
|
import { InstanceState } from "@/effect/instance-state"
|
||||||
import * as Log from "@opencode-ai/core/util/log"
|
|
||||||
import { FileWatcher } from "@/file/watcher"
|
import { FileWatcher } from "@/file/watcher"
|
||||||
import { ShareNext } from "@/share/share-next"
|
import { ShareNext } from "@/share/share-next"
|
||||||
import * as Effect from "effect/Effect"
|
import { Context, Effect, Layer } from "effect"
|
||||||
import { Config } from "@/config/config"
|
import { Config } from "@/config/config"
|
||||||
|
|
||||||
export const InstanceBootstrap = Effect.gen(function* () {
|
export interface Interface {
|
||||||
|
readonly run: Effect.Effect<void>
|
||||||
|
}
|
||||||
|
|
||||||
|
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
|
const ctx = yield* InstanceState.context
|
||||||
Log.Default.info("bootstrapping", { directory: ctx.directory })
|
yield* Effect.logInfo("bootstrapping", { directory: ctx.directory })
|
||||||
// everything depends on config so eager load it for nice traces
|
// everything depends on config so eager load it for nice traces
|
||||||
yield* Config.Service.use((svc) => svc.get())
|
yield* config.get()
|
||||||
// Plugin can mutate config so it has to be initialized before anything else.
|
// Plugin can mutate config so it has to be initialized before anything else.
|
||||||
yield* Plugin.Service.use((svc) => svc.init())
|
yield* plugin.init()
|
||||||
yield* Effect.all(
|
yield* Effect.all(
|
||||||
[
|
[lsp, shareNext, format, file, fileWatcher, vcs, snapshot].map((s) => Effect.forkDetach(s.init())),
|
||||||
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"))
|
).pipe(Effect.withSpan("InstanceBootstrap.init"))
|
||||||
|
|
||||||
const projectID = ctx.project.id
|
const projectID = ctx.project.id
|
||||||
yield* Bus.Service.use((svc) =>
|
yield* bus.subscribeCallback(Command.Event.Executed, async (payload) => {
|
||||||
svc.subscribeCallback(Command.Event.Executed, async (payload) => {
|
|
||||||
if (payload.properties.name === Command.Default.INIT) {
|
if (payload.properties.name === Command.Default.INIT) {
|
||||||
Project.setInitialized(projectID)
|
Project.setInitialized(projectID)
|
||||||
}
|
}
|
||||||
|
})
|
||||||
|
}).pipe(Effect.withSpan("InstanceBootstrap"))
|
||||||
|
|
||||||
|
return Service.of({ run })
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
}).pipe(Effect.withSpan("InstanceBootstrap"))
|
|
||||||
|
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"
|
||||||
|
|||||||
@@ -0,0 +1,24 @@
|
|||||||
|
import { LocalContext } from "@/util/local-context"
|
||||||
|
import { AppFileSystem } from "@opencode-ai/core/filesystem"
|
||||||
|
import type * as Project from "./project"
|
||||||
|
|
||||||
|
export interface InstanceContext {
|
||||||
|
directory: string
|
||||||
|
worktree: string
|
||||||
|
project: Project.Info
|
||||||
|
}
|
||||||
|
|
||||||
|
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)
|
||||||
|
}
|
||||||
@@ -0,0 +1,207 @@
|
|||||||
|
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 { 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 * as Project from "./project"
|
||||||
|
|
||||||
|
export interface LoadInput<R = never> {
|
||||||
|
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>
|
||||||
|
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 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") {}
|
||||||
|
|
||||||
|
interface Entry {
|
||||||
|
readonly deferred: Deferred.Deferred<InstanceContext>
|
||||||
|
}
|
||||||
|
|
||||||
|
export const layer: Layer.Layer<Service, never, Project.Service> = Layer.effect(
|
||||||
|
Service,
|
||||||
|
Effect.gen(function* () {
|
||||||
|
const project = yield* Project.Service
|
||||||
|
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
|
||||||
|
? {
|
||||||
|
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"))
|
||||||
|
|
||||||
|
const removeEntry = (directory: string, entry: Entry) =>
|
||||||
|
Effect.sync(() => {
|
||||||
|
if (cache.get(directory) !== entry) return false
|
||||||
|
cache.delete(directory)
|
||||||
|
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 emitDisposed = (input: { directory: string; project?: string }) =>
|
||||||
|
Effect.sync(() =>
|
||||||
|
GlobalBus.emit("event", {
|
||||||
|
directory: input.directory,
|
||||||
|
project: input.project,
|
||||||
|
workspace: WorkspaceContext.workspaceID,
|
||||||
|
payload: {
|
||||||
|
type: "server.instance.disposed",
|
||||||
|
properties: {
|
||||||
|
directory: input.directory,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
|
||||||
|
const disposeContext = Effect.fn("InstanceStore.disposeContext")(function* (ctx: InstanceContext) {
|
||||||
|
yield* Effect.logInfo("disposing instance", { directory: ctx.directory })
|
||||||
|
yield* Effect.promise(() => runDisposers(ctx.directory))
|
||||||
|
yield* emitDisposed({ directory: ctx.directory, project: ctx.project.id })
|
||||||
|
})
|
||||||
|
|
||||||
|
const disposeEntry = Effect.fnUntraced(function* (directory: string, entry: Entry, ctx: InstanceContext) {
|
||||||
|
if (cache.get(directory) !== entry) return false
|
||||||
|
yield* disposeContext(ctx)
|
||||||
|
if (cache.get(directory) !== entry) return false
|
||||||
|
cache.delete(directory)
|
||||||
|
return true
|
||||||
|
})
|
||||||
|
|
||||||
|
const load = <R>(input: LoadInput<R>): Effect.Effect<InstanceContext, never, R> => {
|
||||||
|
const directory = AppFileSystem.resolve(input.directory)
|
||||||
|
return Effect.uninterruptibleMask((restore) =>
|
||||||
|
Effect.gen(function* () {
|
||||||
|
const existing = cache.get(directory)
|
||||||
|
if (existing) return yield* restore(Deferred.await(existing.deferred))
|
||||||
|
|
||||||
|
const entry: Entry = { deferred: Deferred.makeUnsafe<InstanceContext>() }
|
||||||
|
cache.set(directory, entry)
|
||||||
|
yield* Effect.gen(function* () {
|
||||||
|
yield* Effect.logInfo("creating instance", { directory })
|
||||||
|
yield* completeLoad(directory, input, entry)
|
||||||
|
}).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 directory = AppFileSystem.resolve(input.directory)
|
||||||
|
return Effect.uninterruptibleMask((restore) =>
|
||||||
|
Effect.gen(function* () {
|
||||||
|
const previous = cache.get(directory)
|
||||||
|
const entry: Entry = { deferred: Deferred.makeUnsafe<InstanceContext>() }
|
||||||
|
cache.set(directory, entry)
|
||||||
|
yield* Effect.gen(function* () {
|
||||||
|
yield* Effect.logInfo("reloading instance", { directory })
|
||||||
|
if (previous) {
|
||||||
|
yield* Deferred.await(previous.deferred).pipe(Effect.ignore)
|
||||||
|
yield* Effect.promise(() => runDisposers(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)
|
||||||
|
if (!entry) return yield* disposeContext(ctx)
|
||||||
|
|
||||||
|
const exit = yield* Deferred.await(entry.deferred).pipe(Effect.exit)
|
||||||
|
if (Exit.isFailure(exit)) return yield* removeEntry(ctx.directory, entry).pipe(Effect.asVoid)
|
||||||
|
if (exit.value !== ctx) return
|
||||||
|
yield* disposeEntry(ctx.directory, entry, ctx).pipe(Effect.asVoid)
|
||||||
|
})
|
||||||
|
|
||||||
|
const disposeAllOnce = Effect.fnUntraced(function* () {
|
||||||
|
yield* Effect.logInfo("disposing all instances")
|
||||||
|
yield* Effect.forEach(
|
||||||
|
[...cache.entries()],
|
||||||
|
(item) =>
|
||||||
|
Effect.gen(function* () {
|
||||||
|
const exit = yield* Deferred.await(item[1].deferred).pipe(Effect.exit)
|
||||||
|
if (Exit.isFailure(exit)) {
|
||||||
|
yield* Effect.logWarning("instance dispose failed", { key: item[0], cause: exit.cause })
|
||||||
|
yield* removeEntry(item[0], item[1])
|
||||||
|
return
|
||||||
|
}
|
||||||
|
yield* disposeEntry(item[0], item[1], exit.value)
|
||||||
|
}),
|
||||||
|
{ discard: true },
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
const cachedDisposeAll = yield* Effect.cachedWithTTL(disposeAllOnce(), Duration.zero)
|
||||||
|
const disposeAll = Effect.fn("InstanceStore.disposeAll")(function* () {
|
||||||
|
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({
|
||||||
|
load,
|
||||||
|
reload,
|
||||||
|
dispose,
|
||||||
|
disposeAll,
|
||||||
|
provide,
|
||||||
|
})
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
|
||||||
|
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,77 +1,16 @@
|
|||||||
import { GlobalBus } from "@/bus/global"
|
import { Effect } from "effect"
|
||||||
import { disposeInstance } from "@/effect/instance-registry"
|
import { context, type InstanceContext } from "./instance-context"
|
||||||
import { makeRuntime } from "@/effect/run-service"
|
import { InstanceStore } from "./instance-store"
|
||||||
import { AppFileSystem } from "@opencode-ai/core/filesystem"
|
|
||||||
import { iife } from "@/util/iife"
|
|
||||||
import * as Log from "@opencode-ai/core/util/log"
|
|
||||||
import { LocalContext } from "@/util/local-context"
|
|
||||||
import * as Project from "./project"
|
|
||||||
import { WorkspaceContext } from "@/control-plane/workspace-context"
|
|
||||||
|
|
||||||
export interface InstanceContext {
|
export type { InstanceContext } from "./instance-context"
|
||||||
directory: string
|
export type { LoadInput } from "./instance-store"
|
||||||
worktree: string
|
|
||||||
project: Project.Info
|
|
||||||
}
|
|
||||||
|
|
||||||
const context = LocalContext.create<InstanceContext>("instance")
|
|
||||||
const cache = new Map<string, Promise<InstanceContext>>()
|
|
||||||
const project = makeRuntime(Project.Service, Project.defaultLayer)
|
|
||||||
|
|
||||||
const disposal = {
|
|
||||||
all: undefined as Promise<void> | undefined,
|
|
||||||
}
|
|
||||||
|
|
||||||
function boot(input: { directory: string; init?: () => Promise<any>; worktree?: string; project?: Project.Info }) {
|
|
||||||
return iife(async () => {
|
|
||||||
const ctx =
|
|
||||||
input.project && input.worktree
|
|
||||||
? {
|
|
||||||
directory: input.directory,
|
|
||||||
worktree: input.worktree,
|
|
||||||
project: input.project,
|
|
||||||
}
|
|
||||||
: await project
|
|
||||||
.runPromise((svc) => svc.fromDirectory(input.directory))
|
|
||||||
.then(({ project, sandbox }) => ({
|
|
||||||
directory: input.directory,
|
|
||||||
worktree: sandbox,
|
|
||||||
project,
|
|
||||||
}))
|
|
||||||
await context.provide(ctx, async () => {
|
|
||||||
await input.init?.()
|
|
||||||
})
|
|
||||||
return ctx
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
function track(directory: string, next: Promise<InstanceContext>) {
|
|
||||||
const task = next.catch((error) => {
|
|
||||||
if (cache.get(directory) === task) cache.delete(directory)
|
|
||||||
throw error
|
|
||||||
})
|
|
||||||
cache.set(directory, task)
|
|
||||||
return task
|
|
||||||
}
|
|
||||||
|
|
||||||
export const Instance = {
|
export const Instance = {
|
||||||
async provide<R>(input: { directory: string; init?: () => Promise<any>; fn: () => R }): Promise<R> {
|
async provide<R>(input: { directory: string; init?: Effect.Effect<void>; fn: () => R }): Promise<R> {
|
||||||
const directory = AppFileSystem.resolve(input.directory)
|
const ctx = await InstanceStore.runtime.runPromise((store) =>
|
||||||
let existing = cache.get(directory)
|
store.load({ directory: input.directory, init: input.init }),
|
||||||
if (!existing) {
|
|
||||||
Log.Default.info("creating instance", { directory })
|
|
||||||
existing = track(
|
|
||||||
directory,
|
|
||||||
boot({
|
|
||||||
directory,
|
|
||||||
init: input.init,
|
|
||||||
}),
|
|
||||||
)
|
)
|
||||||
}
|
return context.provide(ctx, async () => input.fn())
|
||||||
const ctx = await existing
|
|
||||||
return context.provide(ctx, async () => {
|
|
||||||
return input.fn()
|
|
||||||
})
|
|
||||||
},
|
},
|
||||||
get current() {
|
get current() {
|
||||||
return context.use()
|
return context.use()
|
||||||
@@ -86,19 +25,6 @@ export const Instance = {
|
|||||||
return context.use().project
|
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
|
* Captures the current instance ALS context and returns a wrapper that
|
||||||
* restores it when called. Use this for callbacks that fire outside the
|
* restores it when called. Use this for callbacks that fire outside the
|
||||||
@@ -116,75 +42,17 @@ export const Instance = {
|
|||||||
restore<R>(ctx: InstanceContext, fn: () => R): R {
|
restore<R>(ctx: InstanceContext, fn: () => R): R {
|
||||||
return context.provide(ctx, fn)
|
return context.provide(ctx, fn)
|
||||||
},
|
},
|
||||||
async reload(input: { directory: string; init?: () => Promise<any>; project?: Project.Info; worktree?: string }) {
|
// followup: `reload` survives because `test/server/project-init-git.test.ts`
|
||||||
const directory = AppFileSystem.resolve(input.directory)
|
// spies on this exact method. Once that test asserts on `InstanceStore.reloadInstance`
|
||||||
Log.Default.info("reloading instance", { directory })
|
// (or moves to an Effect runtime), this wrapper can drop.
|
||||||
await disposeInstance(directory)
|
async reload(input: InstanceStore.LoadInput) {
|
||||||
cache.delete(directory)
|
return InstanceStore.reloadInstance(input)
|
||||||
const next = track(directory, boot({ ...input, directory }))
|
|
||||||
|
|
||||||
GlobalBus.emit("event", {
|
|
||||||
directory,
|
|
||||||
project: input.project?.id,
|
|
||||||
workspace: WorkspaceContext.workspaceID,
|
|
||||||
payload: {
|
|
||||||
type: "server.instance.disposed",
|
|
||||||
properties: {
|
|
||||||
directory,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
})
|
|
||||||
|
|
||||||
return await next
|
|
||||||
},
|
},
|
||||||
|
// followup: `dispose` survives for legacy fixtures that read `Instance.current`
|
||||||
|
// out of ALS (e.g. `test/fixture/fixture.ts` `provideTmpdirInstance`,
|
||||||
|
// `test/question/question.test.ts` cancellation tests). Convert those to call
|
||||||
|
// `InstanceStore.disposeInstance(ctx)` directly once `Instance.provide` is gone.
|
||||||
async dispose() {
|
async dispose() {
|
||||||
const directory = Instance.directory
|
return InstanceStore.disposeInstance(Instance.current)
|
||||||
const project = Instance.project
|
|
||||||
Log.Default.info("disposing instance", { directory })
|
|
||||||
await disposeInstance(directory)
|
|
||||||
cache.delete(directory)
|
|
||||||
|
|
||||||
GlobalBus.emit("event", {
|
|
||||||
directory,
|
|
||||||
project: project.id,
|
|
||||||
workspace: WorkspaceContext.workspaceID,
|
|
||||||
payload: {
|
|
||||||
type: "server.instance.disposed",
|
|
||||||
properties: {
|
|
||||||
directory,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
})
|
|
||||||
},
|
|
||||||
async disposeAll() {
|
|
||||||
if (disposal.all) return disposal.all
|
|
||||||
|
|
||||||
disposal.all = iife(async () => {
|
|
||||||
Log.Default.info("disposing all instances")
|
|
||||||
const entries = [...cache.entries()]
|
|
||||||
for (const [key, value] of entries) {
|
|
||||||
if (cache.get(key) !== value) continue
|
|
||||||
|
|
||||||
const ctx = await value.catch((error) => {
|
|
||||||
Log.Default.warn("instance dispose failed", { key, error })
|
|
||||||
return undefined
|
|
||||||
})
|
|
||||||
|
|
||||||
if (!ctx) {
|
|
||||||
if (cache.get(key) === value) cache.delete(key)
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
if (cache.get(key) !== value) continue
|
|
||||||
|
|
||||||
await context.provide(ctx, async () => {
|
|
||||||
await Instance.dispose()
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}).finally(() => {
|
|
||||||
disposal.all = undefined
|
|
||||||
})
|
|
||||||
|
|
||||||
return disposal.all
|
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,10 +2,10 @@ import { Hono } from "hono"
|
|||||||
import { describeRoute, resolver, validator } from "hono-openapi"
|
import { describeRoute, resolver, validator } from "hono-openapi"
|
||||||
import z from "zod"
|
import z from "zod"
|
||||||
import { Effect } from "effect"
|
import { Effect } from "effect"
|
||||||
import { listAdaptors } from "@/control-plane/adaptors"
|
import { listAdapters } from "@/control-plane/adapters"
|
||||||
import { Workspace } from "@/control-plane/workspace"
|
import { Workspace } from "@/control-plane/workspace"
|
||||||
import { AppRuntime } from "@/effect/app-runtime"
|
import { AppRuntime } from "@/effect/app-runtime"
|
||||||
import { WorkspaceAdaptorEntry } from "@/control-plane/types"
|
import { WorkspaceAdapterEntry } from "@/control-plane/types"
|
||||||
import { zodObject } from "@/util/effect-zod"
|
import { zodObject } from "@/util/effect-zod"
|
||||||
import { Instance } from "@/project/instance"
|
import { Instance } from "@/project/instance"
|
||||||
import { errors } from "../../error"
|
import { errors } from "../../error"
|
||||||
@@ -18,24 +18,24 @@ const log = Log.create({ service: "server.workspace" })
|
|||||||
export const WorkspaceRoutes = lazy(() =>
|
export const WorkspaceRoutes = lazy(() =>
|
||||||
new Hono()
|
new Hono()
|
||||||
.get(
|
.get(
|
||||||
"/adaptor",
|
"/adapter",
|
||||||
describeRoute({
|
describeRoute({
|
||||||
summary: "List workspace adaptors",
|
summary: "List workspace adapters",
|
||||||
description: "List all available workspace adaptors for the current project.",
|
description: "List all available workspace adapters for the current project.",
|
||||||
operationId: "experimental.workspace.adaptor.list",
|
operationId: "experimental.workspace.adapter.list",
|
||||||
responses: {
|
responses: {
|
||||||
200: {
|
200: {
|
||||||
description: "Workspace adaptors",
|
description: "Workspace adapters",
|
||||||
content: {
|
content: {
|
||||||
"application/json": {
|
"application/json": {
|
||||||
schema: resolver(z.array(zodObject(WorkspaceAdaptorEntry))),
|
schema: resolver(z.array(zodObject(WorkspaceAdapterEntry))),
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
async (c) => {
|
async (c) => {
|
||||||
return c.json(await listAdaptors(Instance.project.id))
|
return c.json(await listAdapters(Instance.project.id))
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
.post(
|
.post(
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ import { SyncEvent } from "@/sync"
|
|||||||
import { GlobalBus } from "@/bus/global"
|
import { GlobalBus } from "@/bus/global"
|
||||||
import { AppRuntime } from "@/effect/app-runtime"
|
import { AppRuntime } from "@/effect/app-runtime"
|
||||||
import { AsyncQueue } from "@/util/queue"
|
import { AsyncQueue } from "@/util/queue"
|
||||||
import { Instance } from "../../project/instance"
|
import { InstanceStore } from "../../project/instance-store"
|
||||||
import { Installation } from "@/installation"
|
import { Installation } from "@/installation"
|
||||||
import { InstallationVersion } from "@opencode-ai/core/installation/version"
|
import { InstallationVersion } from "@opencode-ai/core/installation/version"
|
||||||
import * as Log from "@opencode-ai/core/util/log"
|
import * as Log from "@opencode-ai/core/util/log"
|
||||||
@@ -200,7 +200,7 @@ export const GlobalRoutes = lazy(() =>
|
|||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
async (c) => {
|
async (c) => {
|
||||||
await Instance.disposeAll()
|
await InstanceStore.disposeAllInstances()
|
||||||
GlobalBus.emit("event", {
|
GlobalBus.emit("event", {
|
||||||
directory: "global",
|
directory: "global",
|
||||||
payload: {
|
payload: {
|
||||||
|
|||||||
@@ -0,0 +1,8 @@
|
|||||||
|
# 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,5 @@
|
|||||||
import { Workspace } from "@/control-plane/workspace"
|
import { Workspace } from "@/control-plane/workspace"
|
||||||
import { WorkspaceAdaptorEntry } from "@/control-plane/types"
|
import { WorkspaceAdapterEntry } from "@/control-plane/types"
|
||||||
import { NonNegativeInt } from "@/util/schema"
|
import { NonNegativeInt } from "@/util/schema"
|
||||||
import { Schema, Struct } from "effect"
|
import { Schema, Struct } from "effect"
|
||||||
import { HttpApi, HttpApiEndpoint, HttpApiError, HttpApiGroup, OpenApi } from "effect/unstable/httpapi"
|
import { HttpApi, HttpApiEndpoint, HttpApiError, HttpApiGroup, OpenApi } from "effect/unstable/httpapi"
|
||||||
@@ -9,14 +9,17 @@ import { WorkspaceRoutingMiddleware } from "../middleware/workspace-routing"
|
|||||||
import { described } from "./metadata"
|
import { described } from "./metadata"
|
||||||
|
|
||||||
const root = "/experimental/workspace"
|
const root = "/experimental/workspace"
|
||||||
export const CreatePayload = Schema.Struct(Struct.omit(Workspace.CreateInput.fields, ["projectID"]))
|
export const CreatePayload = Schema.Struct({
|
||||||
|
...Struct.omit(Workspace.CreateInput.fields, ["projectID", "extra"]),
|
||||||
|
extra: Schema.optional(Workspace.CreateInput.fields.extra),
|
||||||
|
})
|
||||||
export const SessionRestorePayload = Schema.Struct(Struct.omit(Workspace.SessionRestoreInput.fields, ["workspaceID"]))
|
export const SessionRestorePayload = Schema.Struct(Struct.omit(Workspace.SessionRestoreInput.fields, ["workspaceID"]))
|
||||||
export const SessionRestoreResponse = Schema.Struct({
|
export const SessionRestoreResponse = Schema.Struct({
|
||||||
total: NonNegativeInt,
|
total: NonNegativeInt,
|
||||||
})
|
})
|
||||||
|
|
||||||
export const WorkspacePaths = {
|
export const WorkspacePaths = {
|
||||||
adaptors: `${root}/adaptor`,
|
adapters: `${root}/adapter`,
|
||||||
list: root,
|
list: root,
|
||||||
status: `${root}/status`,
|
status: `${root}/status`,
|
||||||
remove: `${root}/:id`,
|
remove: `${root}/:id`,
|
||||||
@@ -27,13 +30,13 @@ export const WorkspaceApi = HttpApi.make("workspace")
|
|||||||
.add(
|
.add(
|
||||||
HttpApiGroup.make("workspace")
|
HttpApiGroup.make("workspace")
|
||||||
.add(
|
.add(
|
||||||
HttpApiEndpoint.get("adaptors", WorkspacePaths.adaptors, {
|
HttpApiEndpoint.get("adapters", WorkspacePaths.adapters, {
|
||||||
success: described(Schema.Array(WorkspaceAdaptorEntry), "Workspace adaptors"),
|
success: described(Schema.Array(WorkspaceAdapterEntry), "Workspace adapters"),
|
||||||
}).annotateMerge(
|
}).annotateMerge(
|
||||||
OpenApi.annotations({
|
OpenApi.annotations({
|
||||||
identifier: "experimental.workspace.adaptor.list",
|
identifier: "experimental.workspace.adapter.list",
|
||||||
summary: "List workspace adaptors",
|
summary: "List workspace adapters",
|
||||||
description: "List all available workspace adaptors for the current project.",
|
description: "List all available workspace adapters for the current project.",
|
||||||
}),
|
}),
|
||||||
),
|
),
|
||||||
HttpApiEndpoint.get("list", WorkspacePaths.list, {
|
HttpApiEndpoint.get("list", WorkspacePaths.list, {
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { Config } from "@/config/config"
|
import { Config } from "@/config/config"
|
||||||
import { GlobalBus, type GlobalEvent as GlobalBusEvent } from "@/bus/global"
|
import { GlobalBus, type GlobalEvent as GlobalBusEvent } from "@/bus/global"
|
||||||
import { Installation } from "@/installation"
|
import { Installation } from "@/installation"
|
||||||
import { Instance } from "@/project/instance"
|
import { InstanceStore } from "@/project/instance-store"
|
||||||
import { InstallationVersion } from "@opencode-ai/core/installation/version"
|
import { InstallationVersion } from "@opencode-ai/core/installation/version"
|
||||||
import * as Log from "@opencode-ai/core/util/log"
|
import * as Log from "@opencode-ai/core/util/log"
|
||||||
import { Effect, Queue, Schema } from "effect"
|
import { Effect, Queue, Schema } from "effect"
|
||||||
@@ -68,6 +68,7 @@ export const globalHandlers = HttpApiBuilder.group(RootHttpApi, "global", (handl
|
|||||||
Effect.gen(function* () {
|
Effect.gen(function* () {
|
||||||
const config = yield* Config.Service
|
const config = yield* Config.Service
|
||||||
const installation = yield* Installation.Service
|
const installation = yield* Installation.Service
|
||||||
|
const store = yield* InstanceStore.Service
|
||||||
|
|
||||||
const health = Effect.fn("GlobalHttpApi.health")(function* () {
|
const health = Effect.fn("GlobalHttpApi.health")(function* () {
|
||||||
return { healthy: true as const, version: InstallationVersion }
|
return { healthy: true as const, version: InstallationVersion }
|
||||||
@@ -86,7 +87,7 @@ export const globalHandlers = HttpApiBuilder.group(RootHttpApi, "global", (handl
|
|||||||
})
|
})
|
||||||
|
|
||||||
const dispose = Effect.fn("GlobalHttpApi.dispose")(function* () {
|
const dispose = Effect.fn("GlobalHttpApi.dispose")(function* () {
|
||||||
yield* Effect.promise(() => Instance.disposeAll())
|
yield* store.disposeAll()
|
||||||
GlobalBus.emit("event", {
|
GlobalBus.emit("event", {
|
||||||
directory: "global",
|
directory: "global",
|
||||||
payload: { type: "global.disposed", properties: {} },
|
payload: { type: "global.disposed", properties: {} },
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
import { AppRuntime } from "@/effect/app-runtime"
|
import { AppRuntime } from "@/effect/app-runtime"
|
||||||
import * as InstanceState from "@/effect/instance-state"
|
import * as InstanceState from "@/effect/instance-state"
|
||||||
import { InstanceBootstrap } from "@/project/bootstrap"
|
|
||||||
import { Project } from "@/project/project"
|
import { Project } from "@/project/project"
|
||||||
import { ProjectID } from "@/project/schema"
|
import { ProjectID } from "@/project/schema"
|
||||||
import { Effect } from "effect"
|
import { Effect } from "effect"
|
||||||
@@ -29,7 +28,6 @@ export const projectHandlers = HttpApiBuilder.group(InstanceHttpApi, "project",
|
|||||||
directory: ctx.directory,
|
directory: ctx.directory,
|
||||||
worktree: ctx.directory,
|
worktree: ctx.directory,
|
||||||
project: next,
|
project: next,
|
||||||
init: () => AppRuntime.runPromise(InstanceBootstrap),
|
|
||||||
})
|
})
|
||||||
return next
|
return next
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { listAdaptors } from "@/control-plane/adaptors"
|
import { listAdapters } from "@/control-plane/adapters"
|
||||||
import { Workspace } from "@/control-plane/workspace"
|
import { Workspace } from "@/control-plane/workspace"
|
||||||
import * as InstanceState from "@/effect/instance-state"
|
import * as InstanceState from "@/effect/instance-state"
|
||||||
import { Effect } from "effect"
|
import { Effect } from "effect"
|
||||||
@@ -10,9 +10,9 @@ export const workspaceHandlers = HttpApiBuilder.group(InstanceHttpApi, "workspac
|
|||||||
Effect.gen(function* () {
|
Effect.gen(function* () {
|
||||||
const workspace = yield* Workspace.Service
|
const workspace = yield* Workspace.Service
|
||||||
|
|
||||||
const adaptors = Effect.fn("WorkspaceHttpApi.adaptors")(function* () {
|
const adapters = Effect.fn("WorkspaceHttpApi.adapters")(function* () {
|
||||||
const instance = yield* InstanceState.context
|
const instance = yield* InstanceState.context
|
||||||
return yield* Effect.promise(() => listAdaptors(instance.project.id))
|
return yield* Effect.promise(() => listAdapters(instance.project.id))
|
||||||
})
|
})
|
||||||
|
|
||||||
const list = Effect.fn("WorkspaceHttpApi.list")(function* () {
|
const list = Effect.fn("WorkspaceHttpApi.list")(function* () {
|
||||||
@@ -24,6 +24,7 @@ export const workspaceHandlers = HttpApiBuilder.group(InstanceHttpApi, "workspac
|
|||||||
return yield* workspace
|
return yield* workspace
|
||||||
.create({
|
.create({
|
||||||
...ctx.payload,
|
...ctx.payload,
|
||||||
|
extra: ctx.payload.extra ?? null,
|
||||||
projectID: instance.project.id,
|
projectID: instance.project.id,
|
||||||
})
|
})
|
||||||
.pipe(Effect.mapError(() => new HttpApiError.BadRequest({})))
|
.pipe(Effect.mapError(() => new HttpApiError.BadRequest({})))
|
||||||
@@ -51,7 +52,7 @@ export const workspaceHandlers = HttpApiBuilder.group(InstanceHttpApi, "workspac
|
|||||||
})
|
})
|
||||||
|
|
||||||
return handlers
|
return handlers
|
||||||
.handle("adaptors", adaptors)
|
.handle("adapters", adapters)
|
||||||
.handle("list", list)
|
.handle("list", list)
|
||||||
.handle("create", create)
|
.handle("create", create)
|
||||||
.handle("status", status)
|
.handle("status", status)
|
||||||
|
|||||||
@@ -1,13 +1,13 @@
|
|||||||
import type { WorkspaceID } from "@/control-plane/schema"
|
import { EffectBridge } from "@/effect/bridge"
|
||||||
import { WorkspaceContext } from "@/control-plane/workspace-context"
|
import type { InstanceContext } from "@/project/instance"
|
||||||
import { WorkspaceRef } from "@/effect/instance-ref"
|
import { InstanceStore } from "@/project/instance-store"
|
||||||
import { Instance, type InstanceContext } from "@/project/instance"
|
|
||||||
import { Effect } from "effect"
|
import { Effect } from "effect"
|
||||||
import { HttpEffect, HttpMiddleware, HttpServerRequest } from "effect/unstable/http"
|
import { HttpEffect, HttpMiddleware, HttpServerRequest } from "effect/unstable/http"
|
||||||
|
|
||||||
type MarkedInstance = {
|
type MarkedInstance = {
|
||||||
ctx: InstanceContext
|
ctx: InstanceContext
|
||||||
workspaceID?: WorkspaceID
|
store: InstanceStore.Interface
|
||||||
|
bridge: EffectBridge.Shape
|
||||||
}
|
}
|
||||||
|
|
||||||
// Disposal is requested by an endpoint handler, but must run from the outer
|
// Disposal is requested by an endpoint handler, but must run from the outer
|
||||||
@@ -17,20 +17,9 @@ const disposeAfterResponse = new WeakMap<object, MarkedInstance>()
|
|||||||
|
|
||||||
const mark = (ctx: InstanceContext) =>
|
const mark = (ctx: InstanceContext) =>
|
||||||
Effect.gen(function* () {
|
Effect.gen(function* () {
|
||||||
return { ctx, workspaceID: yield* WorkspaceRef }
|
return { ctx, store: yield* InstanceStore.Service, bridge: yield* EffectBridge.make() }
|
||||||
})
|
})
|
||||||
|
|
||||||
// Instance.dispose/reload still publish events through legacy ALS helpers.
|
|
||||||
// Effect request handlers carry these values in services, so bridge them back
|
|
||||||
// into the legacy contexts only around the lifecycle operation.
|
|
||||||
const restoreMarked = <A>(marked: MarkedInstance, fn: () => A) =>
|
|
||||||
Effect.promise(() =>
|
|
||||||
WorkspaceContext.provide({
|
|
||||||
workspaceID: marked.workspaceID,
|
|
||||||
fn: () => Instance.restore(marked.ctx, fn),
|
|
||||||
}),
|
|
||||||
)
|
|
||||||
|
|
||||||
export const markInstanceForDisposal = (ctx: InstanceContext) =>
|
export const markInstanceForDisposal = (ctx: InstanceContext) =>
|
||||||
Effect.gen(function* () {
|
Effect.gen(function* () {
|
||||||
const marked = yield* mark(ctx)
|
const marked = yield* mark(ctx)
|
||||||
@@ -43,11 +32,11 @@ export const markInstanceForDisposal = (ctx: InstanceContext) =>
|
|||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
|
||||||
export const markInstanceForReload = (ctx: InstanceContext, next: Parameters<typeof Instance.reload>[0]) =>
|
export const markInstanceForReload = (ctx: InstanceContext, next: InstanceStore.LoadInput) =>
|
||||||
Effect.gen(function* () {
|
Effect.gen(function* () {
|
||||||
const marked = yield* mark(ctx)
|
const marked = yield* mark(ctx)
|
||||||
return yield* HttpEffect.appendPreResponseHandler((_request, response) =>
|
return yield* HttpEffect.appendPreResponseHandler((_request, response) =>
|
||||||
Effect.as(Effect.uninterruptible(restoreMarked(marked, () => Instance.reload(next))), response),
|
Effect.as(Effect.uninterruptible(marked.bridge.run(marked.store.reload(next))), response),
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -58,6 +47,6 @@ export const disposeMiddleware: HttpMiddleware.HttpMiddleware = (effect) =>
|
|||||||
const marked = disposeAfterResponse.get(request.source)
|
const marked = disposeAfterResponse.get(request.source)
|
||||||
if (!marked) return response
|
if (!marked) return response
|
||||||
disposeAfterResponse.delete(request.source)
|
disposeAfterResponse.delete(request.source)
|
||||||
yield* Effect.uninterruptible(restoreMarked(marked, () => Instance.dispose()))
|
yield* Effect.uninterruptible(marked.bridge.run(marked.store.dispose(marked.ctx)))
|
||||||
return response
|
return response
|
||||||
})
|
})
|
||||||
|
|||||||
+20
-22
@@ -1,9 +1,6 @@
|
|||||||
import { InstanceRef, WorkspaceRef } from "@/effect/instance-ref"
|
import { WorkspaceRef } from "@/effect/instance-ref"
|
||||||
import { AppRuntime } from "@/effect/app-runtime"
|
|
||||||
import { InstanceBootstrap } from "@/project/bootstrap"
|
import { InstanceBootstrap } from "@/project/bootstrap"
|
||||||
import { Instance } from "@/project/instance"
|
import { InstanceStore } from "@/project/instance-store"
|
||||||
import type { InstanceContext } from "@/project/instance"
|
|
||||||
import { Filesystem } from "@/util/filesystem"
|
|
||||||
import { Effect, Layer } from "effect"
|
import { Effect, Layer } from "effect"
|
||||||
import { HttpRouter, HttpServerResponse } from "effect/unstable/http"
|
import { HttpRouter, HttpServerResponse } from "effect/unstable/http"
|
||||||
import { HttpApiMiddleware } from "effect/unstable/httpapi"
|
import { HttpApiMiddleware } from "effect/unstable/httpapi"
|
||||||
@@ -24,32 +21,33 @@ function decode(input: string): string {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function makeInstanceContext(directory: string): Effect.Effect<InstanceContext> {
|
|
||||||
return Effect.promise(() =>
|
|
||||||
Instance.provide({
|
|
||||||
directory: Filesystem.resolve(decode(directory)),
|
|
||||||
init: () => AppRuntime.runPromise(InstanceBootstrap),
|
|
||||||
fn: () => Instance.current,
|
|
||||||
}),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
function provideInstanceContext<E>(
|
function provideInstanceContext<E>(
|
||||||
effect: Effect.Effect<HttpServerResponse.HttpServerResponse, E>,
|
effect: Effect.Effect<HttpServerResponse.HttpServerResponse, E>,
|
||||||
|
store: InstanceStore.Interface,
|
||||||
|
bootstrap: InstanceBootstrap.Interface,
|
||||||
): Effect.Effect<HttpServerResponse.HttpServerResponse, E, WorkspaceRouteContext> {
|
): Effect.Effect<HttpServerResponse.HttpServerResponse, E, WorkspaceRouteContext> {
|
||||||
return Effect.gen(function* () {
|
return Effect.gen(function* () {
|
||||||
const route = yield* WorkspaceRouteContext
|
const route = yield* WorkspaceRouteContext
|
||||||
const ctx = yield* makeInstanceContext(route.directory)
|
return yield* store.provide(
|
||||||
return yield* effect.pipe(
|
{ directory: decode(route.directory), init: bootstrap.run },
|
||||||
Effect.provideService(InstanceRef, ctx),
|
effect.pipe(Effect.provideService(WorkspaceRef, route.workspaceID)),
|
||||||
Effect.provideService(WorkspaceRef, route.workspaceID),
|
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
export const instanceContextLayer = Layer.succeed(
|
export const instanceContextLayer = Layer.effect(
|
||||||
InstanceContextMiddleware,
|
InstanceContextMiddleware,
|
||||||
InstanceContextMiddleware.of((effect) => provideInstanceContext(effect)),
|
Effect.gen(function* () {
|
||||||
|
const store = yield* InstanceStore.Service
|
||||||
|
const bootstrap = yield* InstanceBootstrap.Service
|
||||||
|
return InstanceContextMiddleware.of((effect) => provideInstanceContext(effect, store, bootstrap))
|
||||||
|
}),
|
||||||
)
|
)
|
||||||
|
|
||||||
export const instanceRouterMiddleware = HttpRouter.middleware()((effect) => provideInstanceContext(effect))
|
export const instanceRouterMiddleware = HttpRouter.middleware()(
|
||||||
|
Effect.gen(function* () {
|
||||||
|
const store = yield* InstanceStore.Service
|
||||||
|
const bootstrap = yield* InstanceBootstrap.Service
|
||||||
|
return (effect) => provideInstanceContext(effect, store, bootstrap)
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
|||||||
+5
-15
@@ -1,8 +1,8 @@
|
|||||||
import { getAdaptor } from "@/control-plane/adaptors"
|
import { getAdapter } from "@/control-plane/adapters"
|
||||||
import { WorkspaceID } from "@/control-plane/schema"
|
import { WorkspaceID } from "@/control-plane/schema"
|
||||||
import type { Target } from "@/control-plane/types"
|
import type { Target } from "@/control-plane/types"
|
||||||
import { Workspace } from "@/control-plane/workspace"
|
import { Workspace } from "@/control-plane/workspace"
|
||||||
import { Instance } from "@/project/instance"
|
import { EffectBridge } from "@/effect/bridge"
|
||||||
import { Session } from "@/session/session"
|
import { Session } from "@/session/session"
|
||||||
import { HttpApiProxy } from "./proxy"
|
import { HttpApiProxy } from "./proxy"
|
||||||
import * as Fence from "@/server/fence"
|
import * as Fence from "@/server/fence"
|
||||||
@@ -43,14 +43,6 @@ export class WorkspaceRoutingMiddleware extends HttpApiMiddleware.Service<
|
|||||||
}
|
}
|
||||||
>()("@opencode/ExperimentalHttpApiWorkspaceRouting") {}
|
>()("@opencode/ExperimentalHttpApiWorkspaceRouting") {}
|
||||||
|
|
||||||
function currentDirectory(): string {
|
|
||||||
try {
|
|
||||||
return Instance.directory
|
|
||||||
} catch {
|
|
||||||
return process.cwd()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function requestURL(request: HttpServerRequest.HttpServerRequest): URL {
|
function requestURL(request: HttpServerRequest.HttpServerRequest): URL {
|
||||||
return new URL(request.url, "http://localhost")
|
return new URL(request.url, "http://localhost")
|
||||||
}
|
}
|
||||||
@@ -65,7 +57,7 @@ function selectedWorkspaceID(url: URL, sessionWorkspaceID?: WorkspaceID): Worksp
|
|||||||
}
|
}
|
||||||
|
|
||||||
function defaultDirectory(request: HttpServerRequest.HttpServerRequest, url: URL): string {
|
function defaultDirectory(request: HttpServerRequest.HttpServerRequest, url: URL): string {
|
||||||
return url.searchParams.get("directory") || request.headers["x-opencode-directory"] || currentDirectory()
|
return url.searchParams.get("directory") || request.headers["x-opencode-directory"] || process.cwd()
|
||||||
}
|
}
|
||||||
|
|
||||||
function shouldStayOnControlPlane(request: HttpServerRequest.HttpServerRequest, url: URL): boolean {
|
function shouldStayOnControlPlane(request: HttpServerRequest.HttpServerRequest, url: URL): boolean {
|
||||||
@@ -88,10 +80,8 @@ function missingWorkspaceResponse(id: WorkspaceID): HttpServerResponse.HttpServe
|
|||||||
}
|
}
|
||||||
|
|
||||||
function resolveTarget(workspace: Workspace.Info): Effect.Effect<Target> {
|
function resolveTarget(workspace: Workspace.Info): Effect.Effect<Target> {
|
||||||
return Effect.gen(function* () {
|
const adapter = getAdapter(workspace.projectID, workspace.type)
|
||||||
const adaptor = yield* Effect.sync(() => getAdaptor(workspace.projectID, workspace.type))
|
return EffectBridge.fromPromise(() => adapter.target(workspace))
|
||||||
return yield* Effect.promise(() => Promise.resolve(adaptor.target(workspace)))
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function proxyRemote(
|
function proxyRemote(
|
||||||
|
|||||||
@@ -39,6 +39,7 @@ type OpenApiSchema = {
|
|||||||
maximum?: number
|
maximum?: number
|
||||||
minimum?: number
|
minimum?: number
|
||||||
oneOf?: OpenApiSchema[]
|
oneOf?: OpenApiSchema[]
|
||||||
|
pattern?: string
|
||||||
prefixItems?: OpenApiSchema[]
|
prefixItems?: OpenApiSchema[]
|
||||||
properties?: Record<string, OpenApiSchema>
|
properties?: Record<string, OpenApiSchema>
|
||||||
required?: string[]
|
required?: string[]
|
||||||
@@ -74,9 +75,18 @@ const QueryNumberParameters = new Set(["start", "cursor", "limit", "method"])
|
|||||||
const QueryBooleanParameters = new Set(["roots", "archived"])
|
const QueryBooleanParameters = new Set(["roots", "archived"])
|
||||||
const QueryParameterSchemas = {
|
const QueryParameterSchemas = {
|
||||||
"GET /find/file limit": { type: "integer", minimum: 1, maximum: 200 },
|
"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 },
|
"GET /session/{sessionID}/message limit": { type: "integer", minimum: 0, maximum: Number.MAX_SAFE_INTEGER },
|
||||||
} satisfies Record<string, OpenApiSchema>
|
} 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 = {
|
const LegacyComponentDescriptions = {
|
||||||
LogLevel: "Log level",
|
LogLevel: "Log level",
|
||||||
ServerConfig: "Server configuration for opencode serve and web commands",
|
ServerConfig: "Server configuration for opencode serve and web commands",
|
||||||
@@ -428,6 +438,11 @@ function fixSelfReferencingComponents(spec: OpenApiSpec) {
|
|||||||
|
|
||||||
/** Strip `{type:"null"}` arms that Effect's `Schema.optional` adds to OpenAPI unions. */
|
/** Strip `{type:"null"}` arms that Effect's `Schema.optional` adds to OpenAPI unions. */
|
||||||
function stripOptionalNull(schema: OpenApiSchema): OpenApiSchema {
|
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: {} }
|
if (isEmptyObjectUnion(schema)) return { type: "object", properties: {} }
|
||||||
const options = flattenOptions(schema.anyOf ?? schema.oneOf)
|
const options = flattenOptions(schema.anyOf ?? schema.oneOf)
|
||||||
if (options) {
|
if (options) {
|
||||||
@@ -476,7 +491,12 @@ function flattenOptions(options: OpenApiSchema[] | undefined): OpenApiSchema[] |
|
|||||||
}
|
}
|
||||||
|
|
||||||
function normalizeParameter(param: OpenApiParameter, route: string) {
|
function normalizeParameter(param: OpenApiParameter, route: string) {
|
||||||
if (param.in !== "query" || !param.schema || typeof param.schema !== "object") return
|
if (!param.schema || typeof param.schema !== "object") return
|
||||||
|
if (param.in === "path") {
|
||||||
|
param.schema = pathParameterSchema(route, param.name) ?? stripOptionalNull(param.schema)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (param.in === "query") {
|
||||||
const override = QueryParameterSchemas[`${route} ${param.name}` as keyof typeof QueryParameterSchemas]
|
const override = QueryParameterSchemas[`${route} ${param.name}` as keyof typeof QueryParameterSchemas]
|
||||||
if (override) {
|
if (override) {
|
||||||
param.schema = override
|
param.schema = override
|
||||||
@@ -492,9 +512,19 @@ function normalizeParameter(param: OpenApiParameter, route: string) {
|
|||||||
}
|
}
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
}
|
||||||
param.schema = stripOptionalNull(param.schema)
|
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(
|
export const PublicApi = OpenCodeHttpApi.annotateMerge(
|
||||||
OpenApi.annotations({
|
OpenApi.annotations({
|
||||||
title: "opencode",
|
title: "opencode",
|
||||||
|
|||||||
@@ -11,12 +11,16 @@ import { Config } from "@/config/config"
|
|||||||
import { Command } from "@/command"
|
import { Command } from "@/command"
|
||||||
import * as Observability from "@opencode-ai/core/effect/observability"
|
import * as Observability from "@opencode-ai/core/effect/observability"
|
||||||
import { File } from "@/file"
|
import { File } from "@/file"
|
||||||
|
import { FileWatcher } from "@/file/watcher"
|
||||||
import { Ripgrep } from "@/file/ripgrep"
|
import { Ripgrep } from "@/file/ripgrep"
|
||||||
import { Format } from "@/format"
|
import { Format } from "@/format"
|
||||||
import { LSP } from "@/lsp/lsp"
|
import { LSP } from "@/lsp/lsp"
|
||||||
import { MCP } from "@/mcp"
|
import { MCP } from "@/mcp"
|
||||||
import { Permission } from "@/permission"
|
import { Permission } from "@/permission"
|
||||||
import { Installation } from "@/installation"
|
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 { Project } from "@/project/project"
|
||||||
import { ProviderAuth } from "@/provider/auth"
|
import { ProviderAuth } from "@/provider/auth"
|
||||||
import { Provider } from "@/provider/provider"
|
import { Provider } from "@/provider/provider"
|
||||||
@@ -31,7 +35,9 @@ import { SessionStatus } from "@/session/status"
|
|||||||
import { SessionSummary } from "@/session/summary"
|
import { SessionSummary } from "@/session/summary"
|
||||||
import { Todo } from "@/session/todo"
|
import { Todo } from "@/session/todo"
|
||||||
import { SessionShare } from "@/share/session"
|
import { SessionShare } from "@/share/session"
|
||||||
|
import { ShareNext } from "@/share/share-next"
|
||||||
import { Skill } from "@/skill"
|
import { Skill } from "@/skill"
|
||||||
|
import { Snapshot } from "@/snapshot"
|
||||||
import { SyncEvent } from "@/sync"
|
import { SyncEvent } from "@/sync"
|
||||||
import { ToolRegistry } from "@/tool/registry"
|
import { ToolRegistry } from "@/tool/registry"
|
||||||
import { lazy } from "@/util/lazy"
|
import { lazy } from "@/util/lazy"
|
||||||
@@ -142,11 +148,15 @@ export function createRoutes(corsOptions?: CorsOptions) {
|
|||||||
Command.defaultLayer,
|
Command.defaultLayer,
|
||||||
Config.defaultLayer,
|
Config.defaultLayer,
|
||||||
File.defaultLayer,
|
File.defaultLayer,
|
||||||
|
FileWatcher.defaultLayer,
|
||||||
Format.defaultLayer,
|
Format.defaultLayer,
|
||||||
LSP.defaultLayer,
|
LSP.defaultLayer,
|
||||||
Installation.defaultLayer,
|
Installation.defaultLayer,
|
||||||
|
InstanceBootstrap.defaultLayer,
|
||||||
|
InstanceStore.defaultLayer,
|
||||||
MCP.defaultLayer,
|
MCP.defaultLayer,
|
||||||
Permission.defaultLayer,
|
Permission.defaultLayer,
|
||||||
|
Plugin.defaultLayer,
|
||||||
Project.defaultLayer,
|
Project.defaultLayer,
|
||||||
ProviderAuth.defaultLayer,
|
ProviderAuth.defaultLayer,
|
||||||
Provider.defaultLayer,
|
Provider.defaultLayer,
|
||||||
@@ -161,6 +171,8 @@ export function createRoutes(corsOptions?: CorsOptions) {
|
|||||||
SessionRunState.defaultLayer,
|
SessionRunState.defaultLayer,
|
||||||
SessionStatus.defaultLayer,
|
SessionStatus.defaultLayer,
|
||||||
SessionSummary.defaultLayer,
|
SessionSummary.defaultLayer,
|
||||||
|
ShareNext.defaultLayer,
|
||||||
|
Snapshot.defaultLayer,
|
||||||
SyncEvent.defaultLayer,
|
SyncEvent.defaultLayer,
|
||||||
Skill.defaultLayer,
|
Skill.defaultLayer,
|
||||||
Todo.defaultLayer,
|
Todo.defaultLayer,
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import z from "zod"
|
|||||||
import { Format } from "@/format"
|
import { Format } from "@/format"
|
||||||
import { TuiRoutes } from "./tui"
|
import { TuiRoutes } from "./tui"
|
||||||
import { Instance } from "@/project/instance"
|
import { Instance } from "@/project/instance"
|
||||||
|
import { InstanceStore } from "@/project/instance-store"
|
||||||
import { Vcs } from "@/project/vcs"
|
import { Vcs } from "@/project/vcs"
|
||||||
import { Agent } from "@/agent/agent"
|
import { Agent } from "@/agent/agent"
|
||||||
import { Skill } from "@/skill"
|
import { Skill } from "@/skill"
|
||||||
@@ -62,7 +63,7 @@ export const InstanceRoutes = (upgrade: UpgradeWebSocket): Hono => {
|
|||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
async (c) => {
|
async (c) => {
|
||||||
await Instance.dispose()
|
await InstanceStore.disposeInstance(Instance.current)
|
||||||
return c.json(true)
|
return c.json(true)
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
import type { MiddlewareHandler } from "hono"
|
import type { MiddlewareHandler } from "hono"
|
||||||
import { Instance } from "@/project/instance"
|
import { Instance } from "@/project/instance"
|
||||||
import { InstanceBootstrap } from "@/project/bootstrap"
|
|
||||||
import { AppRuntime } from "@/effect/app-runtime"
|
import { AppRuntime } from "@/effect/app-runtime"
|
||||||
import { AppFileSystem } from "@opencode-ai/core/filesystem"
|
import { AppFileSystem } from "@opencode-ai/core/filesystem"
|
||||||
import { WorkspaceContext } from "@/control-plane/workspace-context"
|
import { WorkspaceContext } from "@/control-plane/workspace-context"
|
||||||
@@ -24,7 +23,6 @@ export function InstanceMiddleware(workspaceID?: WorkspaceID): MiddlewareHandler
|
|||||||
async fn() {
|
async fn() {
|
||||||
return Instance.provide({
|
return Instance.provide({
|
||||||
directory,
|
directory,
|
||||||
init: () => AppRuntime.runPromise(InstanceBootstrap),
|
|
||||||
async fn() {
|
async fn() {
|
||||||
return next()
|
return next()
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -7,7 +7,6 @@ import z from "zod"
|
|||||||
import { ProjectID } from "@/project/schema"
|
import { ProjectID } from "@/project/schema"
|
||||||
import { errors } from "../../error"
|
import { errors } from "../../error"
|
||||||
import { lazy } from "@/util/lazy"
|
import { lazy } from "@/util/lazy"
|
||||||
import { InstanceBootstrap } from "@/project/bootstrap"
|
|
||||||
import { AppRuntime } from "@/effect/app-runtime"
|
import { AppRuntime } from "@/effect/app-runtime"
|
||||||
import { jsonRequest, runRequest } from "./trace"
|
import { jsonRequest, runRequest } from "./trace"
|
||||||
|
|
||||||
@@ -86,7 +85,6 @@ export const ProjectRoutes = lazy(() =>
|
|||||||
directory: dir,
|
directory: dir,
|
||||||
worktree: dir,
|
worktree: dir,
|
||||||
project: next,
|
project: next,
|
||||||
init: () => AppRuntime.runPromise(InstanceBootstrap),
|
|
||||||
})
|
})
|
||||||
return c.json(next)
|
return c.json(next)
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -26,13 +26,17 @@ export function nextTuiRequest() {
|
|||||||
return request.next()
|
return request.next()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function submitTuiRequest(body: TuiRequest) {
|
||||||
|
request.push(body)
|
||||||
|
}
|
||||||
|
|
||||||
export function submitTuiResponse(body: unknown) {
|
export function submitTuiResponse(body: unknown) {
|
||||||
response.push(body)
|
response.push(body)
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function callTui(ctx: Context) {
|
export async function callTui(ctx: Context) {
|
||||||
const body = await ctx.req.json()
|
const body = await ctx.req.json()
|
||||||
request.push({
|
submitTuiRequest({
|
||||||
path: ctx.req.path,
|
path: ctx.req.path,
|
||||||
body,
|
body,
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -1,11 +1,10 @@
|
|||||||
import type { MiddlewareHandler } from "hono"
|
import type { MiddlewareHandler } from "hono"
|
||||||
import type { UpgradeWebSocket } from "hono/ws"
|
import type { UpgradeWebSocket } from "hono/ws"
|
||||||
import { getAdaptor } from "@/control-plane/adaptors"
|
import { getAdapter } from "@/control-plane/adapters"
|
||||||
import { WorkspaceID } from "@/control-plane/schema"
|
import { WorkspaceID } from "@/control-plane/schema"
|
||||||
import { WorkspaceContext } from "@/control-plane/workspace-context"
|
import { WorkspaceContext } from "@/control-plane/workspace-context"
|
||||||
import { Workspace } from "@/control-plane/workspace"
|
import { Workspace } from "@/control-plane/workspace"
|
||||||
import { Flag } from "@opencode-ai/core/flag/flag"
|
import { Flag } from "@opencode-ai/core/flag/flag"
|
||||||
import { InstanceBootstrap } from "@/project/bootstrap"
|
|
||||||
import { Instance } from "@/project/instance"
|
import { Instance } from "@/project/instance"
|
||||||
import { Session } from "@/session/session"
|
import { Session } from "@/session/session"
|
||||||
import { SessionID } from "@/session/schema"
|
import { SessionID } from "@/session/schema"
|
||||||
@@ -91,8 +90,8 @@ export function WorkspaceRouterMiddleware(upgrade: UpgradeWebSocket): Middleware
|
|||||||
return next()
|
return next()
|
||||||
}
|
}
|
||||||
|
|
||||||
const adaptor = getAdaptor(workspace.projectID, workspace.type)
|
const adapter = getAdapter(workspace.projectID, workspace.type)
|
||||||
const target = await adaptor.target(workspace)
|
const target = await adapter.target(workspace)
|
||||||
|
|
||||||
if (target.type === "local") {
|
if (target.type === "local") {
|
||||||
return WorkspaceContext.provide({
|
return WorkspaceContext.provide({
|
||||||
@@ -100,7 +99,6 @@ export function WorkspaceRouterMiddleware(upgrade: UpgradeWebSocket): Middleware
|
|||||||
fn: () =>
|
fn: () =>
|
||||||
Instance.provide({
|
Instance.provide({
|
||||||
directory: target.directory,
|
directory: target.directory,
|
||||||
init: () => AppRuntime.runPromise(InstanceBootstrap),
|
|
||||||
async fn() {
|
async fn() {
|
||||||
return next()
|
return next()
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -772,7 +772,7 @@ export const toModelMessagesEffect = Effect.fnUntraced(function* (
|
|||||||
return {
|
return {
|
||||||
type: "content",
|
type: "content",
|
||||||
value: [
|
value: [
|
||||||
{ type: "text", text: outputObject.text },
|
...(outputObject.text ? [{ type: "text", text: outputObject.text }] : []),
|
||||||
...attachments.map((attachment) => ({
|
...attachments.map((attachment) => ({
|
||||||
type: "media",
|
type: "media",
|
||||||
mediaType: attachment.mime,
|
mediaType: attachment.mime,
|
||||||
@@ -938,10 +938,18 @@ export const toModelMessagesEffect = Effect.fnUntraced(function* (
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
if (part.type === "reasoning") {
|
if (part.type === "reasoning") {
|
||||||
|
if (differentModel) {
|
||||||
|
if (part.text.trim().length > 0)
|
||||||
|
assistantMessage.parts.push({
|
||||||
|
type: "text",
|
||||||
|
text: part.text,
|
||||||
|
})
|
||||||
|
continue
|
||||||
|
}
|
||||||
assistantMessage.parts.push({
|
assistantMessage.parts.push({
|
||||||
type: "reasoning",
|
type: "reasoning",
|
||||||
text: part.text,
|
text: part.text,
|
||||||
...(differentModel ? {} : { providerMetadata: part.metadata }),
|
providerMetadata: part.metadata,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -142,9 +142,9 @@ const Share = Schema.Struct({
|
|||||||
url: Schema.String,
|
url: Schema.String,
|
||||||
})
|
})
|
||||||
|
|
||||||
// Legacy HTTP accepted any number here, and persisted data may already contain
|
// Legacy HTTP accepted negative values here. Keep archive timestamps permissive
|
||||||
// negative values. Keep archive timestamps permissive while other clocks stay non-negative.
|
// while excluding non-finite values that cannot round-trip through JSON.
|
||||||
export const ArchivedTimestamp = Schema.Number
|
export const ArchivedTimestamp = Schema.Finite
|
||||||
|
|
||||||
const Time = Schema.Struct({
|
const Time = Schema.Struct({
|
||||||
created: NonNegativeInt,
|
created: NonNegativeInt,
|
||||||
|
|||||||
@@ -122,6 +122,7 @@ export const Client = lazy(() => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
export function close() {
|
export function close() {
|
||||||
|
if (!Client.loaded()) return
|
||||||
Client().$client.close()
|
Client().$client.close()
|
||||||
Client.reset()
|
Client.reset()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -94,7 +94,7 @@ Importantly, **sync events automatically re-publish as bus events**. This makes
|
|||||||
|
|
||||||
### Event shape
|
### Event shape
|
||||||
|
|
||||||
- The shape of the events are slightly different. A sync event has the `type`, `id`, `seq`, `aggregateID`, and `data` fields. A bus event has the `type` and `properties` fields. `data` and `properties` are largely the same thing. This conversion is automatically handled when the sync system re-published the event throught the bus.
|
- The shape of the events are slightly different. A sync event has the `type`, `id`, `seq`, `aggregateID`, and `data` fields. A bus event has the `type` and `properties` fields. `data` and `properties` are largely the same thing. This conversion is automatically handled when the sync system re-published the event through the bus.
|
||||||
|
|
||||||
The reason for this is because sync events need to track more information. I chose not to copy the `properties` naming to more clearly disambiguate the event types.
|
The reason for this is because sync events need to track more information. I chose not to copy the `properties` naming to more clearly disambiguate the event types.
|
||||||
|
|
||||||
@@ -112,9 +112,9 @@ The system install projectors in `server/projectors.js`. It calls `SyncEvent.ini
|
|||||||
|
|
||||||
This allows you to "reshape" an event from the sync system before it's published to the bus. This should be avoided, but might be necessary for temporary backwards compat.
|
This allows you to "reshape" an event from the sync system before it's published to the bus. This should be avoided, but might be necessary for temporary backwards compat.
|
||||||
|
|
||||||
The only time we use this is the `session.updated` event. Previously this event contained the entire session object. The sync even only contains the fields updated. We convert the event to contain to full object for backwards compatibility (but ideally we'd remove this).
|
The only time we use this is the `session.updated` event. Previously this event contained the entire session object. The sync event only contains the fields updated. We convert the event to contain the full object for backwards compatibility (but ideally we'd remove this).
|
||||||
|
|
||||||
It's very important that types are correct when working with events. Event definitions have a `schema` which carries the defintiion of the event shape (provided by a zod schema, inferred into a TypeScript type). Examples:
|
It's very important that types are correct when working with events. Event definitions have a `schema` which carries the definition of the event shape (provided by a zod schema, inferred into a TypeScript type). Examples:
|
||||||
|
|
||||||
```ts
|
```ts
|
||||||
// The schema from `Updated` typechecks the object correctly
|
// The schema from `Updated` typechecks the object correctly
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ import * as Tool from "./tool"
|
|||||||
import path from "path"
|
import path from "path"
|
||||||
import DESCRIPTION from "./bash.txt"
|
import DESCRIPTION from "./bash.txt"
|
||||||
import * as Log from "@opencode-ai/core/util/log"
|
import * as Log from "@opencode-ai/core/util/log"
|
||||||
import { Instance, type InstanceContext } from "../project/instance"
|
import { containsPath, type InstanceContext } from "../project/instance-context"
|
||||||
import { lazy } from "@/util/lazy"
|
import { lazy } from "@/util/lazy"
|
||||||
import { Language, type Node } from "web-tree-sitter"
|
import { Language, type Node } from "web-tree-sitter"
|
||||||
|
|
||||||
@@ -386,7 +386,7 @@ export const BashTool = Tool.define(
|
|||||||
for (const arg of pathArgs(command, ps)) {
|
for (const arg of pathArgs(command, ps)) {
|
||||||
const resolved = yield* argPath(arg, cwd, ps, shell)
|
const resolved = yield* argPath(arg, cwd, ps, shell)
|
||||||
log.info("resolved path", { arg, resolved })
|
log.info("resolved path", { arg, resolved })
|
||||||
if (!resolved || Instance.containsPath(resolved, instance)) continue
|
if (!resolved || containsPath(resolved, instance)) continue
|
||||||
const dir = (yield* fs.isDir(resolved)) ? resolved : path.dirname(resolved)
|
const dir = (yield* fs.isDir(resolved)) ? resolved : path.dirname(resolved)
|
||||||
scan.dirs.add(dir)
|
scan.dirs.add(dir)
|
||||||
}
|
}
|
||||||
@@ -612,7 +612,7 @@ export const BashTool = Tool.define(
|
|||||||
Effect.sync(() => tree.delete()),
|
Effect.sync(() => tree.delete()),
|
||||||
)
|
)
|
||||||
const scan = yield* collect(tree.rootNode, cwd, ps, shell, executeInstance)
|
const scan = yield* collect(tree.rootNode, cwd, ps, shell, executeInstance)
|
||||||
if (!Instance.containsPath(cwd, executeInstance)) scan.dirs.add(cwd)
|
if (!containsPath(cwd, executeInstance)) scan.dirs.add(cwd)
|
||||||
yield* ask(ctx, scan)
|
yield* ask(ctx, scan)
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ import { Effect } from "effect"
|
|||||||
import * as EffectLogger from "@opencode-ai/core/effect/logger"
|
import * as EffectLogger from "@opencode-ai/core/effect/logger"
|
||||||
import { InstanceState } from "@/effect/instance-state"
|
import { InstanceState } from "@/effect/instance-state"
|
||||||
import type * as Tool from "./tool"
|
import type * as Tool from "./tool"
|
||||||
import { Instance } from "../project/instance"
|
import { containsPath } from "../project/instance-context"
|
||||||
import { AppFileSystem } from "@opencode-ai/core/filesystem"
|
import { AppFileSystem } from "@opencode-ai/core/filesystem"
|
||||||
|
|
||||||
type Kind = "file" | "directory"
|
type Kind = "file" | "directory"
|
||||||
@@ -24,7 +24,7 @@ export const assertExternalDirectoryEffect = Effect.fn("Tool.assertExternalDirec
|
|||||||
|
|
||||||
const ins = yield* InstanceState.context
|
const ins = yield* InstanceState.context
|
||||||
const full = process.platform === "win32" ? AppFileSystem.normalizePath(target) : target
|
const full = process.platform === "win32" ? AppFileSystem.normalizePath(target) : target
|
||||||
if (Instance.containsPath(full, ins)) return
|
if (containsPath(full, ins)) return
|
||||||
|
|
||||||
const kind = options?.kind ?? "file"
|
const kind = options?.kind ?? "file"
|
||||||
const dir = kind === "directory" ? full : path.dirname(full)
|
const dir = kind === "directory" ? full : path.dirname(full)
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ import DESCRIPTION from "./read.txt"
|
|||||||
import { InstanceState } from "@/effect/instance-state"
|
import { InstanceState } from "@/effect/instance-state"
|
||||||
import { assertExternalDirectoryEffect } from "./external-directory"
|
import { assertExternalDirectoryEffect } from "./external-directory"
|
||||||
import { Instruction } from "../session/instruction"
|
import { Instruction } from "../session/instruction"
|
||||||
import { isImageAttachment, isPdfAttachment, sniffAttachmentMime } from "@/util/media"
|
import { isPdfAttachment, sniffAttachmentMime } from "@/util/media"
|
||||||
|
|
||||||
const DEFAULT_READ_LIMIT = 2000
|
const DEFAULT_READ_LIMIT = 2000
|
||||||
const MAX_LINE_LENGTH = 2000
|
const MAX_LINE_LENGTH = 2000
|
||||||
@@ -18,6 +18,7 @@ const MAX_LINE_SUFFIX = `... (line truncated to ${MAX_LINE_LENGTH} chars)`
|
|||||||
const MAX_BYTES = 50 * 1024
|
const MAX_BYTES = 50 * 1024
|
||||||
const MAX_BYTES_LABEL = `${MAX_BYTES / 1024} KB`
|
const MAX_BYTES_LABEL = `${MAX_BYTES / 1024} KB`
|
||||||
const SAMPLE_BYTES = 4096
|
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
|
// `offset` and `limit` were originally `z.coerce.number()` — the runtime
|
||||||
// coercion was useful when the tool was called from a shell but serves no
|
// coercion was useful when the tool was called from a shell but serves no
|
||||||
@@ -220,7 +221,9 @@ export const ReadTool = Tool.define(
|
|||||||
const sample = yield* readSample(filepath, Number(stat.size), SAMPLE_BYTES)
|
const sample = yield* readSample(filepath, Number(stat.size), SAMPLE_BYTES)
|
||||||
|
|
||||||
const mime = sniffAttachmentMime(sample, AppFileSystem.mimeType(filepath))
|
const mime = sniffAttachmentMime(sample, AppFileSystem.mimeType(filepath))
|
||||||
if (isImageAttachment(mime) || isPdfAttachment(mime)) {
|
const isImage = SUPPORTED_IMAGE_MIMES.has(mime)
|
||||||
|
|
||||||
|
if (isImage || isPdfAttachment(mime)) {
|
||||||
const bytes = yield* fs.readFile(filepath)
|
const bytes = yield* fs.readFile(filepath)
|
||||||
const msg = isPdfAttachment(mime) ? "PDF read successfully" : "Image read successfully"
|
const msg = isPdfAttachment(mime) ? "PDF read successfully" : "Image read successfully"
|
||||||
return {
|
return {
|
||||||
|
|||||||
@@ -14,5 +14,7 @@ export function lazy<T>(fn: () => T) {
|
|||||||
value = undefined
|
value = undefined
|
||||||
}
|
}
|
||||||
|
|
||||||
|
result.loaded = () => loaded
|
||||||
|
|
||||||
return result
|
return result
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,7 +2,6 @@ import z from "zod"
|
|||||||
import { NamedError } from "@opencode-ai/core/util/error"
|
import { NamedError } from "@opencode-ai/core/util/error"
|
||||||
import { Global } from "@opencode-ai/core/global"
|
import { Global } from "@opencode-ai/core/global"
|
||||||
import { Instance } from "../project/instance"
|
import { Instance } from "../project/instance"
|
||||||
import { InstanceBootstrap } from "../project/bootstrap"
|
|
||||||
import { Project } from "@/project/project"
|
import { Project } from "@/project/project"
|
||||||
import { Database } from "@/storage/db"
|
import { Database } from "@/storage/db"
|
||||||
import { eq } from "drizzle-orm"
|
import { eq } from "drizzle-orm"
|
||||||
@@ -255,7 +254,6 @@ export const layer: Layer.Layer<
|
|||||||
const booted = yield* Effect.promise(() =>
|
const booted = yield* Effect.promise(() =>
|
||||||
Instance.provide({
|
Instance.provide({
|
||||||
directory: info.directory,
|
directory: info.directory,
|
||||||
init: () => BootstrapRuntime.runPromise(InstanceBootstrap),
|
|
||||||
fn: () => undefined,
|
fn: () => undefined,
|
||||||
})
|
})
|
||||||
.then(() => true)
|
.then(() => true)
|
||||||
|
|||||||
@@ -89,20 +89,17 @@ Use `testEffect(...)` from `test/lib/effect.ts` for tests that exercise Effect s
|
|||||||
```typescript
|
```typescript
|
||||||
import { describe, expect } from "bun:test"
|
import { describe, expect } from "bun:test"
|
||||||
import { Effect, Layer } from "effect"
|
import { Effect, Layer } from "effect"
|
||||||
import { provideTmpdirInstance } from "../fixture/fixture"
|
|
||||||
import { testEffect } from "../lib/effect"
|
import { testEffect } from "../lib/effect"
|
||||||
|
|
||||||
const it = testEffect(Layer.mergeAll(MyService.defaultLayer))
|
const it = testEffect(Layer.mergeAll(MyService.defaultLayer))
|
||||||
|
|
||||||
describe("my service", () => {
|
describe("my service", () => {
|
||||||
it.live("does the thing", () =>
|
it.instance("does the thing", () =>
|
||||||
provideTmpdirInstance(() =>
|
|
||||||
Effect.gen(function* () {
|
Effect.gen(function* () {
|
||||||
const svc = yield* MyService.Service
|
const svc = yield* MyService.Service
|
||||||
const out = yield* svc.run()
|
const out = yield* svc.run()
|
||||||
expect(out).toEqual("ok")
|
expect(out).toEqual("ok")
|
||||||
}),
|
}),
|
||||||
),
|
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
```
|
```
|
||||||
@@ -111,6 +108,7 @@ describe("my service", () => {
|
|||||||
|
|
||||||
- Use `it.effect(...)` when the test should run with `TestClock` and `TestConsole`.
|
- Use `it.effect(...)` when the test should run with `TestClock` and `TestConsole`.
|
||||||
- Use `it.live(...)` when the test depends on real time, filesystem mtimes, child processes, git, locks, or other live OS behavior.
|
- Use `it.live(...)` when the test depends on real time, filesystem mtimes, child processes, git, locks, or other live OS behavior.
|
||||||
|
- Use `it.instance(...)` for live Effect tests that need a scoped temporary directory and instance context.
|
||||||
- Most integration-style tests in this package use `it.live(...)`.
|
- Most integration-style tests in this package use `it.live(...)`.
|
||||||
|
|
||||||
### Effect Fixtures
|
### Effect Fixtures
|
||||||
@@ -122,7 +120,20 @@ Prefer the Effect-aware helpers from `fixture/fixture.ts` instead of building a
|
|||||||
- `provideTmpdirInstance((dir) => effect, options?)` is the convenience helper. It creates a temp directory, binds it as the active instance, and disposes the instance on cleanup.
|
- `provideTmpdirInstance((dir) => effect, options?)` is the convenience helper. It creates a temp directory, binds it as the active instance, and disposes the instance on cleanup.
|
||||||
- `provideTmpdirServer((input) => effect, options?)` does the same, but also provides the test LLM server.
|
- `provideTmpdirServer((input) => effect, options?)` does the same, but also provides the test LLM server.
|
||||||
|
|
||||||
Use `provideTmpdirInstance(...)` by default when a test only needs one temp instance. Use `tmpdirScoped()` plus `provideInstance(...)` when a test needs multiple directories, custom setup before binding, or needs to switch instance context within one test.
|
Use `it.instance(...)` by default when a test only needs one temp instance. Yield `TestInstance` from `fixture/fixture.ts` when the test needs the temp directory path:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import { TestInstance } from "../fixture/fixture"
|
||||||
|
|
||||||
|
it.instance("uses the temp directory", () =>
|
||||||
|
Effect.gen(function* () {
|
||||||
|
const test = yield* TestInstance
|
||||||
|
expect(test.directory).toContain("opencode-test-")
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
```
|
||||||
|
|
||||||
|
Use `provideTmpdirInstance(...)` or `tmpdirScoped()` plus `provideInstance(...)` when a test needs multiple directories, custom setup before binding, needs to switch instance context within one test, or explicitly tests instance disposal/reload lifetime.
|
||||||
|
|
||||||
### Style
|
### Style
|
||||||
|
|
||||||
@@ -130,4 +141,4 @@ Use `provideTmpdirInstance(...)` by default when a test only needs one temp inst
|
|||||||
- Keep the test body inside `Effect.gen(function* () { ... })`.
|
- Keep the test body inside `Effect.gen(function* () { ... })`.
|
||||||
- Yield services directly with `yield* MyService.Service` or `yield* MyTool`.
|
- Yield services directly with `yield* MyService.Service` or `yield* MyTool`.
|
||||||
- Avoid custom `ManagedRuntime`, `attach(...)`, or ad hoc `run(...)` wrappers when `testEffect(...)` already provides the runtime.
|
- Avoid custom `ManagedRuntime`, `attach(...)`, or ad hoc `run(...)` wrappers when `testEffect(...)` already provides the runtime.
|
||||||
- When a test needs instance-local state, prefer `provideTmpdirInstance(...)` or `provideInstance(...)` over manual `Instance.provide(...)` inside Promise-style tests.
|
- When a test needs instance-local state, prefer `it.instance(...)` over manual `Instance.provide(...)` inside Promise-style tests.
|
||||||
|
|||||||
@@ -1,10 +1,11 @@
|
|||||||
import { afterEach, test, expect } from "bun:test"
|
import { afterEach, test, expect } from "bun:test"
|
||||||
import { Effect } from "effect"
|
import { Effect } from "effect"
|
||||||
import path from "path"
|
import path from "path"
|
||||||
import { provideInstance, tmpdir } from "../fixture/fixture"
|
import { disposeAllInstances, provideInstance, tmpdir } from "../fixture/fixture"
|
||||||
import { Instance } from "../../src/project/instance"
|
import { Instance } from "../../src/project/instance"
|
||||||
import { Agent } from "../../src/agent/agent"
|
import { Agent } from "../../src/agent/agent"
|
||||||
import { Permission } from "../../src/permission"
|
import { Permission } from "../../src/permission"
|
||||||
|
import { Global } from "@opencode-ai/core/global"
|
||||||
|
|
||||||
// Helper to evaluate permission for a tool with wildcard pattern
|
// Helper to evaluate permission for a tool with wildcard pattern
|
||||||
function evalPerm(agent: Agent.Info | undefined, permission: string): Permission.Action | undefined {
|
function evalPerm(agent: Agent.Info | undefined, permission: string): Permission.Action | undefined {
|
||||||
@@ -17,7 +18,7 @@ function load<A>(dir: string, fn: (svc: Agent.Interface) => Effect.Effect<A>) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
afterEach(async () => {
|
afterEach(async () => {
|
||||||
await Instance.disposeAll()
|
await disposeAllInstances()
|
||||||
})
|
})
|
||||||
|
|
||||||
test("returns default native agents when no config", async () => {
|
test("returns default native agents when no config", async () => {
|
||||||
@@ -83,7 +84,7 @@ test("explore agent denies edit and write", async () => {
|
|||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
test("explore agent asks for external directories and allows Truncate.GLOB", async () => {
|
test("explore agent asks for external directories and allows whitelisted external paths", async () => {
|
||||||
const { Truncate } = await import("../../src/tool/truncate")
|
const { Truncate } = await import("../../src/tool/truncate")
|
||||||
await using tmp = await tmpdir()
|
await using tmp = await tmpdir()
|
||||||
await Instance.provide({
|
await Instance.provide({
|
||||||
@@ -93,6 +94,9 @@ test("explore agent asks for external directories and allows Truncate.GLOB", asy
|
|||||||
expect(explore).toBeDefined()
|
expect(explore).toBeDefined()
|
||||||
expect(Permission.evaluate("external_directory", "/some/other/path", explore!.permission).action).toBe("ask")
|
expect(Permission.evaluate("external_directory", "/some/other/path", explore!.permission).action).toBe("ask")
|
||||||
expect(Permission.evaluate("external_directory", Truncate.GLOB, explore!.permission).action).toBe("allow")
|
expect(Permission.evaluate("external_directory", Truncate.GLOB, explore!.permission).action).toBe("allow")
|
||||||
|
expect(
|
||||||
|
Permission.evaluate("external_directory", path.join(Global.Path.tmp, "agent-work"), explore!.permission).action,
|
||||||
|
).toBe("allow")
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
@@ -515,6 +519,20 @@ test("Truncate.GLOB is allowed even when user denies external_directory globally
|
|||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
test("global tmp directory children are allowed for external_directory", async () => {
|
||||||
|
await using tmp = await tmpdir()
|
||||||
|
await Instance.provide({
|
||||||
|
directory: tmp.path,
|
||||||
|
fn: async () => {
|
||||||
|
const build = await load(tmp.path, (svc) => svc.get("build"))
|
||||||
|
expect(
|
||||||
|
Permission.evaluate("external_directory", path.join(Global.Path.tmp, "scratch"), build!.permission).action,
|
||||||
|
).toBe("allow")
|
||||||
|
expect(Permission.evaluate("external_directory", "/some/other/path", build!.permission).action).toBe("ask")
|
||||||
|
},
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
test("Truncate.GLOB is allowed even when user denies external_directory per-agent", async () => {
|
test("Truncate.GLOB is allowed even when user denies external_directory per-agent", async () => {
|
||||||
const { Truncate } = await import("../../src/tool/truncate")
|
const { Truncate } = await import("../../src/tool/truncate")
|
||||||
await using tmp = await tmpdir({
|
await using tmp = await tmpdir({
|
||||||
|
|||||||
@@ -2,9 +2,8 @@ import { describe, expect } from "bun:test"
|
|||||||
import { Deferred, Effect, Layer, Schema, Stream } from "effect"
|
import { Deferred, Effect, Layer, Schema, Stream } from "effect"
|
||||||
import { Bus } from "../../src/bus"
|
import { Bus } from "../../src/bus"
|
||||||
import { BusEvent } from "../../src/bus/bus-event"
|
import { BusEvent } from "../../src/bus/bus-event"
|
||||||
import { Instance } from "../../src/project/instance"
|
|
||||||
import { CrossSpawnSpawner } from "@opencode-ai/core/cross-spawn-spawner"
|
import { CrossSpawnSpawner } from "@opencode-ai/core/cross-spawn-spawner"
|
||||||
import { provideInstance, provideTmpdirInstance, tmpdirScoped } from "../fixture/fixture"
|
import { disposeAllInstances, provideInstance, tmpdirScoped } from "../fixture/fixture"
|
||||||
import { testEffect } from "../lib/effect"
|
import { testEffect } from "../lib/effect"
|
||||||
|
|
||||||
const TestEvent = {
|
const TestEvent = {
|
||||||
@@ -19,8 +18,7 @@ const live = Layer.mergeAll(Bus.layer, node)
|
|||||||
const it = testEffect(live)
|
const it = testEffect(live)
|
||||||
|
|
||||||
describe("Bus (Effect-native)", () => {
|
describe("Bus (Effect-native)", () => {
|
||||||
it.live("publish + subscribe stream delivers events", () =>
|
it.instance("publish + subscribe stream delivers events", () =>
|
||||||
provideTmpdirInstance(() =>
|
|
||||||
Effect.gen(function* () {
|
Effect.gen(function* () {
|
||||||
const bus = yield* Bus.Service
|
const bus = yield* Bus.Service
|
||||||
const received: number[] = []
|
const received: number[] = []
|
||||||
@@ -40,11 +38,9 @@ describe("Bus (Effect-native)", () => {
|
|||||||
|
|
||||||
expect(received).toEqual([1, 2])
|
expect(received).toEqual([1, 2])
|
||||||
}),
|
}),
|
||||||
),
|
|
||||||
)
|
)
|
||||||
|
|
||||||
it.live("subscribe filters by event type", () =>
|
it.instance("subscribe filters by event type", () =>
|
||||||
provideTmpdirInstance(() =>
|
|
||||||
Effect.gen(function* () {
|
Effect.gen(function* () {
|
||||||
const bus = yield* Bus.Service
|
const bus = yield* Bus.Service
|
||||||
const pings: number[] = []
|
const pings: number[] = []
|
||||||
@@ -64,11 +60,9 @@ describe("Bus (Effect-native)", () => {
|
|||||||
|
|
||||||
expect(pings).toEqual([42])
|
expect(pings).toEqual([42])
|
||||||
}),
|
}),
|
||||||
),
|
|
||||||
)
|
)
|
||||||
|
|
||||||
it.live("subscribeAll receives all types", () =>
|
it.instance("subscribeAll receives all types", () =>
|
||||||
provideTmpdirInstance(() =>
|
|
||||||
Effect.gen(function* () {
|
Effect.gen(function* () {
|
||||||
const bus = yield* Bus.Service
|
const bus = yield* Bus.Service
|
||||||
const types: string[] = []
|
const types: string[] = []
|
||||||
@@ -89,11 +83,9 @@ describe("Bus (Effect-native)", () => {
|
|||||||
expect(types).toContain("test.effect.ping")
|
expect(types).toContain("test.effect.ping")
|
||||||
expect(types).toContain("test.effect.pong")
|
expect(types).toContain("test.effect.pong")
|
||||||
}),
|
}),
|
||||||
),
|
|
||||||
)
|
)
|
||||||
|
|
||||||
it.live("multiple subscribers each receive the event", () =>
|
it.instance("multiple subscribers each receive the event", () =>
|
||||||
provideTmpdirInstance(() =>
|
|
||||||
Effect.gen(function* () {
|
Effect.gen(function* () {
|
||||||
const bus = yield* Bus.Service
|
const bus = yield* Bus.Service
|
||||||
const a: number[] = []
|
const a: number[] = []
|
||||||
@@ -123,7 +115,6 @@ describe("Bus (Effect-native)", () => {
|
|||||||
expect(a).toEqual([99])
|
expect(a).toEqual([99])
|
||||||
expect(b).toEqual([99])
|
expect(b).toEqual([99])
|
||||||
}),
|
}),
|
||||||
),
|
|
||||||
)
|
)
|
||||||
|
|
||||||
it.live("subscribeAll stream sees InstanceDisposed on disposal", () =>
|
it.live("subscribeAll stream sees InstanceDisposed on disposal", () =>
|
||||||
@@ -151,7 +142,7 @@ describe("Bus (Effect-native)", () => {
|
|||||||
}).pipe(provideInstance(dir))
|
}).pipe(provideInstance(dir))
|
||||||
|
|
||||||
// Dispose from OUTSIDE the instance scope
|
// Dispose from OUTSIDE the instance scope
|
||||||
yield* Effect.promise(() => Instance.disposeAll())
|
yield* Effect.promise(disposeAllInstances)
|
||||||
yield* Deferred.await(disposed).pipe(Effect.timeout("2 seconds"))
|
yield* Deferred.await(disposed).pipe(Effect.timeout("2 seconds"))
|
||||||
|
|
||||||
expect(types).toContain("test.effect.ping")
|
expect(types).toContain("test.effect.ping")
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ import { Schema } from "effect"
|
|||||||
import { Bus } from "../../src/bus"
|
import { Bus } from "../../src/bus"
|
||||||
import { BusEvent } from "../../src/bus/bus-event"
|
import { BusEvent } from "../../src/bus/bus-event"
|
||||||
import { Instance } from "../../src/project/instance"
|
import { Instance } from "../../src/project/instance"
|
||||||
import { tmpdir } from "../fixture/fixture"
|
import { disposeAllInstances, tmpdir } from "../fixture/fixture"
|
||||||
|
|
||||||
const TestEvent = BusEvent.define("test.integration", Schema.Struct({ value: Schema.Number }))
|
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", () => {
|
describe("Bus integration: acquireRelease subscriber pattern", () => {
|
||||||
afterEach(() => Instance.disposeAll())
|
afterEach(() => disposeAllInstances())
|
||||||
|
|
||||||
test("subscriber via callback facade receives events and cleans up on unsub", async () => {
|
test("subscriber via callback facade receives events and cleans up on unsub", async () => {
|
||||||
await using tmp = await tmpdir()
|
await using tmp = await tmpdir()
|
||||||
@@ -78,7 +78,7 @@ describe("Bus integration: acquireRelease subscriber pattern", () => {
|
|||||||
await Bun.sleep(10)
|
await Bun.sleep(10)
|
||||||
})
|
})
|
||||||
|
|
||||||
await Instance.disposeAll()
|
await disposeAllInstances()
|
||||||
await Bun.sleep(50)
|
await Bun.sleep(50)
|
||||||
|
|
||||||
expect(received).toEqual([1])
|
expect(received).toEqual([1])
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ import { Schema } from "effect"
|
|||||||
import { Bus } from "../../src/bus"
|
import { Bus } from "../../src/bus"
|
||||||
import { BusEvent } from "../../src/bus/bus-event"
|
import { BusEvent } from "../../src/bus/bus-event"
|
||||||
import { Instance } from "../../src/project/instance"
|
import { Instance } from "../../src/project/instance"
|
||||||
import { tmpdir } from "../fixture/fixture"
|
import { disposeAllInstances, tmpdir } from "../fixture/fixture"
|
||||||
|
|
||||||
const TestEvent = {
|
const TestEvent = {
|
||||||
Ping: BusEvent.define("test.ping", Schema.Struct({ value: Schema.Number })),
|
Ping: BusEvent.define("test.ping", Schema.Struct({ value: Schema.Number })),
|
||||||
@@ -15,7 +15,7 @@ function withInstance(directory: string, fn: () => Promise<void>) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
describe("Bus", () => {
|
describe("Bus", () => {
|
||||||
afterEach(() => Instance.disposeAll())
|
afterEach(() => disposeAllInstances())
|
||||||
|
|
||||||
describe("publish + subscribe", () => {
|
describe("publish + subscribe", () => {
|
||||||
test("subscriber is live immediately after subscribe returns", async () => {
|
test("subscriber is live immediately after subscribe returns", async () => {
|
||||||
@@ -208,8 +208,8 @@ describe("Bus", () => {
|
|||||||
await Bun.sleep(10)
|
await Bun.sleep(10)
|
||||||
})
|
})
|
||||||
|
|
||||||
// Instance.disposeAll triggers the finalizer which publishes InstanceDisposed
|
// disposeAllInstances triggers the finalizer which publishes InstanceDisposed
|
||||||
await Instance.disposeAll()
|
await disposeAllInstances()
|
||||||
await Bun.sleep(50)
|
await Bun.sleep(50)
|
||||||
|
|
||||||
expect(received).toContain("test.ping")
|
expect(received).toContain("test.ping")
|
||||||
|
|||||||
@@ -0,0 +1,38 @@
|
|||||||
|
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 { AccessToken, AccountID, OrgID } from "../../src/account/schema"
|
||||||
import { AppFileSystem } from "@opencode-ai/core/filesystem"
|
import { AppFileSystem } from "@opencode-ai/core/filesystem"
|
||||||
import { Env } from "../../src/env"
|
import { Env } from "../../src/env"
|
||||||
import { provideTmpdirInstance } from "../fixture/fixture"
|
import { disposeAllInstances, provideTmpdirInstance } from "../fixture/fixture"
|
||||||
import { tmpdir } from "../fixture/fixture"
|
import { tmpdir } from "../fixture/fixture"
|
||||||
import { CrossSpawnSpawner } from "@opencode-ai/core/cross-spawn-spawner"
|
import { CrossSpawnSpawner } from "@opencode-ai/core/cross-spawn-spawner"
|
||||||
import { testEffect } from "../lib/effect"
|
import { testEffect } from "../lib/effect"
|
||||||
@@ -108,7 +108,7 @@ async function check(map: (dir: string) => string) {
|
|||||||
},
|
},
|
||||||
})
|
})
|
||||||
} finally {
|
} finally {
|
||||||
await Instance.disposeAll()
|
await disposeAllInstances()
|
||||||
;(Global.Path as { config: string }).config = prev
|
;(Global.Path as { config: string }).config = prev
|
||||||
await clear()
|
await clear()
|
||||||
}
|
}
|
||||||
|
|||||||
+12
-12
@@ -1,5 +1,5 @@
|
|||||||
import { describe, expect, test } from "bun:test"
|
import { describe, expect, test } from "bun:test"
|
||||||
import { getAdaptor, registerAdaptor } from "../../src/control-plane/adaptors"
|
import { getAdapter, registerAdapter } from "../../src/control-plane/adapters"
|
||||||
import { ProjectID } from "../../src/project/schema"
|
import { ProjectID } from "../../src/project/schema"
|
||||||
import type { WorkspaceInfo } from "../../src/control-plane/types"
|
import type { WorkspaceInfo } from "../../src/control-plane/types"
|
||||||
|
|
||||||
@@ -15,7 +15,7 @@ function info(projectID: WorkspaceInfo["projectID"], type: string): WorkspaceInf
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function adaptor(dir: string) {
|
function adapter(dir: string) {
|
||||||
return {
|
return {
|
||||||
name: dir,
|
name: dir,
|
||||||
description: dir,
|
description: dir,
|
||||||
@@ -33,19 +33,19 @@ function adaptor(dir: string) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
describe("control-plane/adaptors", () => {
|
describe("control-plane/adapters", () => {
|
||||||
test("isolates custom adaptors by project", async () => {
|
test("isolates custom adapters by project", async () => {
|
||||||
const type = `demo-${Math.random().toString(36).slice(2)}`
|
const type = `demo-${Math.random().toString(36).slice(2)}`
|
||||||
const one = ProjectID.make(`project-${Math.random().toString(36).slice(2)}`)
|
const one = ProjectID.make(`project-${Math.random().toString(36).slice(2)}`)
|
||||||
const two = ProjectID.make(`project-${Math.random().toString(36).slice(2)}`)
|
const two = ProjectID.make(`project-${Math.random().toString(36).slice(2)}`)
|
||||||
registerAdaptor(one, type, adaptor("/one"))
|
registerAdapter(one, type, adapter("/one"))
|
||||||
registerAdaptor(two, type, adaptor("/two"))
|
registerAdapter(two, type, adapter("/two"))
|
||||||
|
|
||||||
expect(await (await getAdaptor(one, type)).target(info(one, type))).toEqual({
|
expect(await (await getAdapter(one, type)).target(info(one, type))).toEqual({
|
||||||
type: "local",
|
type: "local",
|
||||||
directory: "/one",
|
directory: "/one",
|
||||||
})
|
})
|
||||||
expect(await (await getAdaptor(two, type)).target(info(two, type))).toEqual({
|
expect(await (await getAdapter(two, type)).target(info(two, type))).toEqual({
|
||||||
type: "local",
|
type: "local",
|
||||||
directory: "/two",
|
directory: "/two",
|
||||||
})
|
})
|
||||||
@@ -54,16 +54,16 @@ describe("control-plane/adaptors", () => {
|
|||||||
test("latest install wins within a project", async () => {
|
test("latest install wins within a project", async () => {
|
||||||
const type = `demo-${Math.random().toString(36).slice(2)}`
|
const type = `demo-${Math.random().toString(36).slice(2)}`
|
||||||
const id = ProjectID.make(`project-${Math.random().toString(36).slice(2)}`)
|
const id = ProjectID.make(`project-${Math.random().toString(36).slice(2)}`)
|
||||||
registerAdaptor(id, type, adaptor("/one"))
|
registerAdapter(id, type, adapter("/one"))
|
||||||
|
|
||||||
expect(await (await getAdaptor(id, type)).target(info(id, type))).toEqual({
|
expect(await (await getAdapter(id, type)).target(info(id, type))).toEqual({
|
||||||
type: "local",
|
type: "local",
|
||||||
directory: "/one",
|
directory: "/one",
|
||||||
})
|
})
|
||||||
|
|
||||||
registerAdaptor(id, type, adaptor("/two"))
|
registerAdapter(id, type, adapter("/two"))
|
||||||
|
|
||||||
expect(await (await getAdaptor(id, type)).target(info(id, type))).toEqual({
|
expect(await (await getAdapter(id, type)).target(info(id, type))).toEqual({
|
||||||
type: "local",
|
type: "local",
|
||||||
directory: "/two",
|
directory: "/two",
|
||||||
})
|
})
|
||||||
@@ -21,12 +21,12 @@ import { ModelID, ProviderID } from "@/provider/schema"
|
|||||||
import { SyncEvent } from "@/sync"
|
import { SyncEvent } from "@/sync"
|
||||||
import { EventSequenceTable, EventTable } from "@/sync/event.sql"
|
import { EventSequenceTable, EventTable } from "@/sync/event.sql"
|
||||||
import { resetDatabase } from "../fixture/db"
|
import { resetDatabase } from "../fixture/db"
|
||||||
import { provideTmpdirInstance, tmpdir } from "../fixture/fixture"
|
import { disposeAllInstances, provideTmpdirInstance, tmpdir } from "../fixture/fixture"
|
||||||
import { testEffect } from "../lib/effect"
|
import { testEffect } from "../lib/effect"
|
||||||
import { registerAdaptor } from "../../src/control-plane/adaptors"
|
import { registerAdapter } from "../../src/control-plane/adapters"
|
||||||
import { WorkspaceID } from "../../src/control-plane/schema"
|
import { WorkspaceID } from "../../src/control-plane/schema"
|
||||||
import { WorkspaceTable } from "../../src/control-plane/workspace.sql"
|
import { WorkspaceTable } from "../../src/control-plane/workspace.sql"
|
||||||
import type { Target, WorkspaceAdaptor, WorkspaceInfo } from "../../src/control-plane/types"
|
import type { Target, WorkspaceAdapter, WorkspaceInfo } from "../../src/control-plane/types"
|
||||||
import * as WorkspaceOld from "../../src/control-plane/workspace"
|
import * as WorkspaceOld from "../../src/control-plane/workspace"
|
||||||
import { AppRuntime } from "@/effect/app-runtime"
|
import { AppRuntime } from "@/effect/app-runtime"
|
||||||
|
|
||||||
@@ -53,8 +53,8 @@ type RecordedCreate = {
|
|||||||
from?: WorkspaceInfo
|
from?: WorkspaceInfo
|
||||||
}
|
}
|
||||||
|
|
||||||
type RecordedAdaptor = {
|
type RecordedAdapter = {
|
||||||
adaptor: WorkspaceAdaptor
|
adapter: WorkspaceAdapter
|
||||||
calls: {
|
calls: {
|
||||||
configure: WorkspaceInfo[]
|
configure: WorkspaceInfo[]
|
||||||
create: RecordedCreate[]
|
create: RecordedCreate[]
|
||||||
@@ -93,7 +93,7 @@ beforeEach(() => {
|
|||||||
|
|
||||||
afterEach(async () => {
|
afterEach(async () => {
|
||||||
mock.restore()
|
mock.restore()
|
||||||
await Instance.disposeAll()
|
await disposeAllInstances()
|
||||||
Flag.OPENCODE_EXPERIMENTAL_WORKSPACES = originalWorkspacesFlag
|
Flag.OPENCODE_EXPERIMENTAL_WORKSPACES = originalWorkspacesFlag
|
||||||
restoreEnv()
|
restoreEnv()
|
||||||
await resetDatabase()
|
await resetDatabase()
|
||||||
@@ -165,13 +165,13 @@ function eventuallyEffect(effect: Effect.Effect<void>, timeout = 1500) {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
function recordedAdaptor(input: {
|
function recordedAdapter(input: {
|
||||||
target: (info: WorkspaceInfo) => Target | Promise<Target>
|
target: (info: WorkspaceInfo) => Target | Promise<Target>
|
||||||
configure?: (info: WorkspaceInfo) => WorkspaceInfo | Promise<WorkspaceInfo>
|
configure?: (info: WorkspaceInfo) => WorkspaceInfo | Promise<WorkspaceInfo>
|
||||||
create?: (info: WorkspaceInfo, env: Record<string, string | undefined>, from?: WorkspaceInfo) => Promise<void>
|
create?: (info: WorkspaceInfo, env: Record<string, string | undefined>, from?: WorkspaceInfo) => Promise<void>
|
||||||
remove?: (info: WorkspaceInfo) => Promise<void>
|
remove?: (info: WorkspaceInfo) => Promise<void>
|
||||||
}): RecordedAdaptor {
|
}): RecordedAdapter {
|
||||||
const calls: RecordedAdaptor["calls"] = {
|
const calls: RecordedAdapter["calls"] = {
|
||||||
configure: [],
|
configure: [],
|
||||||
create: [],
|
create: [],
|
||||||
remove: [],
|
remove: [],
|
||||||
@@ -180,7 +180,7 @@ function recordedAdaptor(input: {
|
|||||||
|
|
||||||
return {
|
return {
|
||||||
calls,
|
calls,
|
||||||
adaptor: {
|
adapter: {
|
||||||
name: "recorded",
|
name: "recorded",
|
||||||
description: "recorded",
|
description: "recorded",
|
||||||
configure(info) {
|
configure(info) {
|
||||||
@@ -207,8 +207,8 @@ function recordedAdaptor(input: {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function localAdaptor(dir: string, input?: { createDir?: boolean; remove?: (info: WorkspaceInfo) => Promise<void> }) {
|
function localAdapter(dir: string, input?: { createDir?: boolean; remove?: (info: WorkspaceInfo) => Promise<void> }) {
|
||||||
return recordedAdaptor({
|
return recordedAdapter({
|
||||||
configure(info) {
|
configure(info) {
|
||||||
return { ...info, directory: dir }
|
return { ...info, directory: dir }
|
||||||
},
|
},
|
||||||
@@ -223,8 +223,8 @@ function localAdaptor(dir: string, input?: { createDir?: boolean; remove?: (info
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
function remoteAdaptor(url: string, input?: { directory?: string | null; headers?: HeadersInit }) {
|
function remoteAdapter(url: string, input?: { directory?: string | null; headers?: HeadersInit }) {
|
||||||
return recordedAdaptor({
|
return recordedAdapter({
|
||||||
configure(info) {
|
configure(info) {
|
||||||
return { ...info, directory: input?.directory ?? info.directory }
|
return { ...info, directory: input?.directory ?? info.directory }
|
||||||
},
|
},
|
||||||
@@ -429,7 +429,7 @@ describe("workspace-old CRUD", () => {
|
|||||||
const workspaceID = WorkspaceID.ascending("wrk_create_local")
|
const workspaceID = WorkspaceID.ascending("wrk_create_local")
|
||||||
const type = unique("create-local")
|
const type = unique("create-local")
|
||||||
const targetDir = path.join(dir, "created-local")
|
const targetDir = path.join(dir, "created-local")
|
||||||
const recorded = recordedAdaptor({
|
const recorded = recordedAdapter({
|
||||||
configure(info) {
|
configure(info) {
|
||||||
return {
|
return {
|
||||||
...info,
|
...info,
|
||||||
@@ -446,7 +446,7 @@ describe("workspace-old CRUD", () => {
|
|||||||
return { type: "local", directory: targetDir }
|
return { type: "local", directory: targetDir }
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
registerAdaptor(Instance.project.id, type, recorded.adaptor)
|
registerAdapter(Instance.project.id, type, recorded.adapter)
|
||||||
|
|
||||||
const info = await createWorkspace({
|
const info = await createWorkspace({
|
||||||
id: workspaceID,
|
id: workspaceID,
|
||||||
@@ -489,17 +489,17 @@ describe("workspace-old CRUD", () => {
|
|||||||
test("create propagates configure failures and does not insert a workspace", async () => {
|
test("create propagates configure failures and does not insert a workspace", async () => {
|
||||||
await withInstance(async () => {
|
await withInstance(async () => {
|
||||||
const type = unique("configure-failure")
|
const type = unique("configure-failure")
|
||||||
registerAdaptor(
|
registerAdapter(
|
||||||
Instance.project.id,
|
Instance.project.id,
|
||||||
type,
|
type,
|
||||||
recordedAdaptor({
|
recordedAdapter({
|
||||||
configure() {
|
configure() {
|
||||||
throw new Error("configure exploded")
|
throw new Error("configure exploded")
|
||||||
},
|
},
|
||||||
target() {
|
target() {
|
||||||
return { type: "local", directory: "/unused" }
|
return { type: "local", directory: "/unused" }
|
||||||
},
|
},
|
||||||
}).adaptor,
|
}).adapter,
|
||||||
)
|
)
|
||||||
|
|
||||||
await expect(
|
await expect(
|
||||||
@@ -509,10 +509,10 @@ describe("workspace-old CRUD", () => {
|
|||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
test("create leaves the inserted row when adaptor create fails", async () => {
|
test("create leaves the inserted row when adapter create fails", async () => {
|
||||||
await withInstance(async () => {
|
await withInstance(async () => {
|
||||||
const type = unique("create-failure")
|
const type = unique("create-failure")
|
||||||
const recorded = recordedAdaptor({
|
const recorded = recordedAdapter({
|
||||||
async create() {
|
async create() {
|
||||||
throw new Error("create exploded")
|
throw new Error("create exploded")
|
||||||
},
|
},
|
||||||
@@ -520,7 +520,7 @@ describe("workspace-old CRUD", () => {
|
|||||||
return { type: "local", directory: "/unused" }
|
return { type: "local", directory: "/unused" }
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
registerAdaptor(Instance.project.id, type, recorded.adaptor)
|
registerAdapter(Instance.project.id, type, recorded.adapter)
|
||||||
|
|
||||||
await expect(
|
await expect(
|
||||||
createWorkspace({ type, branch: "branch", projectID: Instance.project.id, extra: { x: 1 } }),
|
createWorkspace({ type, branch: "branch", projectID: Instance.project.id, extra: { x: 1 } }),
|
||||||
@@ -538,8 +538,8 @@ describe("workspace-old CRUD", () => {
|
|||||||
await withInstance(async (dir) => {
|
await withInstance(async (dir) => {
|
||||||
const type = unique("local-error")
|
const type = unique("local-error")
|
||||||
const missing = path.join(dir, "missing-local-target")
|
const missing = path.join(dir, "missing-local-target")
|
||||||
const recorded = localAdaptor(missing, { createDir: false })
|
const recorded = localAdapter(missing, { createDir: false })
|
||||||
registerAdaptor(Instance.project.id, type, recorded.adaptor)
|
registerAdapter(Instance.project.id, type, recorded.adapter)
|
||||||
|
|
||||||
const info = await createWorkspace({ type, branch: null, projectID: Instance.project.id, extra: null })
|
const info = await createWorkspace({ type, branch: null, projectID: Instance.project.id, extra: null })
|
||||||
|
|
||||||
@@ -576,8 +576,8 @@ describe("workspace-old CRUD", () => {
|
|||||||
Effect.gen(function* () {
|
Effect.gen(function* () {
|
||||||
const workspace = yield* WorkspaceOld.Service
|
const workspace = yield* WorkspaceOld.Service
|
||||||
const type = unique("remote-create")
|
const type = unique("remote-create")
|
||||||
const recorded = remoteAdaptor(`${url}/base/?ignored=1#hash`, { directory: dir })
|
const recorded = remoteAdapter(`${url}/base/?ignored=1#hash`, { directory: dir })
|
||||||
registerAdaptor(Instance.project.id, type, recorded.adaptor)
|
registerAdapter(Instance.project.id, type, recorded.adapter)
|
||||||
|
|
||||||
const info = yield* workspace.create({ type, branch: null, projectID: Instance.project.id, extra: null })
|
const info = yield* workspace.create({ type, branch: null, projectID: Instance.project.id, extra: null })
|
||||||
|
|
||||||
@@ -603,11 +603,11 @@ describe("workspace-old CRUD", () => {
|
|||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
test("remove deletes the workspace, associated sessions, adaptor resources, and status", async () => {
|
test("remove deletes the workspace, associated sessions, adapter resources, and status", async () => {
|
||||||
await withInstance(async (dir) => {
|
await withInstance(async (dir) => {
|
||||||
const type = unique("remove-local")
|
const type = unique("remove-local")
|
||||||
const recorded = localAdaptor(path.join(dir, "remove-local"))
|
const recorded = localAdapter(path.join(dir, "remove-local"))
|
||||||
registerAdaptor(Instance.project.id, type, recorded.adaptor)
|
registerAdapter(Instance.project.id, type, recorded.adapter)
|
||||||
const info = await createWorkspace({ type, branch: null, projectID: Instance.project.id, extra: null })
|
const info = await createWorkspace({ type, branch: null, projectID: Instance.project.id, extra: null })
|
||||||
const one = await AppRuntime.runPromise(SessionNs.Service.use((svc) => svc.create({})))
|
const one = await AppRuntime.runPromise(SessionNs.Service.use((svc) => svc.create({})))
|
||||||
const two = await AppRuntime.runPromise(SessionNs.Service.use((svc) => svc.create({})))
|
const two = await AppRuntime.runPromise(SessionNs.Service.use((svc) => svc.create({})))
|
||||||
@@ -628,21 +628,21 @@ describe("workspace-old CRUD", () => {
|
|||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
test("remove still deletes the row when the adaptor cannot remove resources", async () => {
|
test("remove still deletes the row when the adapter cannot remove resources", async () => {
|
||||||
await withInstance(async () => {
|
await withInstance(async () => {
|
||||||
const type = unique("remove-throws")
|
const type = unique("remove-throws")
|
||||||
const info = workspaceInfo(Instance.project.id, type, { id: WorkspaceID.ascending("wrk_remove_throws") })
|
const info = workspaceInfo(Instance.project.id, type, { id: WorkspaceID.ascending("wrk_remove_throws") })
|
||||||
registerAdaptor(
|
registerAdapter(
|
||||||
Instance.project.id,
|
Instance.project.id,
|
||||||
type,
|
type,
|
||||||
recordedAdaptor({
|
recordedAdapter({
|
||||||
async remove() {
|
async remove() {
|
||||||
throw new Error("remove exploded")
|
throw new Error("remove exploded")
|
||||||
},
|
},
|
||||||
target() {
|
target() {
|
||||||
return { type: "local", directory: "/unused" }
|
return { type: "local", directory: "/unused" }
|
||||||
},
|
},
|
||||||
}).adaptor,
|
}).adapter,
|
||||||
)
|
)
|
||||||
insertWorkspace(info)
|
insertWorkspace(info)
|
||||||
|
|
||||||
@@ -661,7 +661,7 @@ describe("workspace-old sync state", () => {
|
|||||||
const session = await AppRuntime.runPromise(SessionNs.Service.use((svc) => svc.create({})))
|
const session = await AppRuntime.runPromise(SessionNs.Service.use((svc) => svc.create({})))
|
||||||
attachSessionToWorkspace(session.id, info.id)
|
attachSessionToWorkspace(session.id, info.id)
|
||||||
insertWorkspace(info)
|
insertWorkspace(info)
|
||||||
registerAdaptor(Instance.project.id, type, localAdaptor(path.join(dir, "flag-disabled")).adaptor)
|
registerAdapter(Instance.project.id, type, localAdapter(path.join(dir, "flag-disabled")).adapter)
|
||||||
|
|
||||||
startWorkspaceSyncing(Instance.project.id)
|
startWorkspaceSyncing(Instance.project.id)
|
||||||
await delay(25)
|
await delay(25)
|
||||||
@@ -682,8 +682,8 @@ describe("workspace-old sync state", () => {
|
|||||||
await fs.mkdir(withoutSessionDir, { recursive: true })
|
await fs.mkdir(withoutSessionDir, { recursive: true })
|
||||||
insertWorkspace(withSession)
|
insertWorkspace(withSession)
|
||||||
insertWorkspace(withoutSession)
|
insertWorkspace(withoutSession)
|
||||||
registerAdaptor(Instance.project.id, withSessionType, localAdaptor(withSessionDir).adaptor)
|
registerAdapter(Instance.project.id, withSessionType, localAdapter(withSessionDir).adapter)
|
||||||
registerAdaptor(Instance.project.id, withoutSessionType, localAdaptor(withoutSessionDir).adaptor)
|
registerAdapter(Instance.project.id, withoutSessionType, localAdapter(withoutSessionDir).adapter)
|
||||||
attachSessionToWorkspace(
|
attachSessionToWorkspace(
|
||||||
(await AppRuntime.runPromise(SessionNs.Service.use((svc) => svc.create({})))).id,
|
(await AppRuntime.runPromise(SessionNs.Service.use((svc) => svc.create({})))).id,
|
||||||
withSession.id,
|
withSession.id,
|
||||||
@@ -707,10 +707,10 @@ describe("workspace-old sync state", () => {
|
|||||||
const type = unique("missing-local")
|
const type = unique("missing-local")
|
||||||
const info = workspaceInfo(Instance.project.id, type)
|
const info = workspaceInfo(Instance.project.id, type)
|
||||||
insertWorkspace(info)
|
insertWorkspace(info)
|
||||||
registerAdaptor(
|
registerAdapter(
|
||||||
Instance.project.id,
|
Instance.project.id,
|
||||||
type,
|
type,
|
||||||
localAdaptor(path.join(dir, "missing-target"), { createDir: false }).adaptor,
|
localAdapter(path.join(dir, "missing-target"), { createDir: false }).adapter,
|
||||||
)
|
)
|
||||||
attachSessionToWorkspace(
|
attachSessionToWorkspace(
|
||||||
(await AppRuntime.runPromise(SessionNs.Service.use((svc) => svc.create({})))).id,
|
(await AppRuntime.runPromise(SessionNs.Service.use((svc) => svc.create({})))).id,
|
||||||
@@ -738,7 +738,7 @@ describe("workspace-old sync state", () => {
|
|||||||
const target = path.join(dir, "dedupe-local")
|
const target = path.join(dir, "dedupe-local")
|
||||||
await fs.mkdir(target, { recursive: true })
|
await fs.mkdir(target, { recursive: true })
|
||||||
insertWorkspace(info)
|
insertWorkspace(info)
|
||||||
registerAdaptor(Instance.project.id, type, localAdaptor(target).adaptor)
|
registerAdapter(Instance.project.id, type, localAdapter(target).adapter)
|
||||||
attachSessionToWorkspace(
|
attachSessionToWorkspace(
|
||||||
(await AppRuntime.runPromise(SessionNs.Service.use((svc) => svc.create({})))).id,
|
(await AppRuntime.runPromise(SessionNs.Service.use((svc) => svc.create({})))).id,
|
||||||
info.id,
|
info.id,
|
||||||
@@ -795,7 +795,7 @@ describe("workspace-old sync state", () => {
|
|||||||
const type = unique("remote-start")
|
const type = unique("remote-start")
|
||||||
const info = workspaceInfo(Instance.project.id, type)
|
const info = workspaceInfo(Instance.project.id, type)
|
||||||
insertWorkspace(info)
|
insertWorkspace(info)
|
||||||
registerAdaptor(Instance.project.id, type, remoteAdaptor(`${url}/sync`).adaptor)
|
registerAdapter(Instance.project.id, type, remoteAdapter(`${url}/sync`).adapter)
|
||||||
attachSessionToWorkspace((yield* sessionSvc.create({})).id, info.id)
|
attachSessionToWorkspace((yield* sessionSvc.create({})).id, info.id)
|
||||||
|
|
||||||
yield* workspace.startWorkspaceSyncing(Instance.project.id)
|
yield* workspace.startWorkspaceSyncing(Instance.project.id)
|
||||||
@@ -850,7 +850,7 @@ describe("workspace-old sync state", () => {
|
|||||||
const type = unique("remote-connect-fail")
|
const type = unique("remote-connect-fail")
|
||||||
const info = workspaceInfo(Instance.project.id, type)
|
const info = workspaceInfo(Instance.project.id, type)
|
||||||
insertWorkspace(info)
|
insertWorkspace(info)
|
||||||
registerAdaptor(Instance.project.id, type, remoteAdaptor(`${url}/failed`).adaptor)
|
registerAdapter(Instance.project.id, type, remoteAdapter(`${url}/failed`).adapter)
|
||||||
attachSessionToWorkspace((yield* sessionSvc.create({})).id, info.id)
|
attachSessionToWorkspace((yield* sessionSvc.create({})).id, info.id)
|
||||||
|
|
||||||
yield* workspace.startWorkspaceSyncing(Instance.project.id)
|
yield* workspace.startWorkspaceSyncing(Instance.project.id)
|
||||||
@@ -890,7 +890,7 @@ describe("workspace-old sync state", () => {
|
|||||||
const type = unique("remote-history-fail")
|
const type = unique("remote-history-fail")
|
||||||
const info = workspaceInfo(Instance.project.id, type)
|
const info = workspaceInfo(Instance.project.id, type)
|
||||||
insertWorkspace(info)
|
insertWorkspace(info)
|
||||||
registerAdaptor(Instance.project.id, type, remoteAdaptor(`${url}/history-failed`).adaptor)
|
registerAdapter(Instance.project.id, type, remoteAdapter(`${url}/history-failed`).adapter)
|
||||||
attachSessionToWorkspace((yield* sessionSvc.create({})).id, info.id)
|
attachSessionToWorkspace((yield* sessionSvc.create({})).id, info.id)
|
||||||
|
|
||||||
yield* workspace.startWorkspaceSyncing(Instance.project.id)
|
yield* workspace.startWorkspaceSyncing(Instance.project.id)
|
||||||
@@ -947,7 +947,7 @@ describe("workspace-old sync state", () => {
|
|||||||
const type = unique("history-replay")
|
const type = unique("history-replay")
|
||||||
const info = workspaceInfo(Instance.project.id, type)
|
const info = workspaceInfo(Instance.project.id, type)
|
||||||
insertWorkspace(info)
|
insertWorkspace(info)
|
||||||
registerAdaptor(Instance.project.id, type, remoteAdaptor(`${url}/history`).adaptor)
|
registerAdapter(Instance.project.id, type, remoteAdapter(`${url}/history`).adapter)
|
||||||
const session = yield* sessionSvc.create({ title: "before history" })
|
const session = yield* sessionSvc.create({ title: "before history" })
|
||||||
attachSessionToWorkspace(session.id, info.id)
|
attachSessionToWorkspace(session.id, info.id)
|
||||||
historySessionID = session.id
|
historySessionID = session.id
|
||||||
@@ -1014,7 +1014,7 @@ describe("workspace-old sync state", () => {
|
|||||||
const type = unique("sse-forward")
|
const type = unique("sse-forward")
|
||||||
const info = workspaceInfo(Instance.project.id, type)
|
const info = workspaceInfo(Instance.project.id, type)
|
||||||
insertWorkspace(info)
|
insertWorkspace(info)
|
||||||
registerAdaptor(Instance.project.id, type, remoteAdaptor(`${url}/sse-forward`).adaptor)
|
registerAdapter(Instance.project.id, type, remoteAdapter(`${url}/sse-forward`).adapter)
|
||||||
attachSessionToWorkspace((yield* sessionSvc.create({})).id, info.id)
|
attachSessionToWorkspace((yield* sessionSvc.create({})).id, info.id)
|
||||||
|
|
||||||
yield* workspace.startWorkspaceSyncing(Instance.project.id)
|
yield* workspace.startWorkspaceSyncing(Instance.project.id)
|
||||||
@@ -1095,7 +1095,7 @@ describe("workspace-old sync state", () => {
|
|||||||
const type = unique("sse-sync")
|
const type = unique("sse-sync")
|
||||||
const info = workspaceInfo(Instance.project.id, type)
|
const info = workspaceInfo(Instance.project.id, type)
|
||||||
insertWorkspace(info)
|
insertWorkspace(info)
|
||||||
registerAdaptor(Instance.project.id, type, remoteAdaptor(`${url}/sse-sync`).adaptor)
|
registerAdapter(Instance.project.id, type, remoteAdapter(`${url}/sse-sync`).adapter)
|
||||||
const session = yield* sessionSvc.create({ title: "before sse" })
|
const session = yield* sessionSvc.create({ title: "before sse" })
|
||||||
attachSessionToWorkspace(session.id, info.id)
|
attachSessionToWorkspace(session.id, info.id)
|
||||||
sseSessionID = session.id
|
sseSessionID = session.id
|
||||||
@@ -1232,7 +1232,7 @@ describe("workspace-old sessionRestore", () => {
|
|||||||
const type = unique("restore-missing-session")
|
const type = unique("restore-missing-session")
|
||||||
const info = workspaceInfo(Instance.project.id, type, { directory: dir })
|
const info = workspaceInfo(Instance.project.id, type, { directory: dir })
|
||||||
insertWorkspace(info)
|
insertWorkspace(info)
|
||||||
registerAdaptor(Instance.project.id, type, localAdaptor(dir).adaptor)
|
registerAdapter(Instance.project.id, type, localAdapter(dir).adapter)
|
||||||
|
|
||||||
await expect(
|
await expect(
|
||||||
restoreWorkspaceSession({ workspaceID: info.id, sessionID: SessionID.descending("ses_missing_restore") }),
|
restoreWorkspaceSession({ workspaceID: info.id, sessionID: SessionID.descending("ses_missing_restore") }),
|
||||||
@@ -1273,13 +1273,13 @@ describe("workspace-old sessionRestore", () => {
|
|||||||
const type = unique("restore-remote")
|
const type = unique("restore-remote")
|
||||||
const info = workspaceInfo(Instance.project.id, type, { directory: dir })
|
const info = workspaceInfo(Instance.project.id, type, { directory: dir })
|
||||||
insertWorkspace(info)
|
insertWorkspace(info)
|
||||||
registerAdaptor(
|
registerAdapter(
|
||||||
Instance.project.id,
|
Instance.project.id,
|
||||||
type,
|
type,
|
||||||
remoteAdaptor(`${url}/restore/?ignored=1#hash`, {
|
remoteAdapter(`${url}/restore/?ignored=1#hash`, {
|
||||||
directory: dir,
|
directory: dir,
|
||||||
headers: { authorization: "Bearer restore" },
|
headers: { authorization: "Bearer restore" },
|
||||||
}).adaptor,
|
}).adapter,
|
||||||
)
|
)
|
||||||
const session = yield* sessionSvc.create({ title: "restore remote" })
|
const session = yield* sessionSvc.create({ title: "restore remote" })
|
||||||
replaceSessionEvents(session.id, 24)
|
replaceSessionEvents(session.id, 24)
|
||||||
@@ -1353,7 +1353,7 @@ describe("workspace-old sessionRestore", () => {
|
|||||||
const type = unique("restore-null-dir")
|
const type = unique("restore-null-dir")
|
||||||
const info = workspaceInfo(Instance.project.id, type, { directory: null })
|
const info = workspaceInfo(Instance.project.id, type, { directory: null })
|
||||||
insertWorkspace(info)
|
insertWorkspace(info)
|
||||||
registerAdaptor(Instance.project.id, type, remoteAdaptor(`${url}/null-dir`, { directory: null }).adaptor)
|
registerAdapter(Instance.project.id, type, remoteAdapter(`${url}/null-dir`, { directory: null }).adapter)
|
||||||
const session = yield* sessionSvc.create({ title: "null dir" })
|
const session = yield* sessionSvc.create({ title: "null dir" })
|
||||||
replaceSessionEvents(session.id, 0)
|
replaceSessionEvents(session.id, 0)
|
||||||
|
|
||||||
@@ -1397,7 +1397,7 @@ describe("workspace-old sessionRestore", () => {
|
|||||||
const type = unique("restore-remote-fail")
|
const type = unique("restore-remote-fail")
|
||||||
const info = workspaceInfo(Instance.project.id, type, { directory: dir })
|
const info = workspaceInfo(Instance.project.id, type, { directory: dir })
|
||||||
insertWorkspace(info)
|
insertWorkspace(info)
|
||||||
registerAdaptor(Instance.project.id, type, remoteAdaptor(`${url}/fail`, { directory: dir }).adaptor)
|
registerAdapter(Instance.project.id, type, remoteAdapter(`${url}/fail`, { directory: dir }).adapter)
|
||||||
const session = yield* sessionSvc.create({ title: "restore fail" })
|
const session = yield* sessionSvc.create({ title: "restore fail" })
|
||||||
replaceSessionEvents(session.id, 11)
|
replaceSessionEvents(session.id, 11)
|
||||||
|
|
||||||
@@ -1437,7 +1437,7 @@ describe("workspace-old sessionRestore", () => {
|
|||||||
const type = unique("restore-local")
|
const type = unique("restore-local")
|
||||||
const info = workspaceInfo(Instance.project.id, type, { directory: dir })
|
const info = workspaceInfo(Instance.project.id, type, { directory: dir })
|
||||||
insertWorkspace(info)
|
insertWorkspace(info)
|
||||||
registerAdaptor(Instance.project.id, type, localAdaptor(dir).adaptor)
|
registerAdapter(Instance.project.id, type, localAdapter(dir).adapter)
|
||||||
const session = yield* sessionSvc.create({ title: "restore local" })
|
const session = yield* sessionSvc.create({ title: "restore local" })
|
||||||
replaceSessionEvents(session.id, 20)
|
replaceSessionEvents(session.id, 20)
|
||||||
|
|
||||||
@@ -1488,7 +1488,7 @@ describe("workspace-old sessionRestore", () => {
|
|||||||
const type = unique("restore-real-events")
|
const type = unique("restore-real-events")
|
||||||
const info = workspaceInfo(Instance.project.id, type, { directory: dir })
|
const info = workspaceInfo(Instance.project.id, type, { directory: dir })
|
||||||
insertWorkspace(info)
|
insertWorkspace(info)
|
||||||
registerAdaptor(Instance.project.id, type, remoteAdaptor(`${url}/real`, { directory: dir }).adaptor)
|
registerAdapter(Instance.project.id, type, remoteAdapter(`${url}/real`, { directory: dir }).adapter)
|
||||||
const session = yield* sessionSvc.create({ title: "real events" })
|
const session = yield* sessionSvc.create({ title: "real events" })
|
||||||
for (let i = 0; i < 3; i++) {
|
for (let i = 0; i < 3; i++) {
|
||||||
const msg = yield* sessionSvc.updateMessage({
|
const msg = yield* sessionSvc.updateMessage({
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import { $ } from "bun"
|
|||||||
import { Context, Deferred, Duration, Effect, Exit, Fiber, Layer } from "effect"
|
import { Context, Deferred, Duration, Effect, Exit, Fiber, Layer } from "effect"
|
||||||
import { InstanceState } from "@/effect/instance-state"
|
import { InstanceState } from "@/effect/instance-state"
|
||||||
import { Instance } from "../../src/project/instance"
|
import { Instance } from "../../src/project/instance"
|
||||||
import { provideInstance, tmpdirScoped } from "../fixture/fixture"
|
import { disposeAllInstances, provideInstance, tmpdirScoped } from "../fixture/fixture"
|
||||||
import { testEffect } from "../lib/effect"
|
import { testEffect } from "../lib/effect"
|
||||||
|
|
||||||
const it = testEffect(CrossSpawnSpawner.defaultLayer)
|
const it = testEffect(CrossSpawnSpawner.defaultLayer)
|
||||||
@@ -19,7 +19,7 @@ const tmpdirGitScoped = Effect.gen(function* () {
|
|||||||
})
|
})
|
||||||
|
|
||||||
afterEach(async () => {
|
afterEach(async () => {
|
||||||
await Instance.disposeAll()
|
await disposeAllInstances()
|
||||||
})
|
})
|
||||||
|
|
||||||
it.live("InstanceState caches values per directory", () =>
|
it.live("InstanceState caches values per directory", () =>
|
||||||
@@ -94,7 +94,7 @@ it.live("InstanceState invalidates on disposeAll", () =>
|
|||||||
|
|
||||||
yield* access(state, one)
|
yield* access(state, one)
|
||||||
yield* access(state, two)
|
yield* access(state, two)
|
||||||
yield* Effect.promise(() => Instance.disposeAll())
|
yield* Effect.promise(disposeAllInstances)
|
||||||
|
|
||||||
expect(seen.sort()).toEqual([one, two].sort())
|
expect(seen.sort()).toEqual([one, two].sort())
|
||||||
}),
|
}),
|
||||||
|
|||||||
@@ -1,9 +1,12 @@
|
|||||||
import { expect } from "bun:test"
|
import { expect } from "bun:test"
|
||||||
import { Effect, Layer, Context } from "effect"
|
import { Effect, Layer, Context } from "effect"
|
||||||
|
import { InstanceRef } from "../../src/effect/instance-ref"
|
||||||
import { makeRuntime } from "../../src/effect/run-service"
|
import { makeRuntime } from "../../src/effect/run-service"
|
||||||
|
import { ProjectID } from "../../src/project/schema"
|
||||||
import { it } from "../lib/effect"
|
import { it } from "../lib/effect"
|
||||||
|
|
||||||
class Shared extends Context.Service<Shared, { readonly id: number }>()("@test/Shared") {}
|
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", () =>
|
it.live("makeRuntime shares dependent layers through the shared memo map", () =>
|
||||||
Effect.gen(function* () {
|
Effect.gen(function* () {
|
||||||
@@ -47,3 +50,40 @@ it.live("makeRuntime shares dependent layers through the shared memo map", () =>
|
|||||||
expect(n).toBe(1)
|
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 { File } from "../../src/file"
|
||||||
import { Instance } from "../../src/project/instance"
|
import { Instance } from "../../src/project/instance"
|
||||||
import { Filesystem } from "@/util/filesystem"
|
import { Filesystem } from "@/util/filesystem"
|
||||||
import { provideInstance, tmpdir } from "../fixture/fixture"
|
import { disposeAllInstances, provideInstance, tmpdir } from "../fixture/fixture"
|
||||||
|
|
||||||
afterEach(async () => {
|
afterEach(async () => {
|
||||||
await Instance.disposeAll()
|
await disposeAllInstances()
|
||||||
})
|
})
|
||||||
|
|
||||||
const init = () => run(File.Service.use((svc) => svc.init()))
|
const init = () => run(File.Service.use((svc) => svc.init()))
|
||||||
@@ -936,7 +936,7 @@ describe("file/index Filesystem patterns", () => {
|
|||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
await Instance.disposeAll()
|
await disposeAllInstances()
|
||||||
|
|
||||||
await fs.writeFile(path.join(tmp.path, "after.ts"), "after", "utf-8")
|
await fs.writeFile(path.join(tmp.path, "after.ts"), "after", "utf-8")
|
||||||
await fs.rm(path.join(tmp.path, "before.ts"))
|
await fs.rm(path.join(tmp.path, "before.ts"))
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import fs from "fs/promises"
|
|||||||
import { Filesystem } from "@/util/filesystem"
|
import { Filesystem } from "@/util/filesystem"
|
||||||
import { File } from "../../src/file"
|
import { File } from "../../src/file"
|
||||||
import { Instance } from "../../src/project/instance"
|
import { Instance } from "../../src/project/instance"
|
||||||
|
import { containsPath } from "../../src/project/instance-context"
|
||||||
import { provideInstance, tmpdir } from "../fixture/fixture"
|
import { provideInstance, tmpdir } from "../fixture/fixture"
|
||||||
|
|
||||||
const run = <A, E>(eff: Effect.Effect<A, E, File.Service>) =>
|
const run = <A, E>(eff: Effect.Effect<A, E, File.Service>) =>
|
||||||
@@ -121,15 +122,15 @@ describe("File.list path traversal protection", () => {
|
|||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
describe("Instance.containsPath", () => {
|
describe("containsPath", () => {
|
||||||
test("returns true for path inside directory", async () => {
|
test("returns true for path inside directory", async () => {
|
||||||
await using tmp = await tmpdir({ git: true })
|
await using tmp = await tmpdir({ git: true })
|
||||||
|
|
||||||
await Instance.provide({
|
await Instance.provide({
|
||||||
directory: tmp.path,
|
directory: tmp.path,
|
||||||
fn: () => {
|
fn: () => {
|
||||||
expect(Instance.containsPath(path.join(tmp.path, "foo.txt"))).toBe(true)
|
expect(containsPath(path.join(tmp.path, "foo.txt"), Instance.current)).toBe(true)
|
||||||
expect(Instance.containsPath(path.join(tmp.path, "src", "file.ts"))).toBe(true)
|
expect(containsPath(path.join(tmp.path, "src", "file.ts"), Instance.current)).toBe(true)
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
@@ -143,11 +144,11 @@ describe("Instance.containsPath", () => {
|
|||||||
directory: subdir,
|
directory: subdir,
|
||||||
fn: () => {
|
fn: () => {
|
||||||
// .opencode at worktree root, but we're running from packages/lib
|
// .opencode at worktree root, but we're running from packages/lib
|
||||||
expect(Instance.containsPath(path.join(tmp.path, ".opencode", "state"))).toBe(true)
|
expect(containsPath(path.join(tmp.path, ".opencode", "state"), Instance.current)).toBe(true)
|
||||||
// sibling package should also be accessible
|
// sibling package should also be accessible
|
||||||
expect(Instance.containsPath(path.join(tmp.path, "packages", "other", "file.ts"))).toBe(true)
|
expect(containsPath(path.join(tmp.path, "packages", "other", "file.ts"), Instance.current)).toBe(true)
|
||||||
// worktree root itself
|
// worktree root itself
|
||||||
expect(Instance.containsPath(tmp.path)).toBe(true)
|
expect(containsPath(tmp.path, Instance.current)).toBe(true)
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
@@ -158,8 +159,8 @@ describe("Instance.containsPath", () => {
|
|||||||
await Instance.provide({
|
await Instance.provide({
|
||||||
directory: tmp.path,
|
directory: tmp.path,
|
||||||
fn: () => {
|
fn: () => {
|
||||||
expect(Instance.containsPath("/etc/passwd")).toBe(false)
|
expect(containsPath("/etc/passwd", Instance.current)).toBe(false)
|
||||||
expect(Instance.containsPath("/tmp/other-project")).toBe(false)
|
expect(containsPath("/tmp/other-project", Instance.current)).toBe(false)
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
@@ -170,7 +171,7 @@ describe("Instance.containsPath", () => {
|
|||||||
await Instance.provide({
|
await Instance.provide({
|
||||||
directory: tmp.path,
|
directory: tmp.path,
|
||||||
fn: () => {
|
fn: () => {
|
||||||
expect(Instance.containsPath(path.join(tmp.path, "..", "escape.txt"))).toBe(false)
|
expect(containsPath(path.join(tmp.path, "..", "escape.txt"), Instance.current)).toBe(false)
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
@@ -182,8 +183,8 @@ describe("Instance.containsPath", () => {
|
|||||||
directory: tmp.path,
|
directory: tmp.path,
|
||||||
fn: () => {
|
fn: () => {
|
||||||
expect(Instance.directory).toBe(Instance.worktree)
|
expect(Instance.directory).toBe(Instance.worktree)
|
||||||
expect(Instance.containsPath(path.join(tmp.path, "file.txt"))).toBe(true)
|
expect(containsPath(path.join(tmp.path, "file.txt"), Instance.current)).toBe(true)
|
||||||
expect(Instance.containsPath("/etc/passwd")).toBe(false)
|
expect(containsPath("/etc/passwd", Instance.current)).toBe(false)
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
@@ -195,9 +196,9 @@ describe("Instance.containsPath", () => {
|
|||||||
directory: tmp.path,
|
directory: tmp.path,
|
||||||
fn: () => {
|
fn: () => {
|
||||||
// worktree is "/" for non-git projects, but containsPath should NOT allow all paths
|
// worktree is "/" for non-git projects, but containsPath should NOT allow all paths
|
||||||
expect(Instance.containsPath(path.join(tmp.path, "file.txt"))).toBe(true)
|
expect(containsPath(path.join(tmp.path, "file.txt"), Instance.current)).toBe(true)
|
||||||
expect(Instance.containsPath("/etc/passwd")).toBe(false)
|
expect(containsPath("/etc/passwd", Instance.current)).toBe(false)
|
||||||
expect(Instance.containsPath("/tmp/other")).toBe(false)
|
expect(containsPath("/tmp/other", Instance.current)).toBe(false)
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ import { afterEach, describe, expect, test } from "bun:test"
|
|||||||
import fs from "fs/promises"
|
import fs from "fs/promises"
|
||||||
import path from "path"
|
import path from "path"
|
||||||
import { ConfigProvider, Deferred, Effect, Layer, ManagedRuntime, Option } from "effect"
|
import { ConfigProvider, Deferred, Effect, Layer, ManagedRuntime, Option } from "effect"
|
||||||
import { tmpdir } from "../fixture/fixture"
|
import { disposeAllInstances, tmpdir } from "../fixture/fixture"
|
||||||
import { Bus } from "../../src/bus"
|
import { Bus } from "../../src/bus"
|
||||||
import { Config } from "@/config/config"
|
import { Config } from "@/config/config"
|
||||||
import { FileWatcher } from "../../src/file/watcher"
|
import { FileWatcher } from "../../src/file/watcher"
|
||||||
@@ -147,7 +147,7 @@ function ready(directory: string) {
|
|||||||
|
|
||||||
describeWatcher("FileWatcher", () => {
|
describeWatcher("FileWatcher", () => {
|
||||||
afterEach(async () => {
|
afterEach(async () => {
|
||||||
await Instance.disposeAll()
|
await disposeAllInstances()
|
||||||
})
|
})
|
||||||
|
|
||||||
test("publishes root create, update, and delete events", async () => {
|
test("publishes root create, update, and delete events", async () => {
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
import { rm } from "fs/promises"
|
import { rm } from "fs/promises"
|
||||||
import { Instance } from "../../src/project/instance"
|
|
||||||
import { Database } from "@/storage/db"
|
import { Database } from "@/storage/db"
|
||||||
|
import { disposeAllInstances } from "./fixture"
|
||||||
|
|
||||||
export async function resetDatabase() {
|
export async function resetDatabase() {
|
||||||
await Instance.disposeAll().catch(() => undefined)
|
await disposeAllInstances().catch(() => undefined)
|
||||||
Database.close()
|
Database.close()
|
||||||
await rm(Database.Path, { force: true }).catch(() => undefined)
|
await rm(Database.Path, { force: true }).catch(() => undefined)
|
||||||
await rm(`${Database.Path}-wal`, { force: true }).catch(() => undefined)
|
await rm(`${Database.Path}-wal`, { force: true }).catch(() => undefined)
|
||||||
|
|||||||
@@ -5,12 +5,18 @@ import path from "path"
|
|||||||
import { Effect, Context } from "effect"
|
import { Effect, Context } from "effect"
|
||||||
import type * as PlatformError from "effect/PlatformError"
|
import type * as PlatformError from "effect/PlatformError"
|
||||||
import type * as Scope from "effect/Scope"
|
import type * as Scope from "effect/Scope"
|
||||||
|
import { CrossSpawnSpawner } from "@opencode-ai/core/cross-spawn-spawner"
|
||||||
import { ChildProcess, ChildProcessSpawner } from "effect/unstable/process"
|
import { ChildProcess, ChildProcessSpawner } from "effect/unstable/process"
|
||||||
import type { Config } from "@/config/config"
|
import type { Config } from "@/config/config"
|
||||||
import { InstanceRef } from "../../src/effect/instance-ref"
|
import { InstanceRef } from "../../src/effect/instance-ref"
|
||||||
import { Instance } from "../../src/project/instance"
|
import { Instance } from "../../src/project/instance"
|
||||||
|
import { InstanceStore } from "../../src/project/instance-store"
|
||||||
import { TestLLMServer } from "../lib/llm-server"
|
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)
|
// Strip null bytes from paths (defensive fix for CI environment issues)
|
||||||
function sanitizePath(p: string): string {
|
function sanitizePath(p: string): string {
|
||||||
return p.replace(/\0/g, "")
|
return p.replace(/\0/g, "")
|
||||||
@@ -156,6 +162,18 @@ export function provideTmpdirInstance<A, E, R>(
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export class TestInstance extends Context.Service<TestInstance, { readonly directory: string }>()("@test/Instance") {}
|
||||||
|
|
||||||
|
export const withTmpdirInstance =
|
||||||
|
(options?: { git?: boolean; config?: Partial<Config.Info> }) =>
|
||||||
|
<A, E, R>(self: Effect.Effect<A, E, R>) =>
|
||||||
|
Effect.gen(function* () {
|
||||||
|
const directory = yield* tmpdirScoped(options)
|
||||||
|
return yield* InstanceStore.Service.use((store) =>
|
||||||
|
store.provide({ directory }, self.pipe(Effect.provideService(TestInstance, { directory }))),
|
||||||
|
)
|
||||||
|
}).pipe(Effect.provide(InstanceStore.defaultLayer), Effect.provide(CrossSpawnSpawner.defaultLayer))
|
||||||
|
|
||||||
export function provideTmpdirServer<A, E, R>(
|
export function provideTmpdirServer<A, E, R>(
|
||||||
self: (input: { dir: string; llm: TestLLMServer["Service"] }) => Effect.Effect<A, E, R>,
|
self: (input: { dir: string; llm: TestLLMServer["Service"] }) => Effect.Effect<A, E, R>,
|
||||||
options?: { git?: boolean; config?: (url: string) => Partial<Config.Info> },
|
options?: { git?: boolean; config?: (url: string) => Partial<Config.Info> },
|
||||||
|
|||||||
@@ -3,8 +3,11 @@ import { Cause, Effect, Exit, Layer } from "effect"
|
|||||||
import type * as Scope from "effect/Scope"
|
import type * as Scope from "effect/Scope"
|
||||||
import * as TestClock from "effect/testing/TestClock"
|
import * as TestClock from "effect/testing/TestClock"
|
||||||
import * as TestConsole from "effect/testing/TestConsole"
|
import * as TestConsole from "effect/testing/TestConsole"
|
||||||
|
import type { Config } from "@/config/config"
|
||||||
|
import { TestInstance, withTmpdirInstance } from "../fixture/fixture"
|
||||||
|
|
||||||
type Body<A, E, R> = Effect.Effect<A, E, R> | (() => Effect.Effect<A, E, R>)
|
type Body<A, E, R> = Effect.Effect<A, E, R> | (() => Effect.Effect<A, E, R>)
|
||||||
|
type InstanceOptions = { git?: boolean; config?: Partial<Config.Info> }
|
||||||
|
|
||||||
const body = <A, E, R>(value: Body<A, E, R>) => Effect.suspend(() => (typeof value === "function" ? value() : value))
|
const body = <A, E, R>(value: Body<A, E, R>) => Effect.suspend(() => (typeof value === "function" ? value() : value))
|
||||||
|
|
||||||
@@ -38,7 +41,28 @@ const make = <R, E>(testLayer: Layer.Layer<R, E>, liveLayer: Layer.Layer<R, E>)
|
|||||||
live.skip = <A, E2>(name: string, value: Body<A, E2, R | Scope.Scope>, opts?: number | TestOptions) =>
|
live.skip = <A, E2>(name: string, value: Body<A, E2, R | Scope.Scope>, opts?: number | TestOptions) =>
|
||||||
test.skip(name, () => run(value, liveLayer), opts)
|
test.skip(name, () => run(value, liveLayer), opts)
|
||||||
|
|
||||||
return { effect, live }
|
const instance = <A, E2>(
|
||||||
|
name: string,
|
||||||
|
value: Body<A, E2, R | TestInstance | Scope.Scope>,
|
||||||
|
instanceOptions?: InstanceOptions,
|
||||||
|
opts?: number | TestOptions,
|
||||||
|
) => test(name, () => run(body(value).pipe(withTmpdirInstance(instanceOptions)), liveLayer), opts)
|
||||||
|
|
||||||
|
instance.only = <A, E2>(
|
||||||
|
name: string,
|
||||||
|
value: Body<A, E2, R | TestInstance | Scope.Scope>,
|
||||||
|
instanceOptions?: InstanceOptions,
|
||||||
|
opts?: number | TestOptions,
|
||||||
|
) => test.only(name, () => run(body(value).pipe(withTmpdirInstance(instanceOptions)), liveLayer), opts)
|
||||||
|
|
||||||
|
instance.skip = <A, E2>(
|
||||||
|
name: string,
|
||||||
|
value: Body<A, E2, R | TestInstance | Scope.Scope>,
|
||||||
|
instanceOptions?: InstanceOptions,
|
||||||
|
opts?: number | TestOptions,
|
||||||
|
) => test.skip(name, () => run(body(value).pipe(withTmpdirInstance(instanceOptions)), liveLayer), opts)
|
||||||
|
|
||||||
|
return { effect, live, instance }
|
||||||
}
|
}
|
||||||
|
|
||||||
// Test environment with TestClock and TestConsole
|
// Test environment with TestClock and TestConsole
|
||||||
|
|||||||
@@ -2,13 +2,13 @@ import { afterEach, describe, test, expect } from "bun:test"
|
|||||||
import { Permission } from "../src/permission"
|
import { Permission } from "../src/permission"
|
||||||
import { Config } from "@/config/config"
|
import { Config } from "@/config/config"
|
||||||
import { Instance } from "../src/project/instance"
|
import { Instance } from "../src/project/instance"
|
||||||
import { tmpdir } from "./fixture/fixture"
|
import { disposeAllInstances, tmpdir } from "./fixture/fixture"
|
||||||
import { AppRuntime } from "../src/effect/app-runtime"
|
import { AppRuntime } from "../src/effect/app-runtime"
|
||||||
|
|
||||||
const load = () => AppRuntime.runPromise(Config.Service.use((svc) => svc.get()))
|
const load = () => AppRuntime.runPromise(Config.Service.use((svc) => svc.get()))
|
||||||
|
|
||||||
afterEach(async () => {
|
afterEach(async () => {
|
||||||
await Instance.disposeAll()
|
await disposeAllInstances()
|
||||||
})
|
})
|
||||||
|
|
||||||
describe("Permission.evaluate for permission.task", () => {
|
describe("Permission.evaluate for permission.task", () => {
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ import { CrossSpawnSpawner } from "@opencode-ai/core/cross-spawn-spawner"
|
|||||||
import { Permission } from "../../src/permission"
|
import { Permission } from "../../src/permission"
|
||||||
import { PermissionID } from "../../src/permission/schema"
|
import { PermissionID } from "../../src/permission/schema"
|
||||||
import { Instance } from "../../src/project/instance"
|
import { Instance } from "../../src/project/instance"
|
||||||
import { provideInstance, provideTmpdirInstance, tmpdirScoped } from "../fixture/fixture"
|
import { disposeAllInstances, provideInstance, provideTmpdirInstance, tmpdirScoped } from "../fixture/fixture"
|
||||||
import { testEffect } from "../lib/effect"
|
import { testEffect } from "../lib/effect"
|
||||||
import { MessageID, SessionID } from "../../src/session/schema"
|
import { MessageID, SessionID } from "../../src/session/schema"
|
||||||
|
|
||||||
@@ -15,7 +15,7 @@ const env = Layer.mergeAll(Permission.layer.pipe(Layer.provide(bus)), bus, Cross
|
|||||||
const it = testEffect(env)
|
const it = testEffect(env)
|
||||||
|
|
||||||
afterEach(async () => {
|
afterEach(async () => {
|
||||||
await Instance.disposeAll()
|
await disposeAllInstances()
|
||||||
})
|
})
|
||||||
|
|
||||||
const rejectAll = (message?: string) =>
|
const rejectAll = (message?: string) =>
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user