diff --git a/.changeset/current-opencode-dev.md b/.changeset/current-opencode-dev.md new file mode 100644 index 0000000..b1b4054 --- /dev/null +++ b/.changeset/current-opencode-dev.md @@ -0,0 +1,5 @@ +--- +"opencode-drive": patch +--- + +Restore compatibility with current OpenCode V2 checkouts and packed Drive installations. Drive now uses V2's built-in simulation transport and provider shape, isolates scripted service ports and command forms, and compiles standalone scripts against the launching Drive toolchain without package installation or source-directory links. diff --git a/apps/catalog/package.json b/apps/catalog/package.json index 403af91..74d84c8 100644 --- a/apps/catalog/package.json +++ b/apps/catalog/package.json @@ -31,7 +31,7 @@ "@types/bun": "^1.3.14", "@types/react": "^19.2.17", "@types/react-dom": "^19.2.3", - "effect": "4.0.0-beta.98", + "effect": "4.0.0-beta.101", "opencode-drive": "workspace:*", "oxlint": "1.60.0", "typescript": "^7.0.2" diff --git a/bun.lock b/bun.lock index 540da26..beed928 100644 --- a/bun.lock +++ b/bun.lock @@ -23,7 +23,7 @@ "@types/bun": "^1.3.14", "@types/react": "^19.2.17", "@types/react-dom": "^19.2.3", - "effect": "4.0.0-beta.98", + "effect": "4.0.0-beta.101", "opencode-drive": "workspace:*", "oxlint": "1.60.0", "typescript": "^7.0.2", @@ -31,24 +31,25 @@ }, "packages/drive": { "name": "opencode-drive", - "version": "1.3.0", + "version": "1.4.0", "bin": { "opencode-drive": "bin/opencode-drive", }, "dependencies": { - "@effect/platform-node": "4.0.0-beta.98", - "@effect/platform-node-shared": "4.0.0-beta.98", + "@effect/platform-node": "4.0.0-beta.101", + "@effect/platform-node-shared": "4.0.0-beta.101", "@napi-rs/canvas": "1.0.2", - "@opencode-ai/client": "0.0.0-next-15747", + "@opencode-ai/client": "0.0.0-next-16543", + "@opentui/core": "0.4.5", + "@types/bun": "1.3.13", + "@typescript/native-preview": "7.0.0-dev.20251207.1", "@wterm/core": "0.3.0", "@wterm/ghostty": "0.3.0", - "effect": "4.0.0-beta.98", + "effect": "4.0.0-beta.101", }, "devDependencies": { - "@effect/vitest": "4.0.0-beta.98", + "@effect/vitest": "4.0.0-beta.101", "@tsconfig/bun": "1.0.9", - "@types/bun": "1.3.13", - "@typescript/native-preview": "7.0.0-dev.20251207.1", "oxlint": "1.60.0", "oxlint-tsgolint": "0.21.0", "typescript": "5.8.2", @@ -93,11 +94,11 @@ "@changesets/write": ["@changesets/write@0.4.0", "", { "dependencies": { "@changesets/types": "^6.1.0", "fs-extra": "^7.0.1", "human-id": "^4.1.1", "prettier": "^2.7.1" } }, "sha512-CdTLvIOPiCNuH71pyDu3rA+Q0n65cmAbXnwWH84rKGiFumFzkmHNT8KHTMEchcxN+Kl8I54xGUhJ7l3E7X396Q=="], - "@effect/platform-node": ["@effect/platform-node@4.0.0-beta.98", "", { "dependencies": { "@effect/platform-node-shared": "^4.0.0-beta.98", "mime": "^4.1.0", "undici": "^8.7.0" }, "peerDependencies": { "effect": "^4.0.0-beta.98", "ioredis": "^5.7.0" } }, "sha512-IQu1TiLXQEDSGkDBllyYjVadf+UqdjptryqX4mmktVTTbGDq7X4uVxe7cSgXuqZvyfG6kagTzwj2lfynxOaKQg=="], + "@effect/platform-node": ["@effect/platform-node@4.0.0-beta.101", "", { "dependencies": { "@effect/platform-node-shared": "^4.0.0-beta.101", "mime": "^4.1.0", "undici": "^8.7.0" }, "peerDependencies": { "effect": "^4.0.0-beta.101", "ioredis": "^5.7.0" } }, "sha512-pClk7dmMtHgM6Byu7CzGfrPvZ1/4BwmrRlCg2Op+iJozMkwVUxq8v4beK8b9SvxsliJjHZFznjvkVLX7LQjBqw=="], - "@effect/platform-node-shared": ["@effect/platform-node-shared@4.0.0-beta.98", "", { "dependencies": { "@types/ws": "^8.18.1", "ws": "^8.21.0" }, "peerDependencies": { "effect": "^4.0.0-beta.98" } }, "sha512-iySXaffnCJX1sNAIp79ghhIeui9E5qwUQyqd1VLPkB9UNO4vdpd9B5fTEXwe7S/GusL4jsk9vSvX38XJgRFG1w=="], + "@effect/platform-node-shared": ["@effect/platform-node-shared@4.0.0-beta.101", "", { "dependencies": { "@types/ws": "^8.18.1", "ws": "^8.21.0" }, "peerDependencies": { "effect": "^4.0.0-beta.101" } }, "sha512-g4L7XiyJSNJLJVhlslyg2zBCQsoKQf1y1gd+Yfd+3wD9ymC+m7ymbd/5FGqnT1aXV6E2AwRr4D/R1eyRUikvWQ=="], - "@effect/vitest": ["@effect/vitest@4.0.0-beta.98", "", { "peerDependencies": { "effect": "^4.0.0-beta.98", "vitest": "^3.0.0 || ^4.0.0" } }, "sha512-uXRPuN8Y6v43/OVmQwKOd/VFDh+dipaxGKdWPr1bdnM+4bl8NZlYYRJi5omgXLFZ6ZbleduXKcmLM3NeePe69w=="], + "@effect/vitest": ["@effect/vitest@4.0.0-beta.101", "", { "peerDependencies": { "effect": "^4.0.0-beta.101", "vitest": "^3.0.0 || ^4.0.0" } }, "sha512-F5Ur8pZYti0xkZFyb4hPZt8RrKQ1XBoC28ZkKxc7N1iPbhE9fWOvlWA1NC2XlN2wWG08yb5g5jR53LdOxjlhpQ=="], "@emnapi/core": ["@emnapi/core@1.11.1", "", { "dependencies": { "@emnapi/wasi-threads": "1.2.2", "tslib": "^2.4.0" } }, "sha512-RSvbQmHzdKzNsLYa/wHrbc3KN4sYLKAdPZxqiM2HATqv/SBk2/ENSHpvXGaLOMcsAyz0poEGqkmmKYG3OWiJEQ=="], @@ -167,14 +168,32 @@ "@nodelib/fs.walk": ["@nodelib/fs.walk@1.2.8", "", { "dependencies": { "@nodelib/fs.scandir": "2.1.5", "fastq": "^1.6.0" } }, "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg=="], - "@opencode-ai/client": ["@opencode-ai/client@0.0.0-next-15747", "", { "dependencies": { "@opencode-ai/protocol": "0.0.0-next-15747", "@opencode-ai/schema": "0.0.0-next-15747" }, "peerDependencies": { "effect": "4.0.0-beta.98" }, "optionalPeers": ["effect"] }, "sha512-18j1BXJA34wq52PoP0ddalOWE2Ij9cpemT0wYokqAICcztdj4ojWLeTrFeQldTEzTOj0z1hEZjMB6l8S7UCUFQ=="], + "@opencode-ai/client": ["@opencode-ai/client@0.0.0-next-16543", "", { "dependencies": { "@opencode-ai/protocol": "0.0.0-next-16543", "@opencode-ai/schema": "0.0.0-next-16543" }, "peerDependencies": { "effect": "4.0.0-beta.101" }, "optionalPeers": ["effect"] }, "sha512-TjVOLtl2P5/63mTxTRb0WF9IYrPEJwKvUKEMkeII/AHqkH/QmSMmIZjRJyHQMgc5QBC8qtw+hm8WRiCiudu6qw=="], - "@opencode-ai/protocol": ["@opencode-ai/protocol@0.0.0-next-15747", "", { "dependencies": { "@opencode-ai/schema": "0.0.0-next-15747", "effect": "4.0.0-beta.98" } }, "sha512-VX51qDVA7FoaeAKNSM2hguaLg7W6vSXqh8Q7hM96vKxMbzl3P2+ESuwD11GQn93YuvkBlyPzvyEPeYgI/vhTVA=="], + "@opencode-ai/protocol": ["@opencode-ai/protocol@0.0.0-next-16543", "", { "dependencies": { "@opencode-ai/schema": "0.0.0-next-16543", "effect": "4.0.0-beta.101" } }, "sha512-SIYM15csEIf2wtrVCM3Eg3+SCvG66gyjrckqEeTtrfntqAVH5+M9Yce5oefQyZr5vKL+akGzjX5ywsgMqb4DlA=="], - "@opencode-ai/schema": ["@opencode-ai/schema@0.0.0-next-15747", "", { "dependencies": { "effect": "4.0.0-beta.98" } }, "sha512-BrXOa6Ym5ivzJ7rc4ztw8GlLVqJkZ6G4cZXaJRAQhsOetoASSL2zspCX/T2GjI0txx40EWCGtH+r9lMgMWbwew=="], + "@opencode-ai/schema": ["@opencode-ai/schema@0.0.0-next-16543", "", { "dependencies": { "@standard-schema/spec": "1.1.0", "effect": "4.0.0-beta.101" } }, "sha512-X29NyL9urulLSRKjVFukqR6zFdW7uPcdnaup1L3JH46RL0NQIfnXPTpqC/tFBhbu6S4LlhP/SsfKj56ncvPVKQ=="], "@opencode-drive/catalog": ["@opencode-drive/catalog@workspace:apps/catalog"], + "@opentui/core": ["@opentui/core@0.4.5", "", { "dependencies": { "bun-ffi-structs": "0.2.4", "diff": "9.0.0", "marked": "17.0.1", "string-width": "7.2.0", "strip-ansi": "7.1.2" }, "optionalDependencies": { "@opentui/core-darwin-arm64": "0.4.5", "@opentui/core-darwin-x64": "0.4.5", "@opentui/core-linux-arm64": "0.4.5", "@opentui/core-linux-arm64-musl": "0.4.5", "@opentui/core-linux-x64": "0.4.5", "@opentui/core-linux-x64-musl": "0.4.5", "@opentui/core-win32-arm64": "0.4.5", "@opentui/core-win32-x64": "0.4.5" }, "peerDependencies": { "web-tree-sitter": "0.25.10" } }, "sha512-JsgRTPkA6e+Vxmumxai6SElOSlRQkbzNKHlCfemlArRiLhfC1IZ9RXJo2QH4xSu+uBOWAM90uss73/pPlkdEig=="], + + "@opentui/core-darwin-arm64": ["@opentui/core-darwin-arm64@0.4.5", "", { "os": "darwin", "cpu": "arm64" }, "sha512-8KUG0oRidnR+oW1RSZJ72/PhZLl+qRRMk5U/mieF4c0SJ5V3tYACpBZAKzQfHNd1f7QzD8FHZct1lPpQgtmkWg=="], + + "@opentui/core-darwin-x64": ["@opentui/core-darwin-x64@0.4.5", "", { "os": "darwin", "cpu": "x64" }, "sha512-R2bocsg55gwjOqCp/MWFgFYzRmsduKegB6nzgFAPCvAD/L5Jf30xpWJWFlSg3x8vxe1L9WJ84dfqa4M7mZZ3wA=="], + + "@opentui/core-linux-arm64": ["@opentui/core-linux-arm64@0.4.5", "", { "os": "linux", "cpu": "arm64" }, "sha512-R4MZ25a4CzOAGVjW9aj1hUfzQGVfCJwrwBDbNs2SXaIvzcZqkxCVtU4FoQ5LsaD0j/BdNQVg2CIfFkFsm1fDuQ=="], + + "@opentui/core-linux-arm64-musl": ["@opentui/core-linux-arm64-musl@0.4.5", "", { "os": "linux", "cpu": "arm64" }, "sha512-ieqdyKI6EIYPalYAETB2wsdP83hr5Ifi+dFnBFUmdEEFHsoKwBmn2S7bsTOYlX7Bg03F4/YPIg+IvRpeC+cUJw=="], + + "@opentui/core-linux-x64": ["@opentui/core-linux-x64@0.4.5", "", { "os": "linux", "cpu": "x64" }, "sha512-SNyuQoxMKI1vuJhgxSSW96adWM6LqFl2SoS3GM4tGeneGOanVVG2Y06PvlytXvF4cKik97t0rqkVMRetmOs93w=="], + + "@opentui/core-linux-x64-musl": ["@opentui/core-linux-x64-musl@0.4.5", "", { "os": "linux", "cpu": "x64" }, "sha512-mKVKcIcPiSVVZZsdPSBoWwoa2/TCeQAaMDeHF7PFw2kt5bTXZPP7xxWfRQLCNIcA1eaGl59UuwUWHDR2Ve548Q=="], + + "@opentui/core-win32-arm64": ["@opentui/core-win32-arm64@0.4.5", "", { "os": "win32", "cpu": "arm64" }, "sha512-GHTTsqeR45q2Iek9Rb7ty+x/hAKn2jZ1ujlCgPR8LBKyF7h0E1dNFryoZ7ehMc3kJndP1sKn836IemKFqxuDdQ=="], + + "@opentui/core-win32-x64": ["@opentui/core-win32-x64@0.4.5", "", { "os": "win32", "cpu": "x64" }, "sha512-Y8T/yXCDGagRGiQrtmuB6AhRcPucKFs/Dre3v8kJwNYqDccI4FzUPKclZ7djfmRZNjl7JUqPhZZP/PwDpQocMg=="], + "@oxc-project/types": ["@oxc-project/types@0.139.0", "", {}, "sha512-r9gHphtCs+1M7J0pw6Sn/hh/Wpa/iQrOOkrNAlVLF/gHq+/CJmHIWKKUUhdWjcD6CIa8idarspCsASiXCXvFUw=="], "@oxlint-tsgolint/darwin-arm64": ["@oxlint-tsgolint/darwin-arm64@0.21.0", "", { "os": "darwin", "cpu": "arm64" }, "sha512-P20j3MLqfwIT+94qGU3htC7dWp4pXGZW1p1p7FRUzu1aopq7c9nPCgf0W/WjktqQ57+iuTq9mbSlwWinl6+H1A=="], @@ -357,7 +376,7 @@ "ansi-colors": ["ansi-colors@4.1.3", "", {}, "sha512-/6w/C21Pm1A7aZitlI5Ni/2J6FFQN8i1Cvz3kHABAAbw93v/NlvKdVOqz7CCWz/3iv/JplRSEEZ83XION15ovw=="], - "ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="], + "ansi-regex": ["ansi-regex@6.2.2", "", {}, "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg=="], "argparse": ["argparse@2.0.1", "", {}, "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q=="], @@ -369,6 +388,8 @@ "braces": ["braces@3.0.3", "", { "dependencies": { "fill-range": "^7.1.1" } }, "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA=="], + "bun-ffi-structs": ["bun-ffi-structs@0.2.4", "", { "peerDependencies": { "typescript": "^5" } }, "sha512-AJzsqoVFs1KBbJbWHIYrVZLDC3NhTqqh25awRXqzoLzmBAKr5oqk6+CwuYHAekKx+VBCYVohBoKuRq40dV+TYg=="], + "bun-types": ["bun-types@1.3.14", "", { "dependencies": { "@types/node": "*" } }, "sha512-4N0ig0fEomHt5R0KCFWjovxow98rIoRwKolrYdCcknNwMekCXRnWEUvgu5soYV8QXtVsrUD8B95MBOZGPvr6KQ=="], "chai": ["chai@6.2.2", "", {}, "sha512-NUPRluOfOiTKBKvWPtSD4PhFvWCqOi0BGStNWs57X9js7XGTprSmFoz5F0tWhR4WPjNeR9jXqdC7/UpSJTnlRg=="], @@ -391,9 +412,13 @@ "detect-libc": ["detect-libc@2.1.2", "", {}, "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ=="], + "diff": ["diff@9.0.0", "", {}, "sha512-svtcdpS8CgJyqAjEQIXdb3OjhFVVYjzGAPO8WGCmRbrml64SPw/jJD4GoE98aR7r25A0XcgrK3F02yw9R/vhQw=="], + "dir-glob": ["dir-glob@3.0.1", "", { "dependencies": { "path-type": "^4.0.0" } }, "sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA=="], - "effect": ["effect@4.0.0-beta.98", "", { "dependencies": { "@standard-schema/spec": "^1.1.0", "fast-check": "^4.9.0", "find-my-way-ts": "^0.1.6", "ini": "^7.0.0", "kubernetes-types": "^1.30.0", "msgpackr": "^2.0.4", "multipasta": "^0.2.8", "toml": "^4.1.2", "uuid": "^14.0.1", "yaml": "^2.9.0" } }, "sha512-oz+bsG5h+6RNrw4t5GMfQrk/xBS8ROoqkYsuvRhBr5O7mCOrpvH/hbw+QrDzvKIpX4HJClwm86F94c87W0sJxg=="], + "effect": ["effect@4.0.0-beta.101", "", { "dependencies": { "@standard-schema/spec": "^1.1.0", "fast-check": "^4.9.0", "find-my-way-ts": "^0.1.6", "ini": "^7.0.0", "kubernetes-types": "^1.30.0", "msgpackr": "^2.0.4", "multipasta": "^0.2.8", "toml": "^4.1.2", "uuid": "^14.0.1", "yaml": "^2.9.0" } }, "sha512-HjowumlIo+orthn4jMlEJPuzIYPBV+uq/XiciHWhiedLsXQpWHdNJHO5d59BVDP5s1LPuvERcktwFqRXnJqnhA=="], + + "emoji-regex": ["emoji-regex@10.6.0", "", {}, "sha512-toUI84YS5YmxW219erniWD0CIVOo46xGKColeNQRgOzDorgBi1v4D71/OFzgD9GO2UGKIv1C3Sp8DAn0+j5w7A=="], "enquirer": ["enquirer@2.4.1", "", { "dependencies": { "ansi-colors": "^4.1.1", "strip-ansi": "^6.0.1" } }, "sha512-rRqJg/6gd538VHvR3PSrdRBb/1Vy2YfzHqzvbhGIQpDRKIa4FgV/54b5Q1xYSxOOwKvjXweS26E0Q+nAMwp2pQ=="], @@ -427,6 +452,8 @@ "fsevents": ["fsevents@2.3.3", "", { "os": "darwin" }, "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw=="], + "get-east-asian-width": ["get-east-asian-width@1.6.0", "", {}, "sha512-QRbvDIbx6YklUe6RxeTeleMR0yv3cYH6PsPZHcnVn7xv7zO1BHN8r0XETu8n6Ye3Q+ahtSarc3WgtNWmehIBfA=="], + "glob-parent": ["glob-parent@5.1.2", "", { "dependencies": { "is-glob": "^4.0.1" } }, "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow=="], "globby": ["globby@11.1.0", "", { "dependencies": { "array-union": "^2.1.0", "dir-glob": "^3.0.1", "fast-glob": "^3.2.9", "ignore": "^5.2.0", "merge2": "^1.4.1", "slash": "^3.0.0" } }, "sha512-jhIXaOzy1sb8IyocaruWSn1TjmnBVs8Ayhcy83rmxNJ8q2uWKCAj3CnJY+KpGSXCueAPc0i05kVvVKtP1t9S3g=="], @@ -491,6 +518,8 @@ "magic-string": ["magic-string@0.30.21", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.5" } }, "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ=="], + "marked": ["marked@17.0.1", "", { "bin": { "marked": "bin/marked.js" } }, "sha512-boeBdiS0ghpWcSwoNm/jJBwdpFaMnZWRzjA6SkUMYb40SVaN1x7mmfGKp0jvexGcx+7y2La5zRZsYFZI6Qpypg=="], + "merge2": ["merge2@1.4.1", "", {}, "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg=="], "micromatch": ["micromatch@4.0.8", "", { "dependencies": { "braces": "^3.0.3", "picomatch": "^2.3.1" } }, "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA=="], @@ -609,7 +638,9 @@ "std-env": ["std-env@4.2.0", "", {}, "sha512-oCUKSupKTHX53EyjDtuZQ64pjLJ6yYCtpmEw0goYxtjG9KpbRe8KAsl2tBUGU9DyMcJ0RwJ8GqJAFzMXcXW1Rw=="], - "strip-ansi": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="], + "string-width": ["string-width@7.2.0", "", { "dependencies": { "emoji-regex": "^10.3.0", "get-east-asian-width": "^1.0.0", "strip-ansi": "^7.1.0" } }, "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ=="], + + "strip-ansi": ["strip-ansi@7.1.2", "", { "dependencies": { "ansi-regex": "^6.0.1" } }, "sha512-gmBGslpoQJtgnMAvOVqGZpEz9dyoKTCzy2nfz/n8aIFhN/jCE/rCmcxabB6jOOHV+0WNnylOxaxBQPSvcWklhA=="], "strip-bom": ["strip-bom@3.0.0", "", {}, "sha512-vavAMRXOgBVNF6nyEEmL3DBK19iRpDcoIwW+swQ+CbGiu7lju6t+JklA1MHweoWtadgt4ISVUsXLyDq34ddcwA=="], @@ -643,6 +674,8 @@ "vitest": ["vitest@4.1.10", "", { "dependencies": { "@vitest/expect": "4.1.10", "@vitest/mocker": "4.1.10", "@vitest/pretty-format": "4.1.10", "@vitest/runner": "4.1.10", "@vitest/snapshot": "4.1.10", "@vitest/spy": "4.1.10", "@vitest/utils": "4.1.10", "es-module-lexer": "^2.0.0", "expect-type": "^1.3.0", "magic-string": "^0.30.21", "obug": "^2.1.1", "pathe": "^2.0.3", "picomatch": "^4.0.3", "std-env": "^4.0.0-rc.1", "tinybench": "^2.9.0", "tinyexec": "^1.0.2", "tinyglobby": "^0.2.15", "tinyrainbow": "^3.1.0", "vite": "^6.0.0 || ^7.0.0 || ^8.0.0", "why-is-node-running": "^2.3.0" }, "peerDependencies": { "@edge-runtime/vm": "*", "@opentelemetry/api": "^1.9.0", "@types/node": "^20.0.0 || ^22.0.0 || >=24.0.0", "@vitest/browser-playwright": "4.1.10", "@vitest/browser-preview": "4.1.10", "@vitest/browser-webdriverio": "4.1.10", "@vitest/coverage-istanbul": "4.1.10", "@vitest/coverage-v8": "4.1.10", "@vitest/ui": "4.1.10", "happy-dom": "*", "jsdom": "*" }, "optionalPeers": ["@edge-runtime/vm", "@opentelemetry/api", "@types/node", "@vitest/browser-playwright", "@vitest/browser-preview", "@vitest/browser-webdriverio", "@vitest/coverage-istanbul", "@vitest/coverage-v8", "@vitest/ui", "happy-dom", "jsdom"], "bin": { "vitest": "./vitest.mjs" } }, "sha512-R9jUTe5S4Qb0HCd4TNqpC7oGcrMssMRGXLW80ubjWsW9VH5GF8y1Y0SFLY9AbqSk6nt0PnOx4H4WNJYZ13GUPw=="], + "web-tree-sitter": ["web-tree-sitter@0.25.10", "", { "peerDependencies": { "@types/emscripten": "^1.40.0" }, "optionalPeers": ["@types/emscripten"] }, "sha512-Y09sF44/13XvgVKgO2cNDw5rGk6s26MgoZPXLESvMXeefBf7i6/73eFurre0IsTW6E14Y0ArIzhUMmjoc7xyzA=="], + "which": ["which@2.0.2", "", { "dependencies": { "isexe": "^2.0.0" }, "bin": { "node-which": "./bin/node-which" } }, "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA=="], "why-is-node-running": ["why-is-node-running@2.3.0", "", { "dependencies": { "siginfo": "^2.0.0", "stackback": "0.0.2" }, "bin": { "why-is-node-running": "cli.js" } }, "sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w=="], @@ -659,6 +692,10 @@ "@manypkg/get-packages/fs-extra": ["fs-extra@8.1.0", "", { "dependencies": { "graceful-fs": "^4.2.0", "jsonfile": "^4.0.0", "universalify": "^0.1.0" } }, "sha512-yhlQgA6mnOJUKOsRUFsgJdQCvkKhcz8tlZG5HBQfReYZy46OwLcY+Zia0mtdHsOo9y/hP+CxMN0TU9QxoOtG4g=="], + "bun-ffi-structs/typescript": ["typescript@5.8.2", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-aJn6wq13/afZp/jT9QZmwEjDqqvSGp1VT5GVg+f/t6/oVyrgXM6BY1h9BRh/O5p3PlUPAe+WuiEZOmb/49RqoQ=="], + + "enquirer/strip-ansi": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="], + "micromatch/picomatch": ["picomatch@2.3.2", "", {}, "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA=="], "opencode-drive/@types/bun": ["@types/bun@1.3.13", "", { "dependencies": { "bun-types": "1.3.13" } }, "sha512-9fqXWk5YIHGGnUau9TEi+qdlTYDAnOj+xLCmSTwXfAIqXr2x4tytJb43E9uCvt09zJURKXwAtkoH4nLQfzeTXw=="], @@ -667,6 +704,8 @@ "read-yaml-file/js-yaml": ["js-yaml@3.15.0", "", { "dependencies": { "argparse": "^1.0.7", "esprima": "^4.0.0" }, "bin": { "js-yaml": "bin/js-yaml.js" } }, "sha512-ttBQIIQPDeLjpPOohtUdXuXUVoA2uIB6fEH9HyJ7234s5mBJ5wTx20njxplLZQgLaOfpmPQA7X2t5AX6tIPbog=="], + "enquirer/strip-ansi/ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="], + "opencode-drive/@types/bun/bun-types": ["bun-types@1.3.13", "", { "dependencies": { "@types/node": "*" } }, "sha512-QXKeHLlOLqQX9LgYaHJfzdBaV21T63HhFJnvuRCcjZiaUDpbs5ED1MgxbMra71CsryN/1dAoXuJJJwIv/2drVA=="], "read-yaml-file/js-yaml/argparse": ["argparse@1.0.10", "", { "dependencies": { "sprintf-js": "~1.0.2" } }, "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg=="], diff --git a/packages/drive/README.md b/packages/drive/README.md index f438097..0210f01 100644 --- a/packages/drive/README.md +++ b/packages/drive/README.md @@ -24,8 +24,9 @@ npx skills add anomalyco/opencode-drive --agent opencode --skill opencode-drive ## Effect programs The primary way to automate OpenCode is a default-exported, fully provided -Effect. Drive type-checks the module contract before importing it, validates the -export at runtime, and runs it in the CLI's Effect runtime: +Effect. Drive type-checks the module contract, compiles the script and its local +imports against the launching Drive toolchain, then validates and runs the +export in an isolated Bun process: ```ts // drive.ts @@ -248,8 +249,7 @@ generated script is ready for `opencode-drive check ./drive.ts` and `start --script ./drive.ts`. ```ts -import { Effect } from "effect" -import { defineScript, Llm } from "opencode-drive" +import { defineScript, Effect, Llm } from "opencode-drive" export default defineScript({ config: { @@ -405,11 +405,11 @@ Type-check every new or edited script before running it: opencode-drive check ./drive.ts ``` -Drive temporarily exposes its script API and `tsgo` beside the script while -checking, then removes only the links it created. When it detects an old -Promise-style `setup`, `run`, or `ui.waitFor` callback, it prints the equivalent -Effect shape after the TypeScript diagnostics. Use `Effect.sleep(milliseconds)` -for unconditional delays. +Drive resolves its script API, Effect, Bun declarations, and `tsgo` from the +launching installation without installing packages or modifying the script's +directory. When it detects an old Promise-style `setup`, `run`, or `ui.waitFor` +callback, it prints the equivalent Effect shape after the TypeScript +diagnostics. Use `Effect.sleep(milliseconds)` for unconditional delays. The `fs`, `ui`, `llm`, `tools`, `server`, and `tuis` capabilities expose Effect-returning operations. Compose them with `yield*`, `Effect.flatMap`, or @@ -437,8 +437,8 @@ export default defineScript({ }) ``` -Only one server may be launched per script. All TUIs share its LLM backend. TUI processes and temporary -script links are cleaned up when the script ends. +Only one server may be launched per script. All TUIs share its LLM backend. TUI +processes and compiled script artifacts are cleaned up when the script ends. `yield* server.kill()` stops the server so it can be launched again later. `yield* tui.close()` closes a TUI, after which its name may be reused. diff --git a/packages/drive/package.json b/packages/drive/package.json index d535df2..05cfeee 100644 --- a/packages/drive/package.json +++ b/packages/drive/package.json @@ -58,19 +58,20 @@ "release:validate": "bun run check && bun run test && bun pm pack --dry-run" }, "dependencies": { - "@effect/platform-node": "4.0.0-beta.98", - "@effect/platform-node-shared": "4.0.0-beta.98", + "@effect/platform-node": "4.0.0-beta.101", + "@effect/platform-node-shared": "4.0.0-beta.101", "@napi-rs/canvas": "1.0.2", - "@opencode-ai/client": "0.0.0-next-15747", + "@opencode-ai/client": "0.0.0-next-16543", + "@opentui/core": "0.4.5", + "@types/bun": "1.3.13", + "@typescript/native-preview": "7.0.0-dev.20251207.1", "@wterm/core": "0.3.0", "@wterm/ghostty": "0.3.0", - "effect": "4.0.0-beta.98" + "effect": "4.0.0-beta.101" }, "devDependencies": { - "@effect/vitest": "4.0.0-beta.98", + "@effect/vitest": "4.0.0-beta.101", "@tsconfig/bun": "1.0.9", - "@types/bun": "1.3.13", - "@typescript/native-preview": "7.0.0-dev.20251207.1", "oxlint": "1.60.0", "oxlint-tsgolint": "0.21.0", "typescript": "5.8.2", diff --git a/packages/drive/src/cli/index.ts b/packages/drive/src/cli/index.ts index 120975a..403e043 100644 --- a/packages/drive/src/cli/index.ts +++ b/packages/drive/src/cli/index.ts @@ -1,5 +1,6 @@ #!/usr/bin/env bun -import { NodeRuntime, NodeServices } from "@effect/platform-node" +import * as NodeRuntime from "@effect/platform-node/NodeRuntime" +import * as NodeServices from "@effect/platform-node/NodeServices" import { Effect, Option } from "effect" import { Argument, Command, Flag } from "effect/unstable/cli" import packageJson from "../../package.json" with { type: "json" } diff --git a/packages/drive/src/cli/run.ts b/packages/drive/src/cli/run.ts index a8f47d3..8dc65d6 100644 --- a/packages/drive/src/cli/run.ts +++ b/packages/drive/src/cli/run.ts @@ -1,63 +1,32 @@ import { mkdtemp, rm } from "node:fs/promises" import { tmpdir } from "node:os" import { join, resolve } from "node:path" -import { pathToFileURL } from "node:url" import * as Effect from "effect/Effect" -import { - prepareScriptTooling, - typecheckPreparedTooling, -} from "../script/tooling.js" +import * as Process from "../instance/process.js" +import { prepareProgram } from "../script/tooling.js" export const runProgram = Effect.fn("Cli.runProgram")((file: string) => Effect.acquireUseRelease( Effect.tryPromise({ - try: () => prepareProgram(resolve(file)), + try: () => mkdtemp(join(tmpdir(), "opencode-drive-run-")), catch: (cause) => cause, }), - ({ file }) => + (artifacts) => Effect.gen(function* () { - const module = yield* Effect.tryPromise({ - try: () => import(pathToFileURL(file).href), + const runner = yield* Effect.tryPromise({ + try: () => prepareProgram(artifacts, resolve(file)), catch: (cause) => cause, }) - if (!Effect.isEffect(module.default)) - return yield* Effect.fail( - new Error("program must default-export a fully provided Effect"), - ) - return yield* module.default + const result = yield* Process.run([process.execPath, runner], { + extendEnv: true, + stdin: "inherit", + stdout: "inherit", + stderr: "inherit", + }) + if (result.status !== 0) + return yield* Effect.fail(new Error(`program exited with status ${result.status}`)) + return undefined }), - ({ remove }) => Effect.promise(remove), + (artifacts) => Effect.promise(() => rm(artifacts, { recursive: true, force: true })), ), ) - -async function prepareProgram(file: string) { - const artifacts = await mkdtemp(join(tmpdir(), "opencode-drive-run-")) - const contract = join(artifacts, "program-contract.ts") - let links: Awaited>["links"] | undefined - try { - await Bun.write( - contract, - [ - 'import type * as Effect from "effect/Effect"', - `import program from ${JSON.stringify(file)}`, - "const contract: Effect.Effect = program", - "void contract", - "", - ].join("\n"), - ) - const tooling = await prepareScriptTooling(artifacts, contract, file) - links = tooling.links - await typecheckPreparedTooling(tooling, artifacts, "program") - return { - file, - remove: async () => { - await links?.remove() - await rm(artifacts, { recursive: true, force: true }) - }, - } - } catch (error) { - await links?.remove() - await rm(artifacts, { recursive: true, force: true }) - throw error - } -} diff --git a/packages/drive/src/cli/script-init.ts b/packages/drive/src/cli/script-init.ts index a955527..ec86823 100644 --- a/packages/drive/src/cli/script-init.ts +++ b/packages/drive/src/cli/script-init.ts @@ -2,8 +2,7 @@ import { mkdir, open, rm } from "node:fs/promises" import { dirname, resolve } from "node:path" import { logSuccess } from "../log.js" -const template = `import { Effect } from "effect" -import { defineScript, Llm } from "opencode-drive" +const template = `import { defineScript, Effect, Llm } from "opencode-drive" export default defineScript({ project: { diff --git a/packages/drive/src/cli/start.ts b/packages/drive/src/cli/start.ts index 6b1cbf9..464e94a 100644 --- a/packages/drive/src/cli/start.ts +++ b/packages/drive/src/cli/start.ts @@ -12,7 +12,7 @@ import { connectMockBackend } from "./mock-backend.js" import { createResponseSettings } from "./response-generator.js" import { loadScript, runScript } from "./script.js" import type { ScriptDefinition } from "../script/types.js" -import { prepareScriptTooling } from "../script/tooling.js" +import { prepareScriptModule } from "../script/tooling.js" import { finalizeRecording } from "../recording/finalize.js" import { listenControl } from "../instance/control.js" import { configureLogFile, logError, logReadyPaths, logSuccess } from "../log.js" @@ -57,19 +57,15 @@ const startScoped = Effect.fn("DriveCli.startScoped")(function* (options: StartO if (!options.visible && !options.script && !options.daemon) return yield* startDetached(options, initialized.artifacts) const scriptPath = options.script - const scriptTooling = scriptPath - ? yield* Effect.acquireRelease( - fromPromise(async () => { - logSuccess(`preparing script ${scriptPath}`) - return prepareScriptTooling(initialized.artifacts, scriptPath) - }), - (tooling) => fromPromise(() => tooling.links.remove()).pipe(Effect.ignore), - ) + const scriptModule = scriptPath + ? yield* fromPromise(async () => { + logSuccess(`preparing script ${scriptPath}`) + return prepareScriptModule(initialized.artifacts, scriptPath) + }) : undefined - const script = scriptTooling - ? yield* loadScript(scriptTooling.file).pipe( - Effect.tap(() => Effect.sync(() => logSuccess(`loading script ${scriptTooling.file}`))), - Effect.onError(() => fromPromise(() => scriptTooling.links.remove()).pipe(Effect.ignore)), + const script = scriptModule + ? yield* loadScript(scriptModule).pipe( + Effect.tap(() => Effect.sync(() => logSuccess(`loading script ${scriptModule}`))), ) : undefined if (script && "launch" in script && options.record) { @@ -111,7 +107,7 @@ const startScoped = Effect.fn("DriveCli.startScoped")(function* (options: StartO ), () => fromPromise(() => unregister(options.name, process.pid)).pipe(Effect.ignore), ) - return yield* lifecycle(options, instance, responses, script, scriptTooling, log) + return yield* lifecycle(options, instance, responses, script, log) }) function lifecycle( @@ -119,12 +115,11 @@ function lifecycle( instance: OpenCodeInstance.Instance, responses: ReturnType, script: ScriptDefinition | undefined, - scriptTooling: Awaited> | undefined, log: (message: string) => void, ) { return Effect.callback((resume) => { const abort = new AbortController() - const promise = runLifecycle(options, instance, responses, script, scriptTooling, log, abort.signal) + const promise = runLifecycle(options, instance, responses, script, log, abort.signal) void promise.then( () => resume(Effect.void), (error) => resume(Effect.fail(error)), @@ -141,7 +136,6 @@ async function runLifecycle( instance: OpenCodeInstance.Instance, responses: ReturnType, script: ScriptDefinition | undefined, - scriptTooling: Awaited> | undefined, log: (message: string) => void, signal: AbortSignal, ) { @@ -331,10 +325,6 @@ async function runLifecycle( cleanupFailure ??= error logError(`failed to unregister ${options.name}: ${error}`) }) - await scriptTooling?.links.remove().catch((error) => { - cleanupFailure ??= error - logError(`failed to remove script tooling: ${error}`) - }) if (options.script && !options.visible) report(completed ? "completed" : undefined) if (options.script && recordingPath) logSuccess(`recording ${recordingPath}`) if (options.script) diff --git a/packages/drive/src/driver/index.ts b/packages/drive/src/driver/index.ts index d2d699c..cdadcef 100644 --- a/packages/drive/src/driver/index.ts +++ b/packages/drive/src/driver/index.ts @@ -3,7 +3,7 @@ import * as Cause from "effect/Cause" import * as Exit from "effect/Exit" import * as Layer from "effect/Layer" import type * as Scope from "effect/Scope" -import { NodeServices } from "@effect/platform-node" +import * as NodeServices from "@effect/platform-node/NodeServices" import * as OpenCodeInstance from "../instance/runtime.js" import * as SimulationConnector from "../simulation/connector.js" import type { diff --git a/packages/drive/src/driver/opencode.ts b/packages/drive/src/driver/opencode.ts index 433aa0e..cae9bec 100644 --- a/packages/drive/src/driver/opencode.ts +++ b/packages/drive/src/driver/opencode.ts @@ -1,5 +1,5 @@ import { join } from "node:path" -import { NodeFileSystem } from "@effect/platform-node" +import * as NodeFileSystem from "@effect/platform-node/NodeFileSystem" import { OpenCode as OpenCodeService, type OpenCodeClient, diff --git a/packages/drive/src/index.ts b/packages/drive/src/index.ts index fbbb0c8..53ac01c 100644 --- a/packages/drive/src/index.ts +++ b/packages/drive/src/index.ts @@ -1,4 +1,5 @@ export * from "./script/index.js" +export * as Effect from "effect/Effect" export * as Llm from "./llm/index.js" export * as OpenCodeDriver from "./driver/index.js" export * as Errors from "./script/errors.js" diff --git a/packages/drive/src/instance/default-config.jsonc b/packages/drive/src/instance/default-config.jsonc index 809b4a1..f2d2d3c 100644 --- a/packages/drive/src/instance/default-config.jsonc +++ b/packages/drive/src/instance/default-config.jsonc @@ -11,14 +11,9 @@ "providers": { "simulation": { "name": "Simulation", - "package": "aisdk:@ai-sdk/openai-compatible", + "package": "@opencode-ai/ai/providers/openai/chat", "settings": { - "baseURL": "https://api.openai.com/v1" - }, - "request": { - "body": { - "apiKey": "sim-key" - } + "apiKey": "sim-key" }, "models": { "gpt-sim-model": { diff --git a/packages/drive/src/instance/dev.ts b/packages/drive/src/instance/dev.ts index ce5f2e8..9ffe966 100644 --- a/packages/drive/src/instance/dev.ts +++ b/packages/drive/src/instance/dev.ts @@ -14,7 +14,7 @@ export const prepareDev = Effect.fn("OpenCodeInstance.prepareDev")(function* ( const root = resolve(directory) const entrypoint = join(root, "packages", "cli", "src", "index.ts") const solid = join(root, "packages", "tui", "node_modules", "@opentui", "solid") - yield* Effect.tryPromise({ + const standalone = yield* Effect.tryPromise({ try: async () => { if (!(await Bun.file(entrypoint).exists())) throw new Error(`OpenCode development entrypoint not found: ${entrypoint}`) @@ -26,13 +26,18 @@ export const prepareDev = Effect.fn("OpenCodeInstance.prepareDev")(function* ( }) await rm(preload, { recursive: true, force: true }) await symlink(solid, preload, "dir") + return Bun.file(join(root, "packages", "cli", "src", "services", "standalone.ts")).exists() }, catch: (cause) => instanceError("prepare development checkout", cause), }) - return [ - process.execPath, + const preloads = [ "--conditions=browser", - "--preload=@opentui/solid/preload", - entrypoint, + `--preload=${join(solid, "scripts", "preload.js")}`, ] + const base = [process.execPath, ...preloads, entrypoint] + return { + command: [...base, ...(standalone ? ["--standalone"] : [])], + scriptedCommand: base, + preloads, + } }) diff --git a/packages/drive/src/instance/instance.ts b/packages/drive/src/instance/instance.ts index 77a5978..11a8638 100644 --- a/packages/drive/src/instance/instance.ts +++ b/packages/drive/src/instance/instance.ts @@ -75,7 +75,7 @@ export const prepareInstanceProject = Effect.fn("OpenCodeInstance.prepareProject if (options.setup !== undefined) { const protectGit = Boolean(options.project?.git) || (yield* promise(() => hasGitMetadata(files))) - const setup: unknown = options.setup({ + const setup: Effect.Effect = options.setup({ fs: createScriptFileSystem(files, { git: protectGit }), config, tuiConfig: tui, diff --git a/packages/drive/src/instance/runtime.ts b/packages/drive/src/instance/runtime.ts index dc66a45..fe6d316 100644 --- a/packages/drive/src/instance/runtime.ts +++ b/packages/drive/src/instance/runtime.ts @@ -140,13 +140,20 @@ export const make = Effect.fn("OpenCodeInstance.make")(function* ( }).pipe( Effect.mapError((cause) => instanceError("prepare project", cause)), ) + const dev = options.dev !== undefined + ? yield* prepareDev(artifacts, options.dev) + : undefined const environment = stripGitEnvironment({ ...process.env, ...options.env, + BUN_OPTIONS: dev === undefined + ? options.env?.BUN_OPTIONS ?? process.env.BUN_OPTIONS + : [options.env?.BUN_OPTIONS ?? process.env.BUN_OPTIONS, ...dev.preloads].filter(Boolean).join(" "), OPENCODE_SIMULATE: "1", OPENCODE_DRIVE_SCRIPTED: options.scripted ? "1" : undefined, DRIVE_REGISTRY_DIR: drive, OPENCODE_DRIVE_RENDERER: options.visible ? "visible" : "headless", + OPENCODE_DRIVE_DEV: options.dev, OPENCODE_CONFIG_DIR: join(files, ".opencode"), OPENCODE_DB: database, OPENCODE_LOG_LEVEL: !options.visible ? "DEBUG" : process.env.OPENCODE_LOG_LEVEL, @@ -156,11 +163,12 @@ export const make = Effect.fn("OpenCodeInstance.make")(function* ( XDG_DATA_HOME: logs, XDG_STATE_HOME: join(artifacts, "home", ".local", "state"), }) - const command = options.dev !== undefined - ? yield* prepareDev(artifacts, options.dev) + const command = dev !== undefined + ? dev.command : options.command?.length ? [...options.command] : ["opencode2"] + const scriptedCommand = dev?.scriptedCommand ?? command const initialRecording = options.record ? recordingPaths(media) : undefined const processSpawner = yield* ChildProcessSpawner.ChildProcessSpawner const fileSystem = yield* FileSystem.FileSystem @@ -250,12 +258,19 @@ export const make = Effect.fn("OpenCodeInstance.make")(function* ( if (current.server !== undefined || current.pendingServer !== undefined) return yield* Effect.fail(instanceError("launch server", "the script server has already been launched")) const name = processDriveName(options.name, "service") + const serverCommand = [ + ...scriptedCommand, + "serve", + "--service", + "--port", + String(yield* freePort), + ] yield* writeManifest(name, { ui: `ws://127.0.0.1:${yield* freePort}`, backend: endpoints.backend, }) options.log?.("launching script server") - const server = yield* spawn(name, [...command, "serve", "--service"], "service", false) + const server = yield* spawn(name, serverCommand, "service", false) yield* Ref.update(state, (value) => ({ ...value, pendingServer: server })) return server }), @@ -364,7 +379,7 @@ export const make = Effect.fn("OpenCodeInstance.make")(function* ( tuiOptions.viewport ?? options.viewport, ) options.log?.(`launching TUI ${name}`) - const tui = yield* spawn(driveName, command, `tui-${name}`, options.visible ?? false) + const tui = yield* spawn(driveName, scriptedCommand, `tui-${name}`, options.visible ?? false) yield* Ref.update(state, (value) => ({ ...value, pendingTuis: new Map(value.pendingTuis).set(name, tui), diff --git a/packages/drive/src/recording/ffmpeg.ts b/packages/drive/src/recording/ffmpeg.ts index 5921f86..ba3bc22 100644 --- a/packages/drive/src/recording/ffmpeg.ts +++ b/packages/drive/src/recording/ffmpeg.ts @@ -1,4 +1,4 @@ -import { NodeServices } from "@effect/platform-node" +import * as NodeServices from "@effect/platform-node/NodeServices" import * as Effect from "effect/Effect" import * as Process from "../instance/process.js" diff --git a/packages/drive/src/script/project.ts b/packages/drive/src/script/project.ts index 29b428e..2639ffa 100644 --- a/packages/drive/src/script/project.ts +++ b/packages/drive/src/script/project.ts @@ -1,7 +1,7 @@ import { lstat, readdir, rm } from "node:fs/promises" import { devNull } from "node:os" import { join } from "node:path" -import { NodeServices } from "@effect/platform-node" +import * as NodeServices from "@effect/platform-node/NodeServices" import * as Effect from "effect/Effect" import * as Process from "../instance/process.js" import { writeScriptFiles } from "./filesystem.js" diff --git a/packages/drive/src/script/tooling.ts b/packages/drive/src/script/tooling.ts index 8641fa9..6fa1d23 100644 --- a/packages/drive/src/script/tooling.ts +++ b/packages/drive/src/script/tooling.ts @@ -1,48 +1,99 @@ -import { lstat, mkdir, readlink, rm, rmdir, stat, symlink } from "node:fs/promises" +import { mkdir, rm, symlink } from "node:fs/promises" +import { createRequire } from "node:module" import { dirname, join, resolve } from "node:path" import { fileURLToPath } from "node:url" -import { NodeServices } from "@effect/platform-node" -import * as Effect from "effect/Effect" -import * as Process from "../instance/process.js" const packageRoot = resolve(fileURLToPath(new URL("../..", import.meta.url))) +const requireFromDrive = createRequire(join(packageRoot, "package.json")) +const effectRoot = dirname(requireFromDrive.resolve("effect/package.json")) +const compilerRoot = dirname(requireFromDrive.resolve("@typescript/native-preview/package.json")) +const bunTypesRoot = dirname(requireFromDrive.resolve("@types/bun/package.json")) +const requireFromBunTypes = createRequire(join(bunTypesRoot, "package.json")) +const bunRuntimeTypesRoot = dirname(requireFromBunTypes.resolve("bun-types/package.json")) -export async function prepareScriptTooling( - artifacts: string, - script: string, - dependencyEntry: string = script, -) { - const file = resolve(script) - if (!(await Bun.file(file).exists())) throw new Error(`script not found: ${file}`) - const packageJson: unknown = await Bun.file(join(packageRoot, "package.json")).json() - if (!isPackageMetadata(packageJson)) - throw new Error("opencode-drive package metadata is missing script dependencies") - const dependencies = { - "opencode-drive": `file:${packageRoot}`, - effect: packageJson.dependencies.effect, - "@typescript/native-preview": - packageJson.devDependencies["@typescript/native-preview"], - "@types/bun": packageJson.devDependencies["@types/bun"], - } - const manifest = join(artifacts, "package.json") - const contents = `${JSON.stringify({ private: true, dependencies }, undefined, 2)}\n` - if (!(await Bun.file(manifest).exists()) || (await Bun.file(manifest).text()) !== contents) { - await Bun.write(manifest, contents) - } - if (!(await linkLocalTooling(artifacts))) { - const install = await runProcess( - [process.execPath, "install"], - artifacts, - { stdout: "ignore" }, - ) - if (install.status !== 0) - throw new Error( - `bun install failed in ${artifacts}: ${install.stderr.trim()}`, - ) +type Contract = "program" | "script" + +export async function prepareScriptModule(artifacts: string, script: string) { + const source = await resolveSource(script, "script") + const root = await prepareRuntimeRoot(artifacts) + return compileScript(root, source) +} + +export async function prepareProgram(artifacts: string, script: string) { + const source = await resolveSource(script, "program") + const root = await prepareRuntimeRoot(artifacts) + await typecheck(root, source, "program") + const program = await compileScript(root, source) + const runner = join(root, "program-runner.ts") + await Bun.write( + runner, + [ + 'import * as Effect from "effect/Effect"', + `import program from ${JSON.stringify(program)}`, + 'if (!Effect.isEffect(program)) throw new Error("program must default-export a fully provided Effect")', + "await Effect.runPromise(program)", + "", + ].join("\n"), + ) + return runner +} + +export async function checkScript(artifacts: string, script: string) { + const source = await resolveSource(script, "script") + const root = await prepareRuntimeRoot(artifacts) + await typecheck(root, source, "script") +} + +async function resolveSource(script: string, contract: Contract) { + const source = resolve(script) + if (!(await Bun.file(source).exists())) throw new Error(`${contract} not found: ${source}`) + return source +} + +async function compileScript(root: string, source: string) { + const entry = join(root, "script-entry.ts") + const output = join(root, "compiled") + await Bun.write(entry, `export { default } from ${JSON.stringify(source)}\n`) + const result = await Bun.build({ + entrypoints: [entry], + outdir: output, + target: "bun", + packages: "bundle", + external: ["opencode-drive", "opencode-drive/*", "effect", "effect/*"], + sourcemap: "inline", + }) + if (!result.success) { + const diagnostics = result.logs.map((log) => log.message).join("\n") + throw new Error(diagnostics || `failed to compile script: ${source}`) } + const file = result.outputs.find((item) => item.kind === "entry-point")?.path + if (!file) throw new Error(`script compilation produced no entry point: ${source}`) + return file +} +async function typecheck(root: string, source: string, contract: Contract) { + const contractFile = join(root, `${contract}-contract.ts`) + const tsconfig = join(root, `${contract}-tsconfig.json`) + await Bun.write( + contractFile, + contract === "program" + ? [ + 'import type * as Effect from "effect/Effect"', + `import program from ${JSON.stringify(source)}`, + "const checked: Effect.Effect = program", + "void checked", + "", + ].join("\n") + : [ + 'import type { ScriptDefinition } from "opencode-drive/script"', + `import script from ${JSON.stringify(source)}`, + "const checked: ScriptDefinition = script", + "void checked", + "", + ].join("\n"), + ) await Bun.write( - join(artifacts, "tsconfig.json"), + tsconfig, `${JSON.stringify( { compilerOptions: { @@ -57,192 +108,54 @@ export async function prepareScriptTooling( allowImportingTsExtensions: true, skipLibCheck: true, types: ["bun"], + paths: { + "opencode-drive": [join(packageRoot, "src", "index.ts")], + "opencode-drive/*": [join(packageRoot, "src", "*")], + effect: [join(effectRoot, "dist", "index.d.ts")], + "effect/*": [join(effectRoot, "dist", "*.d.ts")], + }, }, - files: [file], + files: [contractFile], }, undefined, 2, )}\n`, ) - return { - file, - tsgo: join(artifacts, "node_modules", ".bin", "tsgo"), - tsconfig: join(artifacts, "tsconfig.json"), - links: await linkScriptDependencies(resolve(dependencyEntry), artifacts), - } -} - -async function linkLocalTooling(artifacts: string) { - const local = { - drive: packageRoot, - effect: join(packageRoot, "node_modules", "effect"), - tsgo: join(packageRoot, "node_modules", ".bin", "tsgo"), - bunTypes: join(packageRoot, "node_modules", "@types", "bun"), - } - if ( - !(await stat(local.tsgo).catch(() => undefined)) || - !(await stat(local.effect).catch(() => undefined)) || - !(await stat(local.bunTypes).catch(() => undefined)) + const capture = contract === "script" + const child = Bun.spawn( + [process.execPath, join(compilerRoot, "bin", "tsgo.js"), "-p", tsconfig], + { + cwd: root, + stdin: "ignore", + stdout: capture ? "pipe" : "inherit", + stderr: capture ? "pipe" : "inherit", + }, ) - return false - const modules = join(artifacts, "node_modules") - await Promise.all([ - mkdir(join(modules, ".bin"), { recursive: true }), - mkdir(join(modules, "@types"), { recursive: true }), + const [status, stdout, stderr] = await Promise.all([ + child.exited, + capture ? new Response(child.stdout).text() : "", + capture ? new Response(child.stderr).text() : "", ]) - const created: string[] = [] - try { - for (const [target, path] of [ - [local.drive, join(modules, "opencode-drive")], - [local.effect, join(modules, "effect")], - [local.tsgo, join(modules, ".bin", "tsgo")], - [local.bunTypes, join(modules, "@types", "bun")], - ] as const) { - await symlink(target, path) - created.push(path) - } - return true - } catch { - await Promise.all(created.map((path) => rm(path, { force: true }))) - return false - } -} - -function isPackageMetadata( - value: unknown, -): value is { - readonly dependencies: { readonly effect: string } - readonly devDependencies: Record -} { - if ( - typeof value !== "object" || - value === null || - !("dependencies" in value) || - !("devDependencies" in value) - ) - return false - const runtime = value.dependencies - const dependencies = value.devDependencies - return ( - typeof runtime === "object" && - runtime !== null && - "effect" in runtime && - typeof runtime.effect === "string" && - typeof dependencies === "object" && - dependencies !== null && - "@typescript/native-preview" in dependencies && - typeof dependencies["@typescript/native-preview"] === "string" && - "@types/bun" in dependencies && - typeof dependencies["@types/bun"] === "string" - ) -} - -export async function checkScript(artifacts: string, script: string) { - const file = resolve(script) - const contract = join(artifacts, "script-contract.ts") - await Bun.write( - contract, - [ - 'import type { ScriptDefinition } from "opencode-drive/script"', - `import script from ${JSON.stringify(file)}`, - "const contract: ScriptDefinition = script", - "void contract", - "", - ].join("\n"), - ) - const tooling = await prepareScriptTooling(artifacts, contract, file) - try { - await typecheckPreparedTooling(tooling, artifacts, "script", true) - } finally { - await tooling.links.remove() - } + if (status === 0) return + const diagnostics = [stdout, stderr] + .map((value) => value.trim()) + .filter(Boolean) + .join("\n") + throw new Error(diagnostics || `${contract} type check failed with status ${status}`) } -export async function typecheckPreparedTooling( - tooling: Awaited>, - cwd: string, - label: string, - capture: boolean = false, -) { - const output = await runProcess( - [tooling.tsgo, "-p", tooling.tsconfig], - cwd, - capture ? {} : { stdout: "inherit", stderr: "inherit" }, - ) - if (output.status !== 0) { - const diagnostics = [output.stdout, output.stderr] - .map((value) => value.trim()) - .filter(Boolean) - .join("\n") - throw new Error( - diagnostics || `${label} type check failed with status ${output.status}`, - ) - } +async function prepareRuntimeRoot(artifacts: string) { + const root = join(resolve(artifacts), "script-runtime") + const modules = join(root, "node_modules") + await rm(root, { recursive: true, force: true }) + await mkdir(join(modules, "@types"), { recursive: true }) + await Promise.all([ + symlink(packageRoot, join(modules, "opencode-drive"), linkType), + symlink(effectRoot, join(modules, "effect"), linkType), + symlink(bunTypesRoot, join(modules, "@types", "bun"), linkType), + symlink(bunRuntimeTypesRoot, join(modules, "bun-types"), linkType), + ]) + return root } -const runProcess = ( - command: ReadonlyArray, - cwd: string, - options: Omit = {}, -) => - Effect.runPromise( - Process.run(command, { ...options, cwd }).pipe( - Effect.provide(NodeServices.layer), - ), - ) - -async function linkScriptDependencies(script: string, artifacts: string) { - const modules = join(dirname(script), "node_modules") - const created: Array<{ readonly path: string; readonly target: string }> = [] - const directories: string[] = [] - const add = async (path: string, target: string) => { - const existing = await lstat(path).catch(() => undefined) - if (existing) { - const previous = existing.isSymbolicLink() - ? await readlink(path).catch(() => undefined) - : undefined - if ( - previous !== undefined && - /(?:^|[\\/])opencode-drive[\\/]run-/.test(previous) && - !(await Bun.file(previous).exists()) - ) { - await rm(path, { force: true }) - } else { - return - } - } - const parent = dirname(path) - if (!(await lstat(parent).catch(() => undefined))) { - await mkdir(parent, { recursive: true }) - directories.push(parent) - } - await symlink(target, path) - created.push({ path, target }) - } - const remove = async () => { - for (const link of created.reverse()) { - const target = await readlink(link.path).catch(() => undefined) - if (target === link.target) await rm(link.path, { force: true }) - } - for (const path of directories.reverse()) - await rmdir(path).catch(() => undefined) - } - try { - await add( - join(modules, "opencode-drive"), - join(artifacts, "node_modules", "opencode-drive"), - ) - await add( - join(modules, "effect"), - join(artifacts, "node_modules", "effect"), - ) - await add( - join(modules, ".bin", "tsgo"), - join(artifacts, "node_modules", ".bin", "tsgo"), - ) - return { remove } - } catch (error) { - await remove() - throw error - } -} +const linkType = process.platform === "win32" ? "junction" : "dir" diff --git a/packages/drive/test/cli/integration.test.ts b/packages/drive/test/cli/integration.test.ts index 366afca..aa16f0c 100644 --- a/packages/drive/test/cli/integration.test.ts +++ b/packages/drive/test/cli/integration.test.ts @@ -121,7 +121,8 @@ describe("opencode-drive", () => { permissions: [{ action: "*", resource: "*", effect: "allow" }], providers: { simulation: { - package: "aisdk:@ai-sdk/openai-compatible", + package: "@opencode-ai/ai/providers/openai/chat", + settings: { apiKey: "sim-key" }, models: { "gpt-sim-model": { capabilities: { tools: true } } }, }, }, @@ -819,7 +820,12 @@ describe("opencode-drive", () => { expect(await realpath(await Bun.file(join(artifacts, "child-cwd.txt")).text())).toBe( await realpath(join(artifacts, "files")), ) - expect(await Bun.file(join(artifacts, "service-argv.json")).json()).toEqual(["serve", "--service"]) + expect(await Bun.file(join(artifacts, "service-argv.json")).json()).toEqual([ + "serve", + "--service", + "--port", + expect.stringMatching(/^\d+$/), + ]) const pid = Number(await Bun.file(join(artifacts, "child.pid")).text()) expect(running(pid)).toBe(false) expect(await Bun.file(join(root, "registry", `${name}.json`)).exists()).toBe(false) @@ -915,7 +921,7 @@ describe("opencode-drive", () => { expect(await Bun.file(artifacts).exists()).toBe(false) }) - test("checks a typed script and removes temporary dependency links", async () => { + test("checks a typed script without modifying its source directory", async () => { const root = await temporary() const directory = join(root, "scripts") await mkdir(directory, { recursive: true }) @@ -967,7 +973,7 @@ describe("opencode-drive", () => { expect(status).toBe(0) expect(stdout.trim()).toBe(file) const source = await Bun.file(file).text() - expect(source).toContain('import { defineScript, Llm } from "opencode-drive"') + expect(source).toContain('import { defineScript, Effect, Llm } from "opencode-drive"') expect(source).toContain('llm.queue(Llm.text("The value is 1."))') const checked = spawn(["check", file], root) @@ -1152,12 +1158,11 @@ describe("opencode-drive", () => { test.each(["setup", "run"] as const)("rejects a Promise-returning script %s callback", async (callback) => { const root = await temporary() const script = join(root, `promise-${callback}-script.js`) - const drive = JSON.stringify(resolve("src/index.ts")) await Bun.write( script, callback === "setup" - ? `import { Effect } from "effect"\nimport { defineScript } from ${drive}\nexport default defineScript({ setup: async () => {}, run: () => Effect.void })\n` - : `import { defineScript } from ${drive}\nexport default defineScript({ run: async () => {} })\n`, + ? `import { Effect } from "effect"\nimport { defineScript } from "opencode-drive"\nexport default defineScript({ setup: async () => {}, run: () => Effect.void })\n` + : `import { defineScript } from "opencode-drive"\nexport default defineScript({ run: async () => {} })\n`, ) const child = spawn( [ @@ -1185,7 +1190,7 @@ describe("opencode-drive", () => { const script = join(root, "promise-predicate-script.js") await Bun.write( script, - `import { defineScript } from ${JSON.stringify(resolve("src/index.ts"))}\nexport default defineScript({ run: ({ ui }) => ui.waitFor(() => Promise.resolve(false)) })\n`, + `import { defineScript } from "opencode-drive"\nexport default defineScript({ run: ({ ui }) => ui.waitFor(() => Promise.resolve(false)) })\n`, ) const child = spawn( [ @@ -1420,7 +1425,7 @@ describe("opencode-drive", () => { const marker = join(root, "script-interrupted") await Bun.write( script, - `import { Effect } from "effect"\nimport { defineScript } from ${JSON.stringify(resolve("src/index.ts"))}\nexport default defineScript({ run: ({ artifacts }) => Effect.never.pipe(Effect.ensuring(Effect.promise(async () => { const pid = Number(await Bun.file(artifacts + "/child.pid").text()); let running = true; try { process.kill(pid, 0) } catch { running = false }; await Bun.write(${JSON.stringify(marker)}, running ? "child-running\\n" : "child-stopped\\n") }))) })\n`, + `import { Effect } from "effect"\nimport { defineScript } from "opencode-drive"\nexport default defineScript({ run: ({ artifacts }) => Effect.never.pipe(Effect.ensuring(Effect.promise(async () => { const pid = Number(await Bun.file(artifacts + "/child.pid").text()); let running = true; try { process.kill(pid, 0) } catch { running = false }; await Bun.write(${JSON.stringify(marker)}, running ? "child-running\\n" : "child-stopped\\n") }))) })\n`, ) const child = spawn( [ diff --git a/packages/drive/test/cli/run.test.ts b/packages/drive/test/cli/run.test.ts index 52402d0..a70decd 100644 --- a/packages/drive/test/cli/run.test.ts +++ b/packages/drive/test/cli/run.test.ts @@ -15,10 +15,11 @@ describe("opencode-drive run", () => { test("type-checks and executes a default-exported Effect", async () => { const root = await temporary() const marker = join(root, "executed.txt") + await Bun.write(join(root, "helper.ts"), 'export const value = "yes\\n"\n') const program = await writeProgram( root, "valid.ts", - `import { Effect } from "effect"\nexport default Effect.promise(() => Bun.write(${JSON.stringify(marker)}, "yes\\n"))\n`, + `import { Effect } from "effect"\nimport { OpenCodeDriver } from "opencode-drive"\nimport { value } from "./helper.ts"\nvoid OpenCodeDriver\nexport default Effect.promise(() => Bun.write(${JSON.stringify(marker)}, value))\n`, ) const child = spawn(["run", program]) diff --git a/packages/drive/test/fixtures/caught-timeout-script.ts b/packages/drive/test/fixtures/caught-timeout-script.ts index 7e9389e..2d50244 100644 --- a/packages/drive/test/fixtures/caught-timeout-script.ts +++ b/packages/drive/test/fixtures/caught-timeout-script.ts @@ -1,4 +1,4 @@ -import { defineScript } from "../../src/index.js" +import { defineScript } from "opencode-drive" import * as Effect from "effect/Effect" export default defineScript({ diff --git a/packages/drive/test/fixtures/crashing-script.ts b/packages/drive/test/fixtures/crashing-script.ts index 7392871..4eb2a46 100644 --- a/packages/drive/test/fixtures/crashing-script.ts +++ b/packages/drive/test/fixtures/crashing-script.ts @@ -1,4 +1,4 @@ -import { defineScript } from "../../src/index.js" +import { defineScript } from "opencode-drive" import * as Effect from "effect/Effect" export default defineScript({ diff --git a/packages/drive/test/fixtures/failed-launch-retry-script.ts b/packages/drive/test/fixtures/failed-launch-retry-script.ts index 50726c7..b5bcbae 100644 --- a/packages/drive/test/fixtures/failed-launch-retry-script.ts +++ b/packages/drive/test/fixtures/failed-launch-retry-script.ts @@ -1,4 +1,4 @@ -import { defineScript } from "../../src/index.js" +import { defineScript } from "opencode-drive" import * as Effect from "effect/Effect" export default defineScript({ diff --git a/packages/drive/test/fixtures/fake-opencode.ts b/packages/drive/test/fixtures/fake-opencode.ts index 3aa1cff..741554c 100644 --- a/packages/drive/test/fixtures/fake-opencode.ts +++ b/packages/drive/test/fixtures/fake-opencode.ts @@ -3,7 +3,7 @@ import { resolve } from "node:path" const serviceMode = process.env.OPENCODE_DRIVE_SCRIPTED === "1" const role = serviceMode - ? process.argv.at(-2) === "serve" && process.argv.at(-1) === "--service" + ? process.argv.includes("serve") && process.argv.includes("--service") ? "service" : "client" : "legacy" diff --git a/packages/drive/test/fixtures/hanging-script.ts b/packages/drive/test/fixtures/hanging-script.ts index 8cded87..c283a9f 100644 --- a/packages/drive/test/fixtures/hanging-script.ts +++ b/packages/drive/test/fixtures/hanging-script.ts @@ -1,4 +1,4 @@ -import { defineScript } from "../../src/index.js" +import { defineScript } from "opencode-drive" import * as Effect from "effect/Effect" export default defineScript({ diff --git a/packages/drive/test/fixtures/kill-server-script.ts b/packages/drive/test/fixtures/kill-server-script.ts index 1b30661..62077fc 100644 --- a/packages/drive/test/fixtures/kill-server-script.ts +++ b/packages/drive/test/fixtures/kill-server-script.ts @@ -1,4 +1,4 @@ -import { defineScript } from "../../src/index.js" +import { defineScript } from "opencode-drive" import * as Effect from "effect/Effect" export default defineScript({ diff --git a/packages/drive/test/fixtures/manual-clients-script.ts b/packages/drive/test/fixtures/manual-clients-script.ts index 55ba3fd..e18c882 100644 --- a/packages/drive/test/fixtures/manual-clients-script.ts +++ b/packages/drive/test/fixtures/manual-clients-script.ts @@ -1,4 +1,4 @@ -import { defineScript } from "../../src/index.js" +import { defineScript } from "opencode-drive" import * as Effect from "effect/Effect" export default defineScript({ diff --git a/packages/drive/test/fixtures/prepared-git-script.ts b/packages/drive/test/fixtures/prepared-git-script.ts index bf6c4a7..a7a2bcd 100644 --- a/packages/drive/test/fixtures/prepared-git-script.ts +++ b/packages/drive/test/fixtures/prepared-git-script.ts @@ -1,5 +1,5 @@ import { join } from "node:path" -import { defineScript } from "../../src/index.js" +import { defineScript } from "opencode-drive" import * as Effect from "effect/Effect" let setupGitError: string | undefined diff --git a/packages/drive/test/fixtures/restart-script.ts b/packages/drive/test/fixtures/restart-script.ts index dd2e86b..eeb66cb 100644 --- a/packages/drive/test/fixtures/restart-script.ts +++ b/packages/drive/test/fixtures/restart-script.ts @@ -1,4 +1,4 @@ -import { defineScript, Llm } from "../../src/index.js" +import { defineScript, Llm } from "opencode-drive" import * as Effect from "effect/Effect" import * as Stream from "effect/Stream" diff --git a/packages/drive/test/fixtures/script.ts b/packages/drive/test/fixtures/script.ts index 8133641..dab2989 100644 --- a/packages/drive/test/fixtures/script.ts +++ b/packages/drive/test/fixtures/script.ts @@ -1,5 +1,5 @@ import { join } from "node:path" -import { defineScript, Llm } from "../../src/index.js" +import { defineScript, Llm } from "opencode-drive" import * as Effect from "effect/Effect" export default defineScript({ diff --git a/packages/drive/test/fixtures/serve-script.ts b/packages/drive/test/fixtures/serve-script.ts index f2c7e68..bba8e6e 100644 --- a/packages/drive/test/fixtures/serve-script.ts +++ b/packages/drive/test/fixtures/serve-script.ts @@ -1,4 +1,4 @@ -import { defineScript, Llm } from "../../src/index.js" +import { defineScript, Llm } from "opencode-drive" import * as Deferred from "effect/Deferred" import * as Effect from "effect/Effect" import * as Stream from "effect/Stream" diff --git a/packages/drive/test/fixtures/title-script.ts b/packages/drive/test/fixtures/title-script.ts index b081f10..17d3374 100644 --- a/packages/drive/test/fixtures/title-script.ts +++ b/packages/drive/test/fixtures/title-script.ts @@ -1,4 +1,4 @@ -import { defineScript, Llm } from "../../src/index.js" +import { defineScript, Llm } from "opencode-drive" import * as Effect from "effect/Effect" export default defineScript({ diff --git a/packages/drive/test/fixtures/tui-options-script.ts b/packages/drive/test/fixtures/tui-options-script.ts index 0580c2c..a2f44e0 100644 --- a/packages/drive/test/fixtures/tui-options-script.ts +++ b/packages/drive/test/fixtures/tui-options-script.ts @@ -1,4 +1,4 @@ -import { defineScript } from "../../src/index.js" +import { defineScript } from "opencode-drive" import * as Effect from "effect/Effect" export default defineScript({ diff --git a/packages/drive/test/instance/config.test.ts b/packages/drive/test/instance/config.test.ts index b8b9881..ca10ac4 100644 --- a/packages/drive/test/instance/config.test.ts +++ b/packages/drive/test/instance/config.test.ts @@ -18,6 +18,21 @@ afterEach(async () => { }) describe("instance configuration", () => { + test("initializes the simulation provider with the current OpenCode provider shape", async () => { + const root = await initializeInstance() + artifacts.push(root) + + expect(await Bun.file(join(root, "files", ".opencode", "opencode.jsonc")).json()).toMatchObject({ + model: "simulation/gpt-sim-model", + providers: { + simulation: { + package: "@opencode-ai/ai/providers/openai/chat", + settings: { apiKey: "sim-key" }, + }, + }, + }) + }) + test("merges JSONC fixtures, replaces arrays, applies setup last, and commits normalized files", async () => { const root = await initializeInstance() artifacts.push(root) diff --git a/packages/drive/test/instance/dev.test.ts b/packages/drive/test/instance/dev.test.ts new file mode 100644 index 0000000..31672c0 --- /dev/null +++ b/packages/drive/test/instance/dev.test.ts @@ -0,0 +1,34 @@ +import { afterEach, expect, test } from "vitest" +import { mkdir, mkdtemp, rm } from "node:fs/promises" +import { tmpdir } from "node:os" +import { join } from "node:path" +import * as Effect from "effect/Effect" +import { prepareDev } from "../../src/instance/dev.js" + +const directories: string[] = [] + +afterEach(async () => { + await Promise.all(directories.splice(0).map((path) => rm(path, { recursive: true, force: true }))) +}) + +test("uses standalone mode and inheritable preloads for V2 development checkouts", async () => { + const root = await mkdtemp(join(tmpdir(), "opencode-drive-dev-")) + const artifacts = await mkdtemp(join(tmpdir(), "opencode-drive-artifacts-")) + directories.push(root, artifacts) + await mkdir(join(root, "packages", "cli", "src", "services"), { recursive: true }) + await mkdir(join(root, "packages", "tui", "node_modules", "@opentui", "solid"), { recursive: true }) + await Promise.all([ + Bun.write(join(root, "packages", "cli", "src", "index.ts"), ""), + Bun.write(join(root, "packages", "cli", "src", "services", "standalone.ts"), ""), + Bun.write(join(root, "packages", "tui", "node_modules", "@opentui", "solid", "package.json"), "{}"), + ]) + + const result = await Effect.runPromise(prepareDev(artifacts, root)) + + expect(result.command.at(-1)).toBe("--standalone") + expect(result.scriptedCommand.at(-1)).toBe(join(root, "packages", "cli", "src", "index.ts")) + expect(result.preloads).toContain("--conditions=browser") + expect(result.preloads).toContain( + `--preload=${join(root, "packages", "tui", "node_modules", "@opentui", "solid", "scripts", "preload.js")}`, + ) +})