Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 0 additions & 1 deletion packages/opencode/script/route-inventory.ts
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,6 @@ const HTTP_METHODS = new Set(["GET", "POST", "PUT", "PATCH", "DELETE"])
const honoRouteSources = [
["packages/opencode/src/server/instance/event.ts", ""],
["packages/opencode/src/server/instance/index.ts", ""],
["packages/opencode/src/server/instance/pty.ts", "/pty"],
] as const

const supplementalHonoRouteSources = [
Expand Down
2 changes: 0 additions & 2 deletions packages/opencode/src/server/instance/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,6 @@ import { PawWorkHome } from "@opencode-ai/core/pawwork-home"
import { Runtime } from "@opencode-ai/core/runtime"
import { LSP } from "../../lsp"
import { Command } from "../../command"
import { PtyRoutes } from "./pty"
import { WorkspaceRouterMiddleware } from "./middleware"
import { AppRuntime } from "@/effect/app-runtime"
import { jsonBodyLimit } from "./json-body-limit"
Expand Down Expand Up @@ -119,7 +118,6 @@ const getLspStatus = Effect.fn("InstanceRoutes.lsp.status")(function* () {
export const InstanceRoutes = (upgrade: UpgradeWebSocket): Hono =>
new Hono()
.use(WorkspaceRouterMiddleware(upgrade))
.route("/pty", PtyRoutes())
.post(
"/instance/dispose",
describeRoute({
Expand Down
203 changes: 2 additions & 201 deletions packages/opencode/src/server/instance/pty.ts
Original file line number Diff line number Diff line change
@@ -1,15 +1,12 @@
import { Hono } from "hono"
import { describeRoute, validator, resolver } from "hono-openapi"
import { HTTPException } from "hono/http-exception"
import z from "zod"
import { Effect } from "effect"
import { AppRuntime } from "@/effect/app-runtime"
import { Pty } from "@/pty"
import { PtyID } from "@/pty/schema"
import { ConnectToken, PtyTicket } from "@/pty/ticket"
import { PtyTicket } from "@/pty/ticket"
import { NotFoundError } from "../../storage/db"
import type { WebSocketEvents } from "../adapter"
import { errors } from "../error"

export function assertPtyConnectTarget(info: unknown) {
if (!info) {
Expand Down Expand Up @@ -74,40 +71,7 @@ function parsePtyConnectInput(request: Request, rawPtyID: string) {
} satisfies PtyConnectInput
}

const listPtySessions = Effect.fn("PtyRoutes.list")(function* () {
const pty = yield* Pty.Service
return yield* pty.list()
})

const createPtySession = Effect.fn("PtyRoutes.create")(function* (input: Pty.CreateInput) {
const pty = yield* Pty.Service
return yield* pty.create(input)
})

const getPtySession = Effect.fn("PtyRoutes.get")(function* (id: PtyID) {
const pty = yield* Pty.Service
return yield* pty.get(id)
})

const updatePtySession = Effect.fn("PtyRoutes.update")(function* (input: { id: PtyID; update: Pty.UpdateInput }) {
const pty = yield* Pty.Service
return yield* pty.update(input.id, input.update)
})

const removePtySession = Effect.fn("PtyRoutes.remove")(function* (id: PtyID) {
const pty = yield* Pty.Service
const info = yield* pty.get(id)
if (!info) return false
yield* pty.remove(id)
return true
})

const assertPtyConnectTokenTarget = Effect.fn("PtyRoutes.connectToken")(function* (id: PtyID) {
const pty = yield* Pty.Service
assertPtyConnectTarget(yield* pty.get(id))
})

const connectPtySession = Effect.fn("PtyRoutes.connect")(function* (input: PtyConnectInput) {
const connectPtySession = Effect.fn("PtyWebSocket.connect")(function* (input: PtyConnectInput) {
const pty = yield* Pty.Service
const id = input.ptyID
assertPtyConnectTicket({ ptyID: id, ticket: input.ticket })
Expand Down Expand Up @@ -158,166 +122,3 @@ export async function createPtyConnectEvents(request: Request, rawPtyID: string)
if (input instanceof Response) return input
return runPtyRoute(connectPtySession(input))
}

export function PtyRoutes() {
return new Hono()
.get(
"/",
describeRoute({
summary: "List PTY sessions",
description: "Get a list of all active pseudo-terminal (PTY) sessions managed by OpenCode.",
operationId: "pty.list",
responses: {
200: {
description: "List of sessions",
content: {
"application/json": {
schema: resolver(Pty.Info.array()),
},
},
},
},
}),
async (c) => {
const sessions = await runPtyRoute(listPtySessions())
return c.json(sessions)
},
)
.post(
"/",
describeRoute({
summary: "Create PTY session",
description: "Create a new pseudo-terminal (PTY) session for running shell commands and processes.",
operationId: "pty.create",
responses: {
200: {
description: "Created session",
content: {
"application/json": {
schema: resolver(Pty.Info),
},
},
},
...errors(400),
},
}),
validator("json", Pty.CreateInput),
async (c) => {
const input = c.req.valid("json")
const info = await runPtyRoute(createPtySession(input))
return c.json(info)
},
)
.get(
"/:ptyID",
describeRoute({
summary: "Get PTY session",
description: "Retrieve detailed information about a specific pseudo-terminal (PTY) session.",
operationId: "pty.get",
responses: {
200: {
description: "Session info",
content: {
"application/json": {
schema: resolver(Pty.Info),
},
},
},
...errors(404),
},
}),
validator("param", z.object({ ptyID: PtyID.zod })),
async (c) => {
const id = c.req.valid("param").ptyID
const info = await runPtyRoute(getPtySession(id))
if (!info) {
throw new NotFoundError({ message: "Session not found" })
}
return c.json(info)
},
)
.put(
"/:ptyID",
describeRoute({
summary: "Update PTY session",
description: "Update properties of an existing pseudo-terminal (PTY) session.",
operationId: "pty.update",
responses: {
200: {
description: "Updated session",
content: {
"application/json": {
schema: resolver(Pty.Info),
},
},
},
...errors(400),
...errors(404),
},
}),
validator("param", z.object({ ptyID: PtyID.zod })),
validator("json", Pty.UpdateInput),
async (c) => {
const id = c.req.valid("param").ptyID
const input = c.req.valid("json")
const info = await runPtyRoute(updatePtySession({ id, update: input }))
if (!info) {
throw new NotFoundError({ message: "Session not found" })
}
return c.json(info)
},
)
.delete(
"/:ptyID",
describeRoute({
summary: "Remove PTY session",
description: "Remove and terminate a specific pseudo-terminal (PTY) session.",
operationId: "pty.remove",
responses: {
200: {
description: "Session removed",
content: {
"application/json": {
schema: resolver(z.boolean()),
},
},
},
...errors(404),
},
}),
validator("param", z.object({ ptyID: PtyID.zod })),
async (c) => {
const id = c.req.valid("param").ptyID
const removed = await runPtyRoute(removePtySession(id))
if (!removed) {
throw new NotFoundError({ message: "Session not found" })
}
return c.json(true)
},
)
.post(
"/:ptyID/connect-token",
describeRoute({
summary: "Create PTY WebSocket token",
description: "Create a short-lived ticket for opening a PTY WebSocket connection.",
operationId: "pty.connectToken",
responses: {
200: {
description: "WebSocket connect token",
content: {
"application/json": {
schema: resolver(ConnectToken),
},
},
},
...errors(404),
},
}),
validator("param", z.object({ ptyID: PtyID.zod })),
async (c) => {
const id = c.req.valid("param").ptyID
await runPtyRoute(assertPtyConnectTokenTarget(id))
return c.json(PtyTicket.issue({ ptyID: id }))
},
)
}
1 change: 0 additions & 1 deletion packages/opencode/src/server/routes/instance/pty.ts

This file was deleted.

12 changes: 12 additions & 0 deletions packages/opencode/test/server/production-boundary.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -305,4 +305,16 @@ describe("production server boundary", () => {
expect(session).not.toContain("export const SessionRoutes")
expect(session).toContain("export const SessionRouteEffects")
})

test("does not retain the retired PTY ordinary legacy Hono route source", async () => {
const instanceRoutes = await readFile(path.join(import.meta.dir, "../../src/server/instance/index.ts"), "utf8")
const pty = await readFile(path.join(import.meta.dir, "../../src/server/instance/pty.ts"), "utf8")

expect(instanceRoutes).not.toContain("PtyRoutes")
expect(instanceRoutes).not.toContain('.route("/pty"')
expect(pty).not.toMatch(/from\s+["']hono["']/)
expect(pty).not.toMatch(/\bnew\s+Hono\s*\(/)
expect(pty).not.toContain("export function PtyRoutes")
expect(pty).toContain("createPtyConnectEvents")
})
})
50 changes: 3 additions & 47 deletions packages/opencode/test/server/pty-routes.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,10 @@ import { NodeFileSystem, NodeHttpPlatform, NodePath } from "@effect/platform-nod
import { Effect, Layer } from "effect"
import { Etag, HttpRouter, HttpServerRequest, HttpServerResponse } from "effect/unstable/http"
import { HttpApiBuilder, OpenApi } from "effect/unstable/httpapi"
import { Hono } from "hono"
import type { UpgradeWebSocket } from "../../src/server/adapter"
import { Log } from "@opencode-ai/core/util/log"
import { AppRuntime } from "../../src/effect/app-runtime"
import { assertPtyConnectTarget, PtyRoutes } from "../../src/server/instance/pty"
import { ErrorMiddleware } from "../../src/server/middleware"
import { assertPtyConnectTarget } from "../../src/server/instance/pty"
import { handleWebSocketCompatibilityRequest } from "../../src/server/websocket-compatibility"
import { NotFoundError } from "../../src/storage/db"
import { Pty } from "../../src/pty"
Expand Down Expand Up @@ -268,7 +266,7 @@ describe("pty routes", () => {
})
})

test("issues a connect token for an existing PTY", async () => {
test("issues a connect token for an existing PTY through the HttpApi handlers", async () => {
if (process.platform === "win32") return
await using tmp = await tmpdir({ git: true })
await Instance.provide({
Expand All @@ -282,10 +280,7 @@ describe("pty routes", () => {
}),
)
try {
const app = new Hono().route("/pty", PtyRoutes())
app.onError(ErrorMiddleware)

const response = await app.request(`/pty/${info.id}/connect-token`, { method: "POST" })
const response = await requestPtyHttpApi(`/pty/${info.id}/connect-token`, { method: "POST" })
const body = await response.json()

expect(response.status).toBe(200)
Expand Down Expand Up @@ -401,43 +396,4 @@ describe("pty routes", () => {
expect(names).toContain("ticket")
})

test("maps missing update targets as not found", async () => {
await using tmp = await tmpdir({ git: true })
await Instance.provide({
directory: tmp.path,
fn: async () => {
const app = new Hono().route("/pty", PtyRoutes())
app.onError(ErrorMiddleware)

const response = await app.request(`/pty/${PtyID.ascending()}`, {
method: "PUT",
body: JSON.stringify({ title: "gone" }),
headers: { "content-type": "application/json" },
})
const body = await response.json()

expect(response.status).toBe(404)
expect(body.name).toBe("NotFoundError")
},
})
})

test("maps missing remove targets as not found", async () => {
await using tmp = await tmpdir({ git: true })
await Instance.provide({
directory: tmp.path,
fn: async () => {
const app = new Hono().route("/pty", PtyRoutes())
app.onError(ErrorMiddleware)

const response = await app.request(`/pty/${PtyID.ascending()}`, {
method: "DELETE",
})
const body = await response.json()

expect(response.status).toBe(404)
expect(body.name).toBe("NotFoundError")
},
})
})
})
6 changes: 3 additions & 3 deletions packages/opencode/test/server/route-inventory-harness.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -312,7 +312,7 @@ describe("route inventory harness", () => {
["POST", "/pty/:ptyID/connect-token"],
] as const) {
expect(inventory.rows.find((row) => row.method === method && row.path === routePath)).toMatchObject({
hono: true,
hono: false,
localHttpApi: true,
})
}
Expand Down Expand Up @@ -581,11 +581,11 @@ describe("route inventory harness", () => {
const inventory = await buildRouteInventory({ root, requireUpstream: false })

expect(inventory.rows.find((row) => row.method === "POST" && row.path === "/pty/:ptyID/connect-token")).toMatchObject({
hono: true,
hono: false,
openapi: true,
v2Sdk: true,
localHttpApi: true,
classification: "openapi-v2-sdk",
classification: expect.stringMatching(/^local-httpapi-(?:only|upstream-only)$/),
specialSurface: "PTY websocket",
})
})
Expand Down
Loading