Просмотр исходного кода

docs(effect): add generated http route inventory

Kit Langton 3 месяцев назад
Родитель
Сommit
2b028287e2

+ 138 - 0
packages/opencode/script/http-route-inventory.ts

@@ -0,0 +1,138 @@
+#!/usr/bin/env bun
+
+import type { Hono } from "hono"
+import type { UpgradeWebSocket } from "hono/ws"
+import { WorkspacePaths } from "../src/server/routes/instance/httpapi/workspace"
+import { FilePaths } from "../src/server/routes/instance/httpapi/file"
+import { McpPaths } from "../src/server/routes/instance/httpapi/mcp"
+import { Flag } from "../src/flag/flag"
+import { ControlPlaneRoutes } from "../src/server/routes/control"
+import { WorkspaceRoutes } from "../src/server/routes/control/workspace"
+import { InstanceRoutes } from "../src/server/routes/instance"
+import { UIRoutes } from "../src/server/routes/ui"
+
+type Method = "GET" | "POST" | "PUT" | "PATCH" | "DELETE" | "ALL"
+
+interface Route {
+  surface: string
+  method: Method
+  path: string
+  status: string
+}
+
+const methodOrder = new Map<Method, number>([
+  ["GET", 0],
+  ["POST", 1],
+  ["PUT", 2],
+  ["PATCH", 3],
+  ["DELETE", 4],
+  ["ALL", 5],
+])
+
+const bridged = new Set([
+  key("GET", "/question"),
+  key("POST", "/question/:requestID/reply"),
+  key("POST", "/question/:requestID/reject"),
+  key("GET", "/permission"),
+  key("POST", "/permission/:requestID/reply"),
+  key("GET", "/config"),
+  key("GET", "/config/providers"),
+  key("GET", "/provider"),
+  key("GET", "/provider/auth"),
+  key("POST", "/provider/:providerID/oauth/authorize"),
+  key("POST", "/provider/:providerID/oauth/callback"),
+  key("GET", "/project"),
+  key("GET", "/project/current"),
+  key("GET", FilePaths.list),
+  key("GET", FilePaths.content),
+  key("GET", FilePaths.status),
+  key("GET", McpPaths.status),
+  ...Object.values(WorkspacePaths).map((path) => key("GET", path)),
+])
+
+const topLevelNext = new Set([
+  key("GET", "/path"),
+  key("GET", "/vcs"),
+  key("GET", "/vcs/diff"),
+  key("GET", "/command"),
+  key("GET", "/agent"),
+  key("GET", "/skill"),
+  key("GET", "/lsp"),
+  key("GET", "/formatter"),
+])
+
+function key(method: string, path: string) {
+  return `${method} ${path}`
+}
+
+function normalize(prefix: string, route: string) {
+  if (!prefix) return route
+  if (route === "/") return prefix
+  return `${prefix}${route}`.replaceAll(/\/+/g, "/")
+}
+
+function routes(surface: string, app: Hono, prefix = "") {
+  const seen = new Map<string, Route>()
+  for (const route of app.routes as Array<{ method: Method; path: string }>) {
+    if (surface !== "ui" && route.method === "ALL" && route.path === "/*") continue
+    const path = normalize(prefix, route.path)
+    seen.set(key(route.method, path), {
+      surface,
+      method: route.method,
+      path,
+      status: classify(route.method, path, surface),
+    })
+  }
+  return [...seen.values()].toSorted(compare)
+}
+
+function compare(a: Route, b: Route) {
+  return (
+    a.surface.localeCompare(b.surface) ||
+    a.path.localeCompare(b.path) ||
+    (methodOrder.get(a.method) ?? 99) - (methodOrder.get(b.method) ?? 99)
+  )
+}
+
+function classify(method: Method, path: string, surface: string) {
+  if (bridged.has(key(method, path))) return "bridged"
+  if (topLevelNext.has(key(method, path))) return "next"
+  if (surface === "ui") return "special"
+  if (path === "/event") return "special"
+  if (path.startsWith("/pty") || path.startsWith("/tui")) return "special"
+  if (path.startsWith("/session") || path.startsWith("/sync")) return "later"
+  if (path.startsWith("/experimental")) return method === "GET" ? "next" : "later"
+  if (path.startsWith("/mcp")) return "later"
+  if (path === "/instance/dispose") return "next"
+  return "later"
+}
+
+function table(items: Route[]) {
+  return [
+    "| Surface | Method | Path | Status |",
+    "| --- | --- | --- | --- |",
+    ...items.map((item) => `| ${item.surface} | \`${item.method}\` | \`${item.path}\` | \`${item.status}\` |`),
+  ].join("\n")
+}
+
+Flag.OPENCODE_EXPERIMENTAL_HTTPAPI = false
+
+const websocket = (() => () => new Response(null, { status: 501 })) as unknown as UpgradeWebSocket
+const inventory = [
+  ...routes("control", ControlPlaneRoutes()),
+  ...routes("workspace", WorkspaceRoutes(), "/experimental/workspace"),
+  ...routes("instance", InstanceRoutes(websocket)),
+  ...routes("ui", UIRoutes()),
+].toSorted(compare)
+
+await Bun.write(
+  new URL("../specs/effect/http-route-inventory.md", import.meta.url),
+  `# Http Route Inventory
+
+Generated from Hono route registrations by \`packages/opencode/script/http-route-inventory.ts\`.
+
+Status meanings are defined in \`specs/effect/http-api.md\`.
+
+${table(inventory)}
+`,
+)

+ 29 - 23
packages/opencode/specs/effect/http-api.md

@@ -53,6 +53,12 @@ Before porting more routes, cover the bridge behavior that every route depends o
 
 Create a route inventory from the actual Hono registrations and classify each route.
 
+The generated inventory lives in `specs/effect/http-route-inventory.md` and is produced by:
+
+```bash
+bun run script/http-route-inventory.ts
+```
+
 Statuses:
 
 - `bridged`: served through the `HttpApi` bridge when the flag is on.
@@ -130,31 +136,31 @@ Use raw Effect HTTP routes where `HttpApi` does not fit. The goal is deleting Ho
 
 ## Current Route Status
 
-| Area                     | Status            | Notes                                                          |
-| ------------------------ | ----------------- | -------------------------------------------------------------- |
-| `question`               | `bridged`         | `GET /question`, reply, reject                                 |
-| `permission`             | `bridged`         | list and reply                                                 |
-| `provider`               | `bridged`         | list, auth, OAuth authorize/callback                           |
-| `config`                 | `bridged` partial | reads only; mutation remains Hono                              |
-| `project`                | `bridged` partial | reads only; git-init remains Hono                              |
-| `file`                   | `bridged` partial | list/content/status only                                       |
-| `mcp`                    | `bridged` partial | status only                                                    |
-| `workspace`              | `implemented`     | `HttpApi` group exists, but bridge mounting needs verification |
-| top-level instance reads | `next`            | path, vcs, command, agent, skill, lsp, formatter               |
-| experimental JSON routes | `next/later`      | console, tool, worktree, resource, global session list         |
-| `session`                | `later/special`   | large stateful surface plus streaming                          |
-| `sync`                   | `later`           | process/control side effects                                   |
-| `event`                  | `special`         | SSE                                                            |
-| `pty`                    | `special`         | websocket                                                      |
-| `tui`                    | `special`         | UI bridge                                                      |
+| Area                     | Status            | Notes                                                                 |
+| ------------------------ | ----------------- | --------------------------------------------------------------------- |
+| `question`               | `bridged`         | `GET /question`, reply, reject                                        |
+| `permission`             | `bridged`         | list and reply                                                        |
+| `provider`               | `bridged`         | list, auth, OAuth authorize/callback                                  |
+| `config`                 | `bridged` partial | reads only; `PATCH /config` remains Hono                              |
+| `project`                | `bridged` partial | reads only; update and git-init remain Hono                           |
+| `file`                   | `bridged` partial | `GET /file`, `GET /file/content`, `GET /file/status`                  |
+| `mcp`                    | `bridged` partial | `GET /mcp` only                                                       |
+| `workspace`              | `bridged` partial | reads only; create/remove/session-restore remain Hono                 |
+| top-level instance reads | `next`            | `GET /path`, `/vcs`, `/vcs/diff`, `/command`, `/agent`, `/skill`, etc. |
+| experimental JSON routes | `next/later`      | console, tool, worktree, resource, global session list                |
+| `session`                | `later/special`   | large stateful surface plus streaming                                 |
+| `sync`                   | `later`           | process/control side effects                                          |
+| `event`                  | `special`         | SSE                                                                   |
+| `pty`                    | `special`         | websocket                                                             |
+| `tui`                    | `special`         | UI bridge                                                             |
+
+See `specs/effect/http-route-inventory.md` for exact paths and per-route status.
 
 ## Next PRs
 
 1. Add bridge-level auth and instance-context tests for the current `HttpApi` bridge.
-2. Produce a generated route inventory from Hono registrations and update `Current Route Status` with exact paths.
-3. Fix the `workspace` status: mount it if it should be reachable, or remove it from the composed `HttpApi` layer.
-4. Port the top-level JSON reads.
-5. Start the Effect OpenAPI/SDK generation path for already-bridged routes.
+2. Port the top-level JSON reads.
+3. Start the Effect OpenAPI/SDK generation path for already-bridged routes.
 
 ## Checklist
 
@@ -165,8 +171,8 @@ Use raw Effect HTTP routes where `HttpApi` does not fit. The goal is deleting Ho
 - [x] Attach auth middleware in route modules.
 - [x] Support `auth_token` as a query security scheme.
 - [ ] Add bridge-level auth and instance tests.
-- [ ] Complete exact Hono route inventory.
-- [ ] Resolve implemented-but-unmounted route groups.
+- [x] Complete exact Hono route inventory.
+- [x] Resolve implemented-but-unmounted route groups.
 - [ ] Port remaining JSON routes.
 - [ ] Generate SDK/OpenAPI from Effect routes.
 - [ ] Flip ported JSON routes to default-on with fallback.

+ 119 - 0
packages/opencode/specs/effect/http-route-inventory.md

@@ -0,0 +1,119 @@
+# Http Route Inventory
+
+Generated from Hono route registrations by `packages/opencode/script/http-route-inventory.ts`.
+
+Status meanings are defined in `specs/effect/http-api.md`.
+
+| Surface | Method | Path | Status |
+| --- | --- | --- | --- |
+| control | `PUT` | `/auth/:providerID` | `later` |
+| control | `DELETE` | `/auth/:providerID` | `later` |
+| control | `GET` | `/doc` | `later` |
+| control | `POST` | `/log` | `later` |
+| instance | `GET` | `/agent` | `next` |
+| instance | `GET` | `/command` | `next` |
+| instance | `GET` | `/config` | `bridged` |
+| instance | `PATCH` | `/config` | `later` |
+| instance | `GET` | `/config/providers` | `bridged` |
+| instance | `GET` | `/event` | `special` |
+| instance | `GET` | `/experimental/console` | `next` |
+| instance | `GET` | `/experimental/console/orgs` | `next` |
+| instance | `POST` | `/experimental/console/switch` | `later` |
+| instance | `GET` | `/experimental/resource` | `next` |
+| instance | `GET` | `/experimental/session` | `next` |
+| instance | `GET` | `/experimental/tool` | `next` |
+| instance | `GET` | `/experimental/tool/ids` | `next` |
+| instance | `GET` | `/experimental/worktree` | `next` |
+| instance | `POST` | `/experimental/worktree` | `later` |
+| instance | `DELETE` | `/experimental/worktree` | `later` |
+| instance | `POST` | `/experimental/worktree/reset` | `later` |
+| instance | `GET` | `/file` | `bridged` |
+| instance | `GET` | `/file/content` | `bridged` |
+| instance | `GET` | `/file/status` | `bridged` |
+| instance | `GET` | `/find` | `later` |
+| instance | `GET` | `/find/file` | `later` |
+| instance | `GET` | `/find/symbol` | `later` |
+| instance | `GET` | `/formatter` | `next` |
+| instance | `POST` | `/instance/dispose` | `next` |
+| instance | `GET` | `/lsp` | `next` |
+| instance | `GET` | `/mcp` | `bridged` |
+| instance | `POST` | `/mcp` | `later` |
+| instance | `POST` | `/mcp/:name/auth` | `later` |
+| instance | `DELETE` | `/mcp/:name/auth` | `later` |
+| instance | `POST` | `/mcp/:name/auth/authenticate` | `later` |
+| instance | `POST` | `/mcp/:name/auth/callback` | `later` |
+| instance | `POST` | `/mcp/:name/connect` | `later` |
+| instance | `POST` | `/mcp/:name/disconnect` | `later` |
+| instance | `GET` | `/path` | `next` |
+| instance | `GET` | `/permission` | `bridged` |
+| instance | `POST` | `/permission/:requestID/reply` | `bridged` |
+| instance | `GET` | `/project` | `bridged` |
+| instance | `PATCH` | `/project/:projectID` | `later` |
+| instance | `GET` | `/project/current` | `bridged` |
+| instance | `POST` | `/project/git/init` | `later` |
+| instance | `GET` | `/provider` | `bridged` |
+| instance | `POST` | `/provider/:providerID/oauth/authorize` | `bridged` |
+| instance | `POST` | `/provider/:providerID/oauth/callback` | `bridged` |
+| instance | `GET` | `/provider/auth` | `bridged` |
+| instance | `GET` | `/pty` | `special` |
+| instance | `POST` | `/pty` | `special` |
+| instance | `GET` | `/pty/:ptyID` | `special` |
+| instance | `PUT` | `/pty/:ptyID` | `special` |
+| instance | `DELETE` | `/pty/:ptyID` | `special` |
+| instance | `GET` | `/pty/:ptyID/connect` | `special` |
+| instance | `GET` | `/question` | `bridged` |
+| instance | `POST` | `/question/:requestID/reject` | `bridged` |
+| instance | `POST` | `/question/:requestID/reply` | `bridged` |
+| instance | `GET` | `/session` | `later` |
+| instance | `POST` | `/session` | `later` |
+| instance | `GET` | `/session/:sessionID` | `later` |
+| instance | `PATCH` | `/session/:sessionID` | `later` |
+| instance | `DELETE` | `/session/:sessionID` | `later` |
+| instance | `POST` | `/session/:sessionID/abort` | `later` |
+| instance | `GET` | `/session/:sessionID/children` | `later` |
+| instance | `POST` | `/session/:sessionID/command` | `later` |
+| instance | `GET` | `/session/:sessionID/diff` | `later` |
+| instance | `POST` | `/session/:sessionID/fork` | `later` |
+| instance | `POST` | `/session/:sessionID/init` | `later` |
+| instance | `GET` | `/session/:sessionID/message` | `later` |
+| instance | `POST` | `/session/:sessionID/message` | `later` |
+| instance | `GET` | `/session/:sessionID/message/:messageID` | `later` |
+| instance | `DELETE` | `/session/:sessionID/message/:messageID` | `later` |
+| instance | `PATCH` | `/session/:sessionID/message/:messageID/part/:partID` | `later` |
+| instance | `DELETE` | `/session/:sessionID/message/:messageID/part/:partID` | `later` |
+| instance | `POST` | `/session/:sessionID/permissions/:permissionID` | `later` |
+| instance | `POST` | `/session/:sessionID/prompt_async` | `later` |
+| instance | `POST` | `/session/:sessionID/revert` | `later` |
+| instance | `POST` | `/session/:sessionID/share` | `later` |
+| instance | `DELETE` | `/session/:sessionID/share` | `later` |
+| instance | `POST` | `/session/:sessionID/shell` | `later` |
+| instance | `POST` | `/session/:sessionID/summarize` | `later` |
+| instance | `GET` | `/session/:sessionID/todo` | `later` |
+| instance | `POST` | `/session/:sessionID/unrevert` | `later` |
+| instance | `GET` | `/session/status` | `later` |
+| instance | `GET` | `/skill` | `next` |
+| instance | `POST` | `/sync/history` | `later` |
+| instance | `POST` | `/sync/replay` | `later` |
+| instance | `POST` | `/sync/start` | `later` |
+| instance | `POST` | `/tui/append-prompt` | `special` |
+| instance | `POST` | `/tui/clear-prompt` | `special` |
+| instance | `GET` | `/tui/control/next` | `special` |
+| instance | `POST` | `/tui/control/response` | `special` |
+| instance | `POST` | `/tui/execute-command` | `special` |
+| instance | `POST` | `/tui/open-help` | `special` |
+| instance | `POST` | `/tui/open-models` | `special` |
+| instance | `POST` | `/tui/open-sessions` | `special` |
+| instance | `POST` | `/tui/open-themes` | `special` |
+| instance | `POST` | `/tui/publish` | `special` |
+| instance | `POST` | `/tui/select-session` | `special` |
+| instance | `POST` | `/tui/show-toast` | `special` |
+| instance | `POST` | `/tui/submit-prompt` | `special` |
+| instance | `GET` | `/vcs` | `next` |
+| instance | `GET` | `/vcs/diff` | `next` |
+| ui | `ALL` | `/*` | `special` |
+| workspace | `GET` | `/experimental/workspace` | `bridged` |
+| workspace | `POST` | `/experimental/workspace` | `later` |
+| workspace | `DELETE` | `/experimental/workspace/:id` | `later` |
+| workspace | `POST` | `/experimental/workspace/:id/session-restore` | `later` |
+| workspace | `GET` | `/experimental/workspace/adaptor` | `bridged` |
+| workspace | `GET` | `/experimental/workspace/status` | `bridged` |