mirror of
https://github.com/anomalyco/opencode.git
synced 2026-04-28 04:29:42 +00:00
feat(httpapi): bridge workspace read endpoints (#24062)
This commit is contained in:
parent
334ab4707c
commit
e50a688ca3
5 changed files with 160 additions and 9 deletions
|
|
@ -409,7 +409,7 @@ Current instance route inventory:
|
|||
- `project` - `bridged` (partial)
|
||||
bridged endpoints: `GET /project`, `GET /project/current`
|
||||
defer git-init mutation first
|
||||
- `workspace` - `next`
|
||||
- `workspace` - `bridged`
|
||||
best small reads: `GET /experimental/workspace/adaptor`, `GET /experimental/workspace`, `GET /experimental/workspace/status`
|
||||
defer create/remove mutations first
|
||||
- `file` - `later`
|
||||
|
|
@ -448,7 +448,7 @@ Recommended near-term sequence:
|
|||
- [x] port `config` providers read endpoint
|
||||
- [x] port `project` read endpoints (`GET /project`, `GET /project/current`)
|
||||
- [x] port `GET /config` full read endpoint
|
||||
- [ ] port `workspace` read endpoints
|
||||
- [x] port `workspace` read endpoints
|
||||
- [ ] port `file` JSON read endpoints
|
||||
- [ ] decide when to remove the flag and make Effect routes the default
|
||||
|
||||
|
|
|
|||
|
|
@ -14,6 +14,7 @@ import { PermissionApi, permissionHandlers } from "./permission"
|
|||
import { ProjectApi, projectHandlers } from "./project"
|
||||
import { ProviderApi, providerHandlers } from "./provider"
|
||||
import { QuestionApi, questionHandlers } from "./question"
|
||||
import { WorkspaceApi, workspaceHandlers } from "./workspace"
|
||||
import { memoMap } from "@/effect/memo-map"
|
||||
|
||||
const Query = Schema.Struct({
|
||||
|
|
@ -112,6 +113,7 @@ const PermissionSecured = PermissionApi.middleware(Authorization)
|
|||
const ProjectSecured = ProjectApi.middleware(Authorization)
|
||||
const ProviderSecured = ProviderApi.middleware(Authorization)
|
||||
const ConfigSecured = ConfigApi.middleware(Authorization)
|
||||
const WorkspaceSecured = WorkspaceApi.middleware(Authorization)
|
||||
|
||||
export const routes = Layer.mergeAll(
|
||||
HttpApiBuilder.layer(ConfigSecured).pipe(Layer.provide(configHandlers)),
|
||||
|
|
@ -119,6 +121,7 @@ export const routes = Layer.mergeAll(
|
|||
HttpApiBuilder.layer(QuestionSecured).pipe(Layer.provide(questionHandlers)),
|
||||
HttpApiBuilder.layer(PermissionSecured).pipe(Layer.provide(permissionHandlers)),
|
||||
HttpApiBuilder.layer(ProviderSecured).pipe(Layer.provide(providerHandlers)),
|
||||
HttpApiBuilder.layer(WorkspaceSecured).pipe(Layer.provide(workspaceHandlers)),
|
||||
).pipe(
|
||||
Layer.provide(auth),
|
||||
Layer.provide(normalize),
|
||||
|
|
|
|||
|
|
@ -0,0 +1,82 @@
|
|||
import { listAdaptors } from "@/control-plane/adaptors"
|
||||
import { Workspace } from "@/control-plane/workspace"
|
||||
import { WorkspaceAdaptorEntry } from "@/control-plane/types"
|
||||
import * as InstanceState from "@/effect/instance-state"
|
||||
import { Effect, Layer, Schema } from "effect"
|
||||
import { HttpApi, HttpApiBuilder, HttpApiEndpoint, HttpApiGroup, OpenApi } from "effect/unstable/httpapi"
|
||||
|
||||
const root = "/experimental/workspace"
|
||||
export const WorkspacePaths = {
|
||||
adaptors: `${root}/adaptor`,
|
||||
list: root,
|
||||
status: `${root}/status`,
|
||||
} as const
|
||||
|
||||
export const WorkspaceApi = HttpApi.make("workspace")
|
||||
.add(
|
||||
HttpApiGroup.make("workspace")
|
||||
.add(
|
||||
HttpApiEndpoint.get("adaptors", WorkspacePaths.adaptors, {
|
||||
success: Schema.Array(WorkspaceAdaptorEntry),
|
||||
}).annotateMerge(
|
||||
OpenApi.annotations({
|
||||
identifier: "experimental.workspace.adaptor.list",
|
||||
summary: "List workspace adaptors",
|
||||
description: "List all available workspace adaptors for the current project.",
|
||||
}),
|
||||
),
|
||||
HttpApiEndpoint.get("list", WorkspacePaths.list, {
|
||||
success: Schema.Array(Workspace.Info),
|
||||
}).annotateMerge(
|
||||
OpenApi.annotations({
|
||||
identifier: "experimental.workspace.list",
|
||||
summary: "List workspaces",
|
||||
description: "List all workspaces.",
|
||||
}),
|
||||
),
|
||||
HttpApiEndpoint.get("status", WorkspacePaths.status, {
|
||||
success: Schema.Array(Workspace.ConnectionStatus),
|
||||
}).annotateMerge(
|
||||
OpenApi.annotations({
|
||||
identifier: "experimental.workspace.status",
|
||||
summary: "Workspace status",
|
||||
description: "Get connection status for workspaces in the current project.",
|
||||
}),
|
||||
),
|
||||
)
|
||||
.annotateMerge(
|
||||
OpenApi.annotations({
|
||||
title: "workspace",
|
||||
description: "Experimental HttpApi workspace routes.",
|
||||
}),
|
||||
),
|
||||
)
|
||||
.annotateMerge(
|
||||
OpenApi.annotations({
|
||||
title: "opencode experimental HttpApi",
|
||||
version: "0.0.1",
|
||||
description: "Experimental HttpApi surface for selected instance routes.",
|
||||
}),
|
||||
)
|
||||
|
||||
export const workspaceHandlers = Layer.unwrap(
|
||||
Effect.gen(function* () {
|
||||
const adaptors = Effect.fn("WorkspaceHttpApi.adaptors")(function* () {
|
||||
const ctx = yield* InstanceState.context
|
||||
return yield* Effect.promise(() => listAdaptors(ctx.project.id))
|
||||
})
|
||||
|
||||
const list = Effect.fn("WorkspaceHttpApi.list")(function* () {
|
||||
return Workspace.list((yield* InstanceState.context).project)
|
||||
})
|
||||
|
||||
const status = Effect.fn("WorkspaceHttpApi.status")(function* () {
|
||||
const ids = new Set(Workspace.list((yield* InstanceState.context).project).map((item) => item.id))
|
||||
return Workspace.status().filter((item) => ids.has(item.workspaceID))
|
||||
})
|
||||
|
||||
return HttpApiBuilder.group(WorkspaceApi, "workspace", (handlers) =>
|
||||
handlers.handle("adaptors", adaptors).handle("list", list).handle("status", status),
|
||||
)
|
||||
}),
|
||||
)
|
||||
|
|
@ -16,6 +16,9 @@ import { GlobalRoutes } from "./routes/global"
|
|||
import { WorkspaceRouterMiddleware } from "./workspace"
|
||||
import { InstanceMiddleware } from "./routes/instance/middleware"
|
||||
import { WorkspaceRoutes } from "./routes/control/workspace"
|
||||
import { ExperimentalHttpApiServer } from "./routes/instance/httpapi/server"
|
||||
import { WorkspacePaths } from "./routes/instance/httpapi/workspace"
|
||||
import { Context } from "effect"
|
||||
|
||||
// @ts-ignore This global is needed to prevent ai-sdk from logging warnings to stdout https://github.com/vercel/ai/blob/2dc67e0ef538307f21368db32d5a12345d98831b/packages/ai/src/logger/log-warnings.ts#L85
|
||||
globalThis.AI_SDK_LOG_WARNINGS = false
|
||||
|
|
@ -54,16 +57,24 @@ function create(opts: { cors?: string[] }) {
|
|||
}
|
||||
}
|
||||
|
||||
const workspaceApp = new Hono()
|
||||
const workspaceLegacyApp = new Hono()
|
||||
.use(InstanceMiddleware())
|
||||
.route("/experimental/workspace", WorkspaceRoutes())
|
||||
.use(WorkspaceRouterMiddleware(runtime.upgradeWebSocket))
|
||||
if (Flag.OPENCODE_EXPERIMENTAL_HTTPAPI) {
|
||||
const handler = ExperimentalHttpApiServer.webHandler().handler
|
||||
const context = Context.empty() as Context.Context<unknown>
|
||||
workspaceApp.get(WorkspacePaths.adaptors, (c) => handler(c.req.raw, context))
|
||||
workspaceApp.get(WorkspacePaths.list, (c) => handler(c.req.raw, context))
|
||||
workspaceApp.get(WorkspacePaths.status, (c) => handler(c.req.raw, context))
|
||||
}
|
||||
workspaceApp.route("/", workspaceLegacyApp)
|
||||
|
||||
return {
|
||||
app: app
|
||||
.route("/", ControlPlaneRoutes())
|
||||
.route(
|
||||
"/",
|
||||
new Hono()
|
||||
.use(InstanceMiddleware())
|
||||
.route("/experimental/workspace", WorkspaceRoutes())
|
||||
.use(WorkspaceRouterMiddleware(runtime.upgradeWebSocket)),
|
||||
)
|
||||
.route("/", workspaceApp)
|
||||
.route("/", InstanceRoutes(runtime.upgradeWebSocket))
|
||||
.route("/", UIRoutes()),
|
||||
runtime,
|
||||
|
|
|
|||
55
packages/opencode/test/server/httpapi-workspace.test.ts
Normal file
55
packages/opencode/test/server/httpapi-workspace.test.ts
Normal file
|
|
@ -0,0 +1,55 @@
|
|||
import { afterEach, describe, expect, test } from "bun:test"
|
||||
import { Context } from "effect"
|
||||
import { ExperimentalHttpApiServer } from "../../src/server/routes/instance/httpapi/server"
|
||||
import { WorkspacePaths } from "../../src/server/routes/instance/httpapi/workspace"
|
||||
import { Log } from "../../src/util"
|
||||
import { resetDatabase } from "../fixture/db"
|
||||
import { tmpdir } from "../fixture/fixture"
|
||||
import { Instance } from "../../src/project/instance"
|
||||
|
||||
void Log.init({ print: false })
|
||||
|
||||
const context = Context.empty() as Context.Context<unknown>
|
||||
|
||||
function request(path: string, directory: string) {
|
||||
return ExperimentalHttpApiServer.webHandler().handler(
|
||||
new Request(`http://localhost${path}`, {
|
||||
headers: {
|
||||
"x-opencode-directory": directory,
|
||||
},
|
||||
}),
|
||||
context,
|
||||
)
|
||||
}
|
||||
|
||||
afterEach(async () => {
|
||||
await Instance.disposeAll()
|
||||
await resetDatabase()
|
||||
})
|
||||
|
||||
describe("workspace HttpApi", () => {
|
||||
test("serves read endpoints", async () => {
|
||||
await using tmp = await tmpdir({ git: true })
|
||||
|
||||
const [adaptors, workspaces, status] = await Promise.all([
|
||||
request(WorkspacePaths.adaptors, tmp.path),
|
||||
request(WorkspacePaths.list, tmp.path),
|
||||
request(WorkspacePaths.status, tmp.path),
|
||||
])
|
||||
|
||||
expect(adaptors.status).toBe(200)
|
||||
expect(await adaptors.json()).toEqual([
|
||||
{
|
||||
type: "worktree",
|
||||
name: "Worktree",
|
||||
description: "Create a git worktree",
|
||||
},
|
||||
])
|
||||
|
||||
expect(workspaces.status).toBe(200)
|
||||
expect(await workspaces.json()).toEqual([])
|
||||
|
||||
expect(status.status).toBe(200)
|
||||
expect(await status.json()).toEqual([])
|
||||
})
|
||||
})
|
||||
Loading…
Add table
Add a link
Reference in a new issue