diff --git a/packages/opencode/src/server/routes/instance/httpapi/pty.ts b/packages/opencode/src/server/routes/instance/httpapi/pty.ts index f1ac093998a7..21700025358f 100644 --- a/packages/opencode/src/server/routes/instance/httpapi/pty.ts +++ b/packages/opencode/src/server/routes/instance/httpapi/pty.ts @@ -118,7 +118,6 @@ export const PtyConnectApi = HttpApi.make("pty-connect").add( .add( HttpApiEndpoint.get("connect", PtyPaths.connect, { params: Params, - query: CursorQuery, success: Schema.Boolean, }).annotateMerge( OpenApi.annotations({ diff --git a/packages/opencode/src/server/routes/instance/httpapi/public.ts b/packages/opencode/src/server/routes/instance/httpapi/public.ts index 1a7f675b3f94..c26d16e91e6c 100644 --- a/packages/opencode/src/server/routes/instance/httpapi/public.ts +++ b/packages/opencode/src/server/routes/instance/httpapi/public.ts @@ -17,6 +17,56 @@ import { SyncApi } from "./sync" import { TuiApi } from "./tui" import { WorkspaceApi } from "./workspace" +type OpenApiParameter = { + name: string + in: string + required?: boolean + schema?: unknown +} + +type OpenApiOperation = { + parameters?: OpenApiParameter[] +} + +type OpenApiPathItem = Partial> + +type OpenApiSpec = { + paths?: Record +} + +const InstanceQueryParameters = [ + { + name: "directory", + in: "query", + required: false, + schema: { type: "string" }, + }, + { + name: "workspace", + in: "query", + required: false, + schema: { type: "string" }, + }, +] satisfies OpenApiParameter[] + +function documentInstanceQueryParameters(input: Record) { + const spec = input as OpenApiSpec + for (const [path, item] of Object.entries(spec.paths ?? {})) { + if (path.startsWith("/global/") || path.startsWith("/auth/")) continue + for (const method of ["get", "post", "put", "delete", "patch"] as const) { + const operation = item[method] + if (!operation) continue + operation.parameters = [ + ...InstanceQueryParameters, + ...(operation.parameters ?? []).filter( + (param) => param.in !== "query" || (param.name !== "directory" && param.name !== "workspace"), + ), + ] + } + } + return input +} + export const PublicApi = HttpApi.make("opencode") .addHttpApi(ControlApi) .addHttpApi(GlobalApi) @@ -41,5 +91,6 @@ export const PublicApi = HttpApi.make("opencode") title: "opencode", version: "1.0.0", description: "opencode api", + transform: documentInstanceQueryParameters, }), ) diff --git a/packages/opencode/test/server/httpapi-bridge.test.ts b/packages/opencode/test/server/httpapi-bridge.test.ts index 150cf53c0428..d4d14dbc0b66 100644 --- a/packages/opencode/test/server/httpapi-bridge.test.ts +++ b/packages/opencode/test/server/httpapi-bridge.test.ts @@ -34,6 +34,32 @@ function openApiRouteKeys(spec: { paths: Record>> }) { + return Object.fromEntries( + Object.entries(spec.paths).flatMap(([path, item]) => + methods + .filter((method) => item[method]) + .map((method) => [ + `${method.toUpperCase()} ${path}`, + (item[method]?.parameters ?? []) + .map(parameterKey) + .filter((param) => param !== undefined) + .sort(), + ]), + ), + ) +} + +type Operation = { + parameters?: unknown[] +} + +function parameterKey(param: unknown) { + if (!param || typeof param !== "object" || !("in" in param) || !("name" in param)) return + if (typeof param.in !== "string" || typeof param.name !== "string") return + return `${param.in}:${param.name}:${"required" in param && param.required === true}` +} + function authorization(username: string, password: string) { return `Basic ${Buffer.from(`${username}:${password}`).toString("base64")}` } @@ -63,6 +89,17 @@ describe("HttpApi server", () => { expect(effectRoutes.filter((route) => !honoRoutes.includes(route))).toEqual([]) }) + test("matches generated OpenAPI route parameters", async () => { + const hono = openApiParameters(await Server.openapi()) + const effect = openApiParameters(OpenApi.fromApi(PublicApi)) + + expect( + Object.keys(hono) + .filter((route) => JSON.stringify(hono[route]) !== JSON.stringify(effect[route])) + .map((route) => ({ route, hono: hono[route], effect: effect[route] })), + ).toEqual([]) + }) + test("allows requests when auth is disabled", async () => { await using tmp = await tmpdir({ git: true }) await Bun.write(`${tmp.path}/hello.txt`, "hello")