diff --git a/packages/opencode/src/server/instance/index.ts b/packages/opencode/src/server/instance/index.ts index 88d2c5af0..ae72a5165 100644 --- a/packages/opencode/src/server/instance/index.ts +++ b/packages/opencode/src/server/instance/index.ts @@ -2,6 +2,7 @@ import { describeRoute, resolver, validator } from "hono-openapi" import { Hono } from "hono" import type { UpgradeWebSocket } from "hono/ws" import fs from "fs/promises" +import { Effect } from "effect" import z from "zod" import { Format } from "../../format" import { Instance } from "../../project/instance" @@ -89,7 +90,11 @@ export const InstanceRoutes = (upgrade: UpgradeWebSocket): Hono => }, }), async (c) => { - await Instance.dispose() + await AppRuntime.runPromise( + Effect.gen(function* () { + yield* Effect.promise(() => Instance.dispose()) + }), + ) return c.json(true) }, ) @@ -135,19 +140,26 @@ export const InstanceRoutes = (upgrade: UpgradeWebSocket): Hono => }), async (c) => { const ensureConfig = c.req.query("ensureConfig") === "true" - const config = Runtime.isPawWork() - ? ensureConfig - ? await PawWorkHome.ensurePrimary() - : PawWorkHome.primary() - : Global.Path.config - if (ensureConfig && !Runtime.isPawWork()) await fs.mkdir(config, { recursive: true }) - return c.json({ - home: Global.Path.home, - state: Global.Path.state, - config, - worktree: Instance.worktree, - directory: Instance.directory, - }) + const paths = await AppRuntime.runPromise( + Effect.gen(function* () { + const config = Runtime.isPawWork() + ? ensureConfig + ? yield* Effect.promise(() => PawWorkHome.ensurePrimary()) + : PawWorkHome.primary() + : Global.Path.config + if (ensureConfig && !Runtime.isPawWork()) { + yield* Effect.promise(() => fs.mkdir(config, { recursive: true })) + } + return { + home: Global.Path.home, + state: Global.Path.state, + config, + worktree: Instance.worktree, + directory: Instance.directory, + } + }), + ) + return c.json(paths) }, ) .get( @@ -168,7 +180,12 @@ export const InstanceRoutes = (upgrade: UpgradeWebSocket): Hono => }, }), async (c) => { - const [branch, default_branch] = await Promise.all([Vcs.branch(), Vcs.defaultBranch()]) + const [branch, default_branch] = await AppRuntime.runPromise( + Effect.gen(function* () { + const vcs = yield* Vcs.Service + return yield* Effect.all([vcs.branch(), vcs.defaultBranch()], { concurrency: 2 }) + }), + ) return c.json({ branch, default_branch, @@ -199,7 +216,14 @@ export const InstanceRoutes = (upgrade: UpgradeWebSocket): Hono => }), ), async (c) => { - return c.json(await Vcs.diff(c.req.valid("query").mode)) + const mode = c.req.valid("query").mode + const diff = await AppRuntime.runPromise( + Effect.gen(function* () { + const vcs = yield* Vcs.Service + return yield* vcs.diff(mode) + }), + ) + return c.json(diff) }, ) .get( @@ -220,7 +244,13 @@ export const InstanceRoutes = (upgrade: UpgradeWebSocket): Hono => }, }), async (c) => { - return c.json(await Vcs.status()) + const status = await AppRuntime.runPromise( + Effect.gen(function* () { + const vcs = yield* Vcs.Service + return yield* vcs.status() + }), + ) + return c.json(status) }, ) .get( @@ -339,7 +369,12 @@ export const InstanceRoutes = (upgrade: UpgradeWebSocket): Hono => }, }), async (c) => { - const commands = await AppRuntime.runPromise(Command.Service.use((svc) => svc.list())) + const commands = await AppRuntime.runPromise( + Effect.gen(function* () { + const command = yield* Command.Service + return yield* command.list() + }), + ) return c.json(commands) }, ) @@ -361,7 +396,12 @@ export const InstanceRoutes = (upgrade: UpgradeWebSocket): Hono => }, }), async (c) => { - const modes = await Agent.list() + const modes = await AppRuntime.runPromise( + Effect.gen(function* () { + const agent = yield* Agent.Service + return yield* agent.list() + }), + ) return c.json(modes) }, ) @@ -383,7 +423,12 @@ export const InstanceRoutes = (upgrade: UpgradeWebSocket): Hono => }, }), async (c) => { - const skills = await Skill.all() + const skills = await AppRuntime.runPromise( + Effect.gen(function* () { + const skill = yield* Skill.Service + return yield* skill.all() + }), + ) return c.json(skills) }, ) @@ -405,7 +450,13 @@ export const InstanceRoutes = (upgrade: UpgradeWebSocket): Hono => }, }), async (c) => { - return c.json(await LSP.status()) + const status = await AppRuntime.runPromise( + Effect.gen(function* () { + const lsp = yield* LSP.Service + return yield* lsp.status() + }), + ) + return c.json(status) }, ) .get( @@ -426,6 +477,12 @@ export const InstanceRoutes = (upgrade: UpgradeWebSocket): Hono => }, }), async (c) => { - return c.json(await AppRuntime.runPromise(Format.Service.use((svc) => svc.status()))) + const status = await AppRuntime.runPromise( + Effect.gen(function* () { + const format = yield* Format.Service + return yield* format.status() + }), + ) + return c.json(status) }, ) diff --git a/packages/opencode/test/server/instance-root-routes.test.ts b/packages/opencode/test/server/instance-root-routes.test.ts new file mode 100644 index 000000000..cb3b90470 --- /dev/null +++ b/packages/opencode/test/server/instance-root-routes.test.ts @@ -0,0 +1,67 @@ +import { $ } from "bun" +import { afterEach, describe, expect, test } from "bun:test" +import fs from "fs/promises" +import path from "path" +import { Instance } from "../../src/project/instance" +import { Server } from "../../src/server/server" +import { resetDatabase } from "../fixture/db" +import { tmpdir } from "../fixture/fixture" + +afterEach(async () => { + await Instance.disposeAll() + await resetDatabase() +}) + +describe("instance root routes", () => { + test("returns path and metadata JSON through the route runtime", async () => { + await using tmp = await tmpdir({ git: true }) + const app = Server.Default().app + const headers = { "x-opencode-directory": tmp.path } + + const pathResponse = await app.request("/path", { headers }) + expect(pathResponse.status).toBe(200) + expect(await pathResponse.json()).toMatchObject({ directory: tmp.path, worktree: tmp.path }) + + for (const route of ["/agent", "/skill", "/command", "/lsp", "/formatter"]) { + const response = await app.request(route, { headers }) + expect(response.status, route).toBe(200) + expect(await response.json(), route).toBeArray() + } + }) + + test("returns VCS JSON through the route runtime", async () => { + await using tmp = await tmpdir({ git: true }) + const app = Server.Default().app + const headers = { "x-opencode-directory": tmp.path } + + await fs.writeFile(path.join(tmp.path, "tracked.txt"), "original\n", "utf-8") + await $`git add tracked.txt`.cwd(tmp.path).quiet() + await $`git commit --no-gpg-sign -m "add file"`.cwd(tmp.path).quiet() + await fs.writeFile(path.join(tmp.path, "tracked.txt"), "changed\n", "utf-8") + + const info = await app.request("/vcs", { headers }) + expect(info.status).toBe(200) + expect(await info.json()).toMatchObject({ branch: expect.any(String) }) + + const diff = await app.request("/vcs/diff?mode=git", { headers }) + expect(diff.status).toBe(200) + expect(await diff.json()).toEqual([ + expect.objectContaining({ file: "tracked.txt", additions: 1, deletions: 1, status: "modified" }), + ]) + + const status = await app.request("/vcs/status", { headers }) + expect(status.status).toBe(200) + expect(await status.json()).toEqual([{ file: "tracked.txt", additions: 1, deletions: 1, status: "modified" }]) + }) + + test("disposes the current instance through the route runtime", async () => { + await using tmp = await tmpdir({ git: true }) + const response = await Server.Default().app.request("/instance/dispose", { + method: "POST", + headers: { "x-opencode-directory": tmp.path }, + }) + + expect(response.status).toBe(200) + expect(await response.json()).toBe(true) + }) +})