diff --git a/packages/opencode/script/route-inventory.ts b/packages/opencode/script/route-inventory.ts index a8a12aa7f..707a6ebd9 100644 --- a/packages/opencode/script/route-inventory.ts +++ b/packages/opencode/script/route-inventory.ts @@ -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 = [ diff --git a/packages/opencode/src/server/instance/index.ts b/packages/opencode/src/server/instance/index.ts index 0c770aeb4..2658201f8 100644 --- a/packages/opencode/src/server/instance/index.ts +++ b/packages/opencode/src/server/instance/index.ts @@ -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" @@ -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({ diff --git a/packages/opencode/src/server/instance/pty.ts b/packages/opencode/src/server/instance/pty.ts index 9df45ba44..30c55dc98 100644 --- a/packages/opencode/src/server/instance/pty.ts +++ b/packages/opencode/src/server/instance/pty.ts @@ -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) { @@ -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 }) @@ -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 })) - }, - ) -} diff --git a/packages/opencode/src/server/routes/instance/pty.ts b/packages/opencode/src/server/routes/instance/pty.ts deleted file mode 100644 index b026c4bac..000000000 --- a/packages/opencode/src/server/routes/instance/pty.ts +++ /dev/null @@ -1 +0,0 @@ -export { PtyRoutes } from "../../instance/pty" diff --git a/packages/opencode/test/server/production-boundary.test.ts b/packages/opencode/test/server/production-boundary.test.ts index e82c2ea05..29d41ee50 100644 --- a/packages/opencode/test/server/production-boundary.test.ts +++ b/packages/opencode/test/server/production-boundary.test.ts @@ -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") + }) }) diff --git a/packages/opencode/test/server/pty-routes.test.ts b/packages/opencode/test/server/pty-routes.test.ts index 5b31c3489..2bc21b379 100644 --- a/packages/opencode/test/server/pty-routes.test.ts +++ b/packages/opencode/test/server/pty-routes.test.ts @@ -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" @@ -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({ @@ -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) @@ -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") - }, - }) - }) }) diff --git a/packages/opencode/test/server/route-inventory-harness.test.ts b/packages/opencode/test/server/route-inventory-harness.test.ts index 014f8f32d..23316fb51 100644 --- a/packages/opencode/test/server/route-inventory-harness.test.ts +++ b/packages/opencode/test/server/route-inventory-harness.test.ts @@ -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, }) } @@ -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", }) })