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)
|
- `project` - `bridged` (partial)
|
||||||
bridged endpoints: `GET /project`, `GET /project/current`
|
bridged endpoints: `GET /project`, `GET /project/current`
|
||||||
defer git-init mutation first
|
defer git-init mutation first
|
||||||
- `workspace` - `next`
|
- `workspace` - `bridged`
|
||||||
best small reads: `GET /experimental/workspace/adaptor`, `GET /experimental/workspace`, `GET /experimental/workspace/status`
|
best small reads: `GET /experimental/workspace/adaptor`, `GET /experimental/workspace`, `GET /experimental/workspace/status`
|
||||||
defer create/remove mutations first
|
defer create/remove mutations first
|
||||||
- `file` - `later`
|
- `file` - `later`
|
||||||
|
|
@ -448,7 +448,7 @@ Recommended near-term sequence:
|
||||||
- [x] port `config` providers read endpoint
|
- [x] port `config` providers read endpoint
|
||||||
- [x] port `project` read endpoints (`GET /project`, `GET /project/current`)
|
- [x] port `project` read endpoints (`GET /project`, `GET /project/current`)
|
||||||
- [x] port `GET /config` full read endpoint
|
- [x] port `GET /config` full read endpoint
|
||||||
- [ ] port `workspace` read endpoints
|
- [x] port `workspace` read endpoints
|
||||||
- [ ] port `file` JSON read endpoints
|
- [ ] port `file` JSON read endpoints
|
||||||
- [ ] decide when to remove the flag and make Effect routes the default
|
- [ ] 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 { ProjectApi, projectHandlers } from "./project"
|
||||||
import { ProviderApi, providerHandlers } from "./provider"
|
import { ProviderApi, providerHandlers } from "./provider"
|
||||||
import { QuestionApi, questionHandlers } from "./question"
|
import { QuestionApi, questionHandlers } from "./question"
|
||||||
|
import { WorkspaceApi, workspaceHandlers } from "./workspace"
|
||||||
import { memoMap } from "@/effect/memo-map"
|
import { memoMap } from "@/effect/memo-map"
|
||||||
|
|
||||||
const Query = Schema.Struct({
|
const Query = Schema.Struct({
|
||||||
|
|
@ -112,6 +113,7 @@ const PermissionSecured = PermissionApi.middleware(Authorization)
|
||||||
const ProjectSecured = ProjectApi.middleware(Authorization)
|
const ProjectSecured = ProjectApi.middleware(Authorization)
|
||||||
const ProviderSecured = ProviderApi.middleware(Authorization)
|
const ProviderSecured = ProviderApi.middleware(Authorization)
|
||||||
const ConfigSecured = ConfigApi.middleware(Authorization)
|
const ConfigSecured = ConfigApi.middleware(Authorization)
|
||||||
|
const WorkspaceSecured = WorkspaceApi.middleware(Authorization)
|
||||||
|
|
||||||
export const routes = Layer.mergeAll(
|
export const routes = Layer.mergeAll(
|
||||||
HttpApiBuilder.layer(ConfigSecured).pipe(Layer.provide(configHandlers)),
|
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(QuestionSecured).pipe(Layer.provide(questionHandlers)),
|
||||||
HttpApiBuilder.layer(PermissionSecured).pipe(Layer.provide(permissionHandlers)),
|
HttpApiBuilder.layer(PermissionSecured).pipe(Layer.provide(permissionHandlers)),
|
||||||
HttpApiBuilder.layer(ProviderSecured).pipe(Layer.provide(providerHandlers)),
|
HttpApiBuilder.layer(ProviderSecured).pipe(Layer.provide(providerHandlers)),
|
||||||
|
HttpApiBuilder.layer(WorkspaceSecured).pipe(Layer.provide(workspaceHandlers)),
|
||||||
).pipe(
|
).pipe(
|
||||||
Layer.provide(auth),
|
Layer.provide(auth),
|
||||||
Layer.provide(normalize),
|
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 { WorkspaceRouterMiddleware } from "./workspace"
|
||||||
import { InstanceMiddleware } from "./routes/instance/middleware"
|
import { InstanceMiddleware } from "./routes/instance/middleware"
|
||||||
import { WorkspaceRoutes } from "./routes/control/workspace"
|
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
|
// @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
|
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 {
|
return {
|
||||||
app: app
|
app: app
|
||||||
.route("/", ControlPlaneRoutes())
|
.route("/", ControlPlaneRoutes())
|
||||||
.route(
|
.route("/", workspaceApp)
|
||||||
"/",
|
|
||||||
new Hono()
|
|
||||||
.use(InstanceMiddleware())
|
|
||||||
.route("/experimental/workspace", WorkspaceRoutes())
|
|
||||||
.use(WorkspaceRouterMiddleware(runtime.upgradeWebSocket)),
|
|
||||||
)
|
|
||||||
.route("/", InstanceRoutes(runtime.upgradeWebSocket))
|
.route("/", InstanceRoutes(runtime.upgradeWebSocket))
|
||||||
.route("/", UIRoutes()),
|
.route("/", UIRoutes()),
|
||||||
runtime,
|
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