diff --git a/packages/opencode/specs/effect/http-api.md b/packages/opencode/specs/effect/http-api.md index 261c8b76b031..e5a64d920922 100644 --- a/packages/opencode/specs/effect/http-api.md +++ b/packages/opencode/specs/effect/http-api.md @@ -176,7 +176,7 @@ Use raw Effect HTTP routes where `HttpApi` does not fit. The goal is deleting Ho | `permission` | `bridged` | list and reply | | `provider` | `bridged` | list, auth, OAuth authorize/callback | | `config` | `bridged` | read, providers, update | -| `project` | `bridged` | list, current, git init | +| `project` | `bridged` | list, current, git init, update | | `file` | `bridged` partial | find text/file/symbol, list/content/status | | `mcp` | `bridged` partial | status only | | `workspace` | `bridged` | list, get, enter | @@ -188,10 +188,24 @@ Use raw Effect HTTP routes where `HttpApi` does not fit. The goal is deleting Ho | `pty` | `special` | websocket | | `tui` | `special` | UI bridge | -## Next PRs - -1. Produce a generated route inventory from Hono registrations and update `Current Route Status` with exact paths. -2. Start the Effect OpenAPI/SDK generation path for already-bridged routes. +## Remaining PR Plan + +Prefer smaller PRs from here so route behavior and SDK/OpenAPI fallout stays reviewable. + +1. Bridge `PATCH /project/:projectID`. +2. Bridge MCP add/connect/disconnect routes. +3. Bridge MCP OAuth routes: start, callback, authenticate, remove. +4. Bridge experimental console switch and tool list routes. +5. Bridge experimental global session list. +6. Bridge sync start/replay/history routes. +7. Bridge session read routes: list, status, get, children, todo, diff, messages. +8. Bridge session lifecycle mutation routes: create, delete, update, fork, abort. +9. Bridge session share/summary/message/part mutation routes. +10. Replace event SSE with non-Hono Effect HTTP. +11. Replace pty websocket/control routes with non-Hono Effect HTTP. +12. Replace tui bridge routes or explicitly isolate them behind a non-Hono compatibility layer. +13. Switch OpenAPI/SDK generation to Effect routes and compare SDK output. +14. Flip ported JSON routes default-on, keep a short fallback, then delete replaced Hono route files. ## Checklist diff --git a/packages/opencode/src/project/project.ts b/packages/opencode/src/project/project.ts index fc34a6296f1e..c26114506d48 100644 --- a/packages/opencode/src/project/project.ts +++ b/packages/opencode/src/project/project.ts @@ -91,6 +91,15 @@ export const UpdateInput = z.object({ }) export type UpdateInput = z.infer +export const UpdatePayload = Schema.Struct({ + name: Schema.optional(Schema.String), + icon: Schema.optional(ProjectIcon), + commands: Schema.optional(ProjectCommands), +}) + .annotate({ identifier: "ProjectUpdateInput" }) + .pipe(withStatics((s) => ({ zod: zod(s) }))) +export type UpdatePayload = Types.DeepMutable> + // --------------------------------------------------------------------------- // Effect service // --------------------------------------------------------------------------- diff --git a/packages/opencode/src/server/routes/instance/httpapi/project.ts b/packages/opencode/src/server/routes/instance/httpapi/project.ts index 95a11a1a5e60..63190180ccb6 100644 --- a/packages/opencode/src/server/routes/instance/httpapi/project.ts +++ b/packages/opencode/src/server/routes/instance/httpapi/project.ts @@ -2,6 +2,7 @@ import * as InstanceState from "@/effect/instance-state" import { AppRuntime } from "@/effect/app-runtime" import { Project } from "@/project" import { InstanceBootstrap } from "@/project/bootstrap" +import { ProjectID } from "@/project/schema" import { Effect, Layer, Schema } from "effect" import { HttpApi, HttpApiBuilder, HttpApiEndpoint, HttpApiGroup, OpenApi } from "effect/unstable/httpapi" import { Authorization } from "./auth" @@ -40,6 +41,17 @@ export const ProjectApi = HttpApi.make("project") description: "Create a git repository for the current project and return the refreshed project info.", }), ), + HttpApiEndpoint.patch("update", `${root}/:projectID`, { + params: { projectID: ProjectID }, + payload: Project.UpdatePayload, + success: Project.Info, + }).annotateMerge( + OpenApi.annotations({ + identifier: "project.update", + summary: "Update project", + description: "Update project properties such as name, icon, and commands.", + }), + ), ) .annotateMerge( OpenApi.annotations({ @@ -83,8 +95,15 @@ export const projectHandlers = Layer.unwrap( return next }) + const update = Effect.fn("ProjectHttpApi.update")(function* (ctx: { + params: { projectID: ProjectID } + payload: Project.UpdatePayload + }) { + return yield* svc.update({ ...Project.UpdatePayload.zod.parse(ctx.payload), projectID: ctx.params.projectID }) + }) + return HttpApiBuilder.group(ProjectApi, "project", (handlers) => - handlers.handle("list", list).handle("current", current).handle("initGit", initGit), + handlers.handle("list", list).handle("current", current).handle("initGit", initGit).handle("update", update), ) }), ).pipe(Layer.provide(Project.defaultLayer)) diff --git a/packages/opencode/src/server/routes/instance/index.ts b/packages/opencode/src/server/routes/instance/index.ts index 8b8126f5defe..8d341b8a05c9 100644 --- a/packages/opencode/src/server/routes/instance/index.ts +++ b/packages/opencode/src/server/routes/instance/index.ts @@ -62,6 +62,7 @@ export const InstanceRoutes = (upgrade: UpgradeWebSocket): Hono => { app.get("/project", (c) => handler(c.req.raw, context)) app.get("/project/current", (c) => handler(c.req.raw, context)) app.post("/project/git/init", (c) => handler(c.req.raw, context)) + app.patch("/project/:projectID", (c) => handler(c.req.raw, context)) app.get(FilePaths.findText, (c) => handler(c.req.raw, context)) app.get(FilePaths.findFile, (c) => handler(c.req.raw, context)) app.get(FilePaths.findSymbol, (c) => handler(c.req.raw, context)) diff --git a/packages/opencode/test/server/httpapi-instance.test.ts b/packages/opencode/test/server/httpapi-instance.test.ts index 139712652719..404807984997 100644 --- a/packages/opencode/test/server/httpapi-instance.test.ts +++ b/packages/opencode/test/server/httpapi-instance.test.ts @@ -115,6 +115,33 @@ describe("instance HttpApi", () => { expect(await current.json()).toMatchObject({ vcs: "git", worktree: tmp.path }) }) + test("serves project update through Hono bridge", async () => { + await using tmp = await tmpdir({ config: { formatter: false, lsp: false } }) + + const current = await app().request("/project/current", { headers: { "x-opencode-directory": tmp.path } }) + expect(current.status).toBe(200) + const project = (await current.json()) as { id: string } + + const response = await app().request(`/project/${project.id}`, { + method: "PATCH", + headers: { "x-opencode-directory": tmp.path, "content-type": "application/json" }, + body: JSON.stringify({ name: "patched-project", commands: { start: "bun dev" } }), + }) + + expect(response.status).toBe(200) + expect(await response.json()).toMatchObject({ + id: project.id, + name: "patched-project", + commands: { start: "bun dev" }, + }) + + const list = await app().request("/project", { headers: { "x-opencode-directory": tmp.path } }) + expect(list.status).toBe(200) + expect(await list.json()).toContainEqual( + expect.objectContaining({ id: project.id, name: "patched-project", commands: { start: "bun dev" } }), + ) + }) + test("serves instance dispose through Hono bridge", async () => { await using tmp = await tmpdir()