From f62ebfd7a45047dc135c932d38b99a5d656cbd4d Mon Sep 17 00:00:00 2001 From: Kit Langton Date: Wed, 29 Jul 2026 23:43:56 -0400 Subject: [PATCH] fix(drive): launch current V2 checkouts --- .changeset/fuzzy-taxis-drive.md | 5 +++ packages/drive/src/instance/dev.ts | 26 ++++++++--- packages/drive/src/instance/runtime.ts | 28 ++++++++++-- packages/drive/test/instance/dev.test.ts | 57 ++++++++++++++++++++++++ 4 files changed, 106 insertions(+), 10 deletions(-) create mode 100644 .changeset/fuzzy-taxis-drive.md create mode 100644 packages/drive/test/instance/dev.test.ts diff --git a/.changeset/fuzzy-taxis-drive.md b/.changeset/fuzzy-taxis-drive.md new file mode 100644 index 0000000..029226f --- /dev/null +++ b/.changeset/fuzzy-taxis-drive.md @@ -0,0 +1,5 @@ +--- +"opencode-drive": patch +--- + +Launch current OpenCode V2 development checkouts through their native simulation integration. diff --git a/packages/drive/src/instance/dev.ts b/packages/drive/src/instance/dev.ts index ce5f2e8..a70632f 100644 --- a/packages/drive/src/instance/dev.ts +++ b/packages/drive/src/instance/dev.ts @@ -1,5 +1,6 @@ import { mkdir, rm, symlink } from "node:fs/promises" import { join, resolve } from "node:path" +import { pathToFileURL } from "node:url" import * as Effect from "effect/Effect" import { instanceError } from "./error.js" @@ -13,8 +14,9 @@ export const prepareDev = Effect.fn("OpenCodeInstance.prepareDev")(function* ( ) { const root = resolve(directory) const entrypoint = join(root, "packages", "cli", "src", "index.ts") + const launcher = join(artifacts, "opencode-dev.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 +28,23 @@ export const prepareDev = Effect.fn("OpenCodeInstance.prepareDev")(function* ( }) await rm(preload, { recursive: true, force: true }) await symlink(solid, preload, "dir") + await Bun.write( + launcher, + `if (process.argv[2] === "serve") delete process.env.BUN_OPTIONS\nawait import(${JSON.stringify(pathToFileURL(entrypoint).href)})\n`, + ) + return Bun.file(join(root, "packages", "cli", "src", "services", "standalone.ts")).exists() }, catch: (cause) => instanceError("prepare development checkout", cause), }) - return [ - process.execPath, - "--conditions=browser", - "--preload=@opentui/solid/preload", - entrypoint, - ] + const bunOptions = ["--conditions=browser", "--preload=@opentui/solid/preload"] + return { + command: [ + process.execPath, + "--conditions=browser", + `--preload=${join(solid, "scripts", "preload.js")}`, + launcher, + ], + bunOptions, + standalone, + } }) diff --git a/packages/drive/src/instance/runtime.ts b/packages/drive/src/instance/runtime.ts index dc66a45..75a6abc 100644 --- a/packages/drive/src/instance/runtime.ts +++ b/packages/drive/src/instance/runtime.ts @@ -140,9 +140,26 @@ 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 + if (dev?.standalone && options.scripted) { + const port = yield* freePort + yield* Effect.tryPromise({ + try: () => + Bun.write( + join(files, ".opencode", "service-local.json"), + `${JSON.stringify({ port }, undefined, 2)}\n`, + ), + catch: (cause) => instanceError("configure development service", cause), + }) + } 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.bunOptions].filter(Boolean).join(" "), OPENCODE_SIMULATE: "1", OPENCODE_DRIVE_SCRIPTED: options.scripted ? "1" : undefined, DRIVE_REGISTRY_DIR: drive, @@ -156,8 +173,8 @@ 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"] @@ -231,7 +248,12 @@ export const make = Effect.fn("OpenCodeInstance.make")(function* ( ) { yield* writeManifest(options.name, endpoints, recording, options.viewport) options.log?.("launching OpenCode") - return yield* spawn(options.name, command, "opencode", options.visible ?? false) + return yield* spawn( + options.name, + dev?.standalone && !options.visible ? [...command, "--standalone"] : command, + "opencode", + options.visible ?? false, + ) }) if (!options.scripted) { diff --git a/packages/drive/test/instance/dev.test.ts b/packages/drive/test/instance/dev.test.ts new file mode 100644 index 0000000..f443e3b --- /dev/null +++ b/packages/drive/test/instance/dev.test.ts @@ -0,0 +1,57 @@ +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 { pathToFileURL } from "node:url" +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("detects standalone V2 development checkouts without changing the base command", 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", "scripts"), { 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"), + JSON.stringify({ exports: { "./preload": "./scripts/preload.js" } }), + ), + Bun.write(join(root, "packages", "tui", "node_modules", "@opentui", "solid", "scripts", "preload.js"), ""), + ]) + + const result = await Effect.runPromise(prepareDev(artifacts, root)) + + expect(result.standalone).toBe(true) + expect(result.command.at(-1)).toBe(join(artifacts, "opencode-dev.ts")) + expect(result.command).toContain( + `--preload=${join(root, "packages", "tui", "node_modules", "@opentui", "solid", "scripts", "preload.js")}`, + ) + expect(result.bunOptions).toEqual(["--conditions=browser", "--preload=@opentui/solid/preload"]) + expect(await Bun.file(join(artifacts, "opencode-dev.ts")).text()).toContain( + `await import(${JSON.stringify(pathToFileURL(join(root, "packages", "cli", "src", "index.ts")).href)})`, + ) + + const output = join(artifacts, "server-environment.json") + await Bun.write( + join(root, "packages", "cli", "src", "index.ts"), + `await Bun.write(${JSON.stringify(output)}, JSON.stringify({ args: process.argv.slice(2), bunOptions: process.env.BUN_OPTIONS }))\n`, + ) + const server = Bun.spawn([...result.command, "serve", "--service"], { + cwd: artifacts, + env: { ...process.env, BUN_OPTIONS: result.bunOptions.join(" ") }, + stderr: "pipe", + }) + const [status, stderr] = await Promise.all([server.exited, new Response(server.stderr).text()]) + expect(stderr).toBe("") + expect(status).toBe(0) + expect(await Bun.file(output).json()).toEqual({ args: ["serve", "--service"] }) +})