feat(httpapi): bridge project update endpoint (#24398)
This commit is contained in:
@@ -176,7 +176,7 @@ Use raw Effect HTTP routes where `HttpApi` does not fit. The goal is deleting Ho
|
|||||||
| `permission` | `bridged` | list and reply |
|
| `permission` | `bridged` | list and reply |
|
||||||
| `provider` | `bridged` | list, auth, OAuth authorize/callback |
|
| `provider` | `bridged` | list, auth, OAuth authorize/callback |
|
||||||
| `config` | `bridged` | read, providers, update |
|
| `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 |
|
| `file` | `bridged` partial | find text/file/symbol, list/content/status |
|
||||||
| `mcp` | `bridged` partial | status only |
|
| `mcp` | `bridged` partial | status only |
|
||||||
| `workspace` | `bridged` | list, get, enter |
|
| `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 |
|
| `pty` | `special` | websocket |
|
||||||
| `tui` | `special` | UI bridge |
|
| `tui` | `special` | UI bridge |
|
||||||
|
|
||||||
## Next PRs
|
## Remaining PR Plan
|
||||||
|
|
||||||
1. Produce a generated route inventory from Hono registrations and update `Current Route Status` with exact paths.
|
Prefer smaller PRs from here so route behavior and SDK/OpenAPI fallout stays reviewable.
|
||||||
2. Start the Effect OpenAPI/SDK generation path for already-bridged routes.
|
|
||||||
|
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
|
## Checklist
|
||||||
|
|
||||||
|
|||||||
@@ -91,6 +91,15 @@ export const UpdateInput = z.object({
|
|||||||
})
|
})
|
||||||
export type UpdateInput = z.infer<typeof UpdateInput>
|
export type UpdateInput = z.infer<typeof UpdateInput>
|
||||||
|
|
||||||
|
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<Schema.Schema.Type<typeof UpdatePayload>>
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
// Effect service
|
// Effect service
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import * as InstanceState from "@/effect/instance-state"
|
|||||||
import { AppRuntime } from "@/effect/app-runtime"
|
import { AppRuntime } from "@/effect/app-runtime"
|
||||||
import { Project } from "@/project"
|
import { Project } from "@/project"
|
||||||
import { InstanceBootstrap } from "@/project/bootstrap"
|
import { InstanceBootstrap } from "@/project/bootstrap"
|
||||||
|
import { ProjectID } from "@/project/schema"
|
||||||
import { Effect, Layer, Schema } from "effect"
|
import { Effect, Layer, Schema } from "effect"
|
||||||
import { HttpApi, HttpApiBuilder, HttpApiEndpoint, HttpApiGroup, OpenApi } from "effect/unstable/httpapi"
|
import { HttpApi, HttpApiBuilder, HttpApiEndpoint, HttpApiGroup, OpenApi } from "effect/unstable/httpapi"
|
||||||
import { Authorization } from "./auth"
|
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.",
|
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(
|
.annotateMerge(
|
||||||
OpenApi.annotations({
|
OpenApi.annotations({
|
||||||
@@ -83,8 +95,15 @@ export const projectHandlers = Layer.unwrap(
|
|||||||
return next
|
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) =>
|
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))
|
).pipe(Layer.provide(Project.defaultLayer))
|
||||||
|
|||||||
@@ -62,6 +62,7 @@ export const InstanceRoutes = (upgrade: UpgradeWebSocket): Hono => {
|
|||||||
app.get("/project", (c) => handler(c.req.raw, context))
|
app.get("/project", (c) => handler(c.req.raw, context))
|
||||||
app.get("/project/current", (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.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.findText, (c) => handler(c.req.raw, context))
|
||||||
app.get(FilePaths.findFile, (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))
|
app.get(FilePaths.findSymbol, (c) => handler(c.req.raw, context))
|
||||||
|
|||||||
@@ -115,6 +115,33 @@ describe("instance HttpApi", () => {
|
|||||||
expect(await current.json()).toMatchObject({ vcs: "git", worktree: tmp.path })
|
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 () => {
|
test("serves instance dispose through Hono bridge", async () => {
|
||||||
await using tmp = await tmpdir()
|
await using tmp = await tmpdir()
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user