summaryrefslogtreecommitdiffhomepage
path: root/packages
diff options
context:
space:
mode:
authorKit Langton <[email protected]>2026-04-25 15:24:07 -0400
committerGitHub <[email protected]>2026-04-25 15:24:07 -0400
commitb4f4134e8100bbe37fe6ad0d8bb2520599f88271 (patch)
tree522da5427cad37c62b17efde000471a8b9affbb2 /packages
parentcd64b670388f45dfddad7fe543ab9c0ff490b81c (diff)
downloadopencode-b4f4134e8100bbe37fe6ad0d8bb2520599f88271.tar.gz
opencode-b4f4134e8100bbe37fe6ad0d8bb2520599f88271.zip
feat(httpapi): bridge instance dispose endpoint (#24368)
Diffstat (limited to 'packages')
-rw-r--r--packages/opencode/specs/effect/http-api.md2
-rw-r--r--packages/opencode/src/server/routes/instance/httpapi/instance.ts17
-rw-r--r--packages/opencode/src/server/routes/instance/httpapi/lifecycle.ts24
-rw-r--r--packages/opencode/src/server/routes/instance/httpapi/server.ts2
-rw-r--r--packages/opencode/src/server/routes/instance/index.ts1
-rw-r--r--packages/opencode/test/server/httpapi-instance.test.ts23
6 files changed, 68 insertions, 1 deletions
diff --git a/packages/opencode/specs/effect/http-api.md b/packages/opencode/specs/effect/http-api.md
index a8f9f1e0a..cbd4987cc 100644
--- a/packages/opencode/specs/effect/http-api.md
+++ b/packages/opencode/specs/effect/http-api.md
@@ -163,7 +163,7 @@ Use raw Effect HTTP routes where `HttpApi` does not fit. The goal is deleting Ho
| `file` | `bridged` partial | find text/file/symbol, list/content/status |
| `mcp` | `bridged` partial | status only |
| `workspace` | `bridged` | list, get, enter |
-| top-level instance reads | `bridged` | path, vcs, command, agent, skill, lsp, formatter |
+| top-level instance routes | `bridged` | path, vcs, command, agent, skill, lsp, formatter, dispose |
| experimental JSON routes | `bridged` partial | console reads, tool ids, worktree list, resource list; global session list remains later |
| `session` | `later/special` | large stateful surface plus streaming |
| `sync` | `later` | process/control side effects |
diff --git a/packages/opencode/src/server/routes/instance/httpapi/instance.ts b/packages/opencode/src/server/routes/instance/httpapi/instance.ts
index 016703e77..8788dee5f 100644
--- a/packages/opencode/src/server/routes/instance/httpapi/instance.ts
+++ b/packages/opencode/src/server/routes/instance/httpapi/instance.ts
@@ -9,6 +9,7 @@ import * as InstanceState from "@/effect/instance-state"
import { Effect, Layer, Schema } from "effect"
import { HttpApi, HttpApiBuilder, HttpApiEndpoint, HttpApiGroup, OpenApi } from "effect/unstable/httpapi"
import { Authorization } from "./auth"
+import { markInstanceForDisposal } from "./lifecycle"
const PathInfo = Schema.Struct({
home: Schema.String,
@@ -23,6 +24,7 @@ const VcsDiffQuery = Schema.Struct({
})
export const InstancePaths = {
+ dispose: "/instance/dispose",
path: "/path",
vcs: "/vcs",
vcsDiff: "/vcs/diff",
@@ -37,6 +39,15 @@ export const InstanceApi = HttpApi.make("instance")
.add(
HttpApiGroup.make("instance")
.add(
+ HttpApiEndpoint.post("dispose", InstancePaths.dispose, {
+ success: Schema.Boolean,
+ }).annotateMerge(
+ OpenApi.annotations({
+ identifier: "instance.dispose",
+ summary: "Dispose instance",
+ description: "Clean up and dispose the current OpenCode instance, releasing all resources.",
+ }),
+ ),
HttpApiEndpoint.get("path", InstancePaths.path, {
success: PathInfo,
}).annotateMerge(
@@ -138,6 +149,11 @@ export const instanceHandlers = Layer.unwrap(
const skill = yield* Skill.Service
const vcs = yield* Vcs.Service
+ const dispose = Effect.fn("InstanceHttpApi.dispose")(function* () {
+ yield* markInstanceForDisposal(yield* InstanceState.context)
+ return true
+ })
+
const getPath = Effect.fn("InstanceHttpApi.path")(function* () {
const ctx = yield* InstanceState.context
return {
@@ -180,6 +196,7 @@ export const instanceHandlers = Layer.unwrap(
return HttpApiBuilder.group(InstanceApi, "instance", (handlers) =>
handlers
+ .handle("dispose", dispose)
.handle("path", getPath)
.handle("vcs", getVcs)
.handle("vcsDiff", getVcsDiff)
diff --git a/packages/opencode/src/server/routes/instance/httpapi/lifecycle.ts b/packages/opencode/src/server/routes/instance/httpapi/lifecycle.ts
new file mode 100644
index 000000000..6b11dffd5
--- /dev/null
+++ b/packages/opencode/src/server/routes/instance/httpapi/lifecycle.ts
@@ -0,0 +1,24 @@
+import { Instance, type InstanceContext } from "@/project/instance"
+import { Effect } from "effect"
+import { HttpEffect, HttpMiddleware, HttpServerRequest } from "effect/unstable/http"
+
+const disposeAfterResponse = new WeakMap<object, InstanceContext>()
+
+export const markInstanceForDisposal = (ctx: InstanceContext) =>
+ HttpEffect.appendPreResponseHandler((request, response) =>
+ Effect.sync(() => {
+ disposeAfterResponse.set(request.source, ctx)
+ return response
+ }),
+ )
+
+export const disposeMiddleware: HttpMiddleware.HttpMiddleware = (effect) =>
+ Effect.gen(function* () {
+ const response = yield* effect
+ const request = yield* HttpServerRequest.HttpServerRequest
+ const ctx = disposeAfterResponse.get(request.source)
+ if (!ctx) return response
+ disposeAfterResponse.delete(request.source)
+ yield* Effect.promise(() => Instance.restore(ctx, () => Instance.dispose()))
+ return response
+ })
diff --git a/packages/opencode/src/server/routes/instance/httpapi/server.ts b/packages/opencode/src/server/routes/instance/httpapi/server.ts
index 77a2832ce..be574c914 100644
--- a/packages/opencode/src/server/routes/instance/httpapi/server.ts
+++ b/packages/opencode/src/server/routes/instance/httpapi/server.ts
@@ -19,6 +19,7 @@ import { ProjectApi, projectHandlers } from "./project"
import { ProviderApi, providerHandlers } from "./provider"
import { QuestionApi, questionHandlers } from "./question"
import { WorkspaceApi, workspaceHandlers } from "./workspace"
+import { disposeMiddleware } from "./lifecycle"
import { memoMap } from "@opencode-ai/core/effect/memo-map"
const Query = Schema.Struct({
@@ -83,6 +84,7 @@ export const routes = Layer.mergeAll(
export const webHandler = lazy(() =>
HttpRouter.toWebHandler(routes, {
memoMap,
+ middleware: disposeMiddleware,
}),
)
diff --git a/packages/opencode/src/server/routes/instance/index.ts b/packages/opencode/src/server/routes/instance/index.ts
index 8e4c497bd..2a8fc602a 100644
--- a/packages/opencode/src/server/routes/instance/index.ts
+++ b/packages/opencode/src/server/routes/instance/index.ts
@@ -64,6 +64,7 @@ export const InstanceRoutes = (upgrade: UpgradeWebSocket): Hono => {
app.get(FilePaths.content, (c) => handler(c.req.raw, context))
app.get(FilePaths.status, (c) => handler(c.req.raw, context))
app.get(InstancePaths.path, (c) => handler(c.req.raw, context))
+ app.post(InstancePaths.dispose, (c) => handler(c.req.raw, context))
app.get(InstancePaths.vcs, (c) => handler(c.req.raw, context))
app.get(InstancePaths.vcsDiff, (c) => handler(c.req.raw, context))
app.get(InstancePaths.command, (c) => handler(c.req.raw, context))
diff --git a/packages/opencode/test/server/httpapi-instance.test.ts b/packages/opencode/test/server/httpapi-instance.test.ts
index a066e0e92..463dbaa87 100644
--- a/packages/opencode/test/server/httpapi-instance.test.ts
+++ b/packages/opencode/test/server/httpapi-instance.test.ts
@@ -2,6 +2,7 @@ import { afterEach, describe, expect, test } from "bun:test"
import type { UpgradeWebSocket } from "hono/ws"
import path from "path"
import { Flag } from "@opencode-ai/core/flag/flag"
+import { GlobalBus } from "@/bus/global"
import { Instance } from "../../src/project/instance"
import { InstanceRoutes } from "../../src/server/routes/instance"
import { InstancePaths } from "../../src/server/routes/instance/httpapi/instance"
@@ -77,4 +78,26 @@ describe("instance HttpApi", () => {
expect(formatter.status).toBe(200)
expect(await formatter.json()).toEqual([])
})
+
+ test("serves instance dispose through Hono bridge", async () => {
+ await using tmp = await tmpdir()
+
+ const disposed = new Promise<string | undefined>((resolve) => {
+ const onEvent = (event: { directory?: string; payload: { type?: string } }) => {
+ if (event.payload.type !== "server.instance.disposed") return
+ GlobalBus.off("event", onEvent)
+ resolve(event.directory)
+ }
+ GlobalBus.on("event", onEvent)
+ })
+
+ const response = await app().request(InstancePaths.dispose, {
+ method: "POST",
+ headers: { "x-opencode-directory": tmp.path },
+ })
+
+ expect(response.status).toBe(200)
+ expect(await response.json()).toBe(true)
+ expect(await disposed).toBe(tmp.path)
+ })
})