浏览代码

fix(httpapi): document instance query parameters (#24809)

Kit Langton 3 月之前
父节点
当前提交
ea3c6c3481

+ 0 - 1
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({

+ 51 - 0
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<Record<"get" | "post" | "put" | "delete" | "patch", OpenApiOperation>>
+
+type OpenApiSpec = {
+  paths?: Record<string, OpenApiPathItem>
+}
+
+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<string, unknown>) {
+  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,
     }),
   )

+ 37 - 0
packages/opencode/test/server/httpapi-bridge.test.ts

@@ -34,6 +34,32 @@ function openApiRouteKeys(spec: { paths: Record<string, Partial<Record<(typeof m
     .sort()
 }
 
+function openApiParameters(spec: { paths: Record<string, Partial<Record<(typeof methods)[number], Operation>>> }) {
+  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")