feat(core): remove workspace server and reuse core server in workspaces
parent
bcf18edde4
commit
77aabd9c57
|
|
@ -1,16 +0,0 @@
|
||||||
import { cmd } from "./cmd"
|
|
||||||
import { withNetworkOptions, resolveNetworkOptions } from "../network"
|
|
||||||
import { WorkspaceServer } from "../../control-plane/workspace-server/server"
|
|
||||||
|
|
||||||
export const WorkspaceServeCommand = cmd({
|
|
||||||
command: "workspace-serve",
|
|
||||||
builder: (yargs) => withNetworkOptions(yargs),
|
|
||||||
describe: "starts a remote workspace event server",
|
|
||||||
handler: async (args) => {
|
|
||||||
const opts = await resolveNetworkOptions(args)
|
|
||||||
const server = WorkspaceServer.Listen(opts)
|
|
||||||
console.log(`workspace event server listening on http://${server.hostname}:${server.port}/event`)
|
|
||||||
await new Promise(() => {})
|
|
||||||
await server.stop()
|
|
||||||
},
|
|
||||||
})
|
|
||||||
|
|
@ -2,6 +2,8 @@ import z from "zod"
|
||||||
import { Worktree } from "@/worktree"
|
import { Worktree } from "@/worktree"
|
||||||
import { type Adaptor, WorkspaceInfo } from "../types"
|
import { type Adaptor, WorkspaceInfo } from "../types"
|
||||||
|
|
||||||
|
import { Server } from "../../server/server"
|
||||||
|
|
||||||
const Config = WorkspaceInfo.extend({
|
const Config = WorkspaceInfo.extend({
|
||||||
name: WorkspaceInfo.shape.name.unwrap(),
|
name: WorkspaceInfo.shape.name.unwrap(),
|
||||||
branch: WorkspaceInfo.shape.branch.unwrap(),
|
branch: WorkspaceInfo.shape.branch.unwrap(),
|
||||||
|
|
@ -34,12 +36,11 @@ export const WorktreeAdaptor: Adaptor = {
|
||||||
},
|
},
|
||||||
async fetch(info, input: RequestInfo | URL, init?: RequestInit) {
|
async fetch(info, input: RequestInfo | URL, init?: RequestInit) {
|
||||||
const config = Config.parse(info)
|
const config = Config.parse(info)
|
||||||
const { WorkspaceServer } = await import("../workspace-server/server")
|
|
||||||
const url = input instanceof Request || input instanceof URL ? input : new URL(input, "http://opencode.internal")
|
const url = input instanceof Request || input instanceof URL ? input : new URL(input, "http://opencode.internal")
|
||||||
const headers = new Headers(init?.headers ?? (input instanceof Request ? input.headers : undefined))
|
const headers = new Headers(init?.headers ?? (input instanceof Request ? input.headers : undefined))
|
||||||
headers.set("x-opencode-directory", config.directory)
|
headers.set("x-opencode-directory", config.directory)
|
||||||
|
|
||||||
const request = new Request(url, { ...init, headers })
|
const request = new Request(url, { ...init, headers })
|
||||||
return WorkspaceServer.App().fetch(request)
|
return Server.Default().fetch(request)
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,33 +0,0 @@
|
||||||
import { GlobalBus } from "../../bus/global"
|
|
||||||
import { Hono } from "hono"
|
|
||||||
import { streamSSE } from "hono/streaming"
|
|
||||||
|
|
||||||
export function WorkspaceServerRoutes() {
|
|
||||||
return new Hono().get("/event", async (c) => {
|
|
||||||
c.header("X-Accel-Buffering", "no")
|
|
||||||
c.header("X-Content-Type-Options", "nosniff")
|
|
||||||
return streamSSE(c, async (stream) => {
|
|
||||||
const send = async (event: unknown) => {
|
|
||||||
await stream.writeSSE({
|
|
||||||
data: JSON.stringify(event),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
const handler = async (event: { directory?: string; payload: unknown }) => {
|
|
||||||
await send(event.payload)
|
|
||||||
}
|
|
||||||
GlobalBus.on("event", handler)
|
|
||||||
await send({ type: "server.connected", properties: {} })
|
|
||||||
const heartbeat = setInterval(() => {
|
|
||||||
void send({ type: "server.heartbeat", properties: {} })
|
|
||||||
}, 10_000)
|
|
||||||
|
|
||||||
await new Promise<void>((resolve) => {
|
|
||||||
stream.onAbort(() => {
|
|
||||||
clearInterval(heartbeat)
|
|
||||||
GlobalBus.off("event", handler)
|
|
||||||
resolve()
|
|
||||||
})
|
|
||||||
})
|
|
||||||
})
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
@ -1,65 +0,0 @@
|
||||||
import { Hono } from "hono"
|
|
||||||
import { Instance } from "../../project/instance"
|
|
||||||
import { InstanceBootstrap } from "../../project/bootstrap"
|
|
||||||
import { SessionRoutes } from "../../server/routes/session"
|
|
||||||
import { WorkspaceServerRoutes } from "./routes"
|
|
||||||
import { WorkspaceContext } from "../workspace-context"
|
|
||||||
import { WorkspaceID } from "../schema"
|
|
||||||
|
|
||||||
export namespace WorkspaceServer {
|
|
||||||
export function App() {
|
|
||||||
const session = new Hono()
|
|
||||||
.use(async (c, next) => {
|
|
||||||
// Right now, we need handle all requests because we don't
|
|
||||||
// have syncing. In the future all GET requests will handled
|
|
||||||
// by the control plane
|
|
||||||
//
|
|
||||||
// if (c.req.method === "GET") return c.notFound()
|
|
||||||
await next()
|
|
||||||
})
|
|
||||||
.route("/", SessionRoutes())
|
|
||||||
|
|
||||||
return new Hono()
|
|
||||||
.use(async (c, next) => {
|
|
||||||
const rawWorkspaceID = c.req.query("workspace") || c.req.header("x-opencode-workspace")
|
|
||||||
const raw = c.req.query("directory") || c.req.header("x-opencode-directory")
|
|
||||||
if (rawWorkspaceID == null) {
|
|
||||||
throw new Error("workspaceID parameter is required")
|
|
||||||
}
|
|
||||||
if (raw == null) {
|
|
||||||
throw new Error("directory parameter is required")
|
|
||||||
}
|
|
||||||
|
|
||||||
const directory = (() => {
|
|
||||||
try {
|
|
||||||
return decodeURIComponent(raw)
|
|
||||||
} catch {
|
|
||||||
return raw
|
|
||||||
}
|
|
||||||
})()
|
|
||||||
|
|
||||||
return WorkspaceContext.provide({
|
|
||||||
workspaceID: WorkspaceID.make(rawWorkspaceID),
|
|
||||||
async fn() {
|
|
||||||
return Instance.provide({
|
|
||||||
directory,
|
|
||||||
init: InstanceBootstrap,
|
|
||||||
async fn() {
|
|
||||||
return next()
|
|
||||||
},
|
|
||||||
})
|
|
||||||
},
|
|
||||||
})
|
|
||||||
})
|
|
||||||
.route("/session", session)
|
|
||||||
.route("/", WorkspaceServerRoutes())
|
|
||||||
}
|
|
||||||
|
|
||||||
export function Listen(opts: { hostname: string; port: number }) {
|
|
||||||
return Bun.serve({
|
|
||||||
hostname: opts.hostname,
|
|
||||||
port: opts.port,
|
|
||||||
fetch: App().fetch,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -14,7 +14,6 @@ import { Installation } from "./installation"
|
||||||
import { NamedError } from "@opencode-ai/util/error"
|
import { NamedError } from "@opencode-ai/util/error"
|
||||||
import { FormatError } from "./cli/error"
|
import { FormatError } from "./cli/error"
|
||||||
import { ServeCommand } from "./cli/cmd/serve"
|
import { ServeCommand } from "./cli/cmd/serve"
|
||||||
import { WorkspaceServeCommand } from "./cli/cmd/workspace-serve"
|
|
||||||
import { Filesystem } from "./util/filesystem"
|
import { Filesystem } from "./util/filesystem"
|
||||||
import { DebugCommand } from "./cli/cmd/debug"
|
import { DebugCommand } from "./cli/cmd/debug"
|
||||||
import { StatsCommand } from "./cli/cmd/stats"
|
import { StatsCommand } from "./cli/cmd/stats"
|
||||||
|
|
@ -47,7 +46,7 @@ process.on("uncaughtException", (e) => {
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
let cli = yargs(hideBin(process.argv))
|
const cli = yargs(hideBin(process.argv))
|
||||||
.parserConfiguration({ "populate--": true })
|
.parserConfiguration({ "populate--": true })
|
||||||
.scriptName("opencode")
|
.scriptName("opencode")
|
||||||
.wrap(100)
|
.wrap(100)
|
||||||
|
|
@ -145,12 +144,6 @@ let cli = yargs(hideBin(process.argv))
|
||||||
.command(PrCommand)
|
.command(PrCommand)
|
||||||
.command(SessionCommand)
|
.command(SessionCommand)
|
||||||
.command(DbCommand)
|
.command(DbCommand)
|
||||||
|
|
||||||
if (Installation.isLocal()) {
|
|
||||||
cli = cli.command(WorkspaceServeCommand)
|
|
||||||
}
|
|
||||||
|
|
||||||
cli = cli
|
|
||||||
.fail((msg, err) => {
|
.fail((msg, err) => {
|
||||||
if (
|
if (
|
||||||
msg?.startsWith("Unknown argument") ||
|
msg?.startsWith("Unknown argument") ||
|
||||||
|
|
|
||||||
|
|
@ -1,70 +0,0 @@
|
||||||
import { afterEach, describe, expect, test } from "bun:test"
|
|
||||||
import { Log } from "../../src/util/log"
|
|
||||||
import { WorkspaceServer } from "../../src/control-plane/workspace-server/server"
|
|
||||||
import { parseSSE } from "../../src/control-plane/sse"
|
|
||||||
import { GlobalBus } from "../../src/bus/global"
|
|
||||||
import { resetDatabase } from "../fixture/db"
|
|
||||||
import { tmpdir } from "../fixture/fixture"
|
|
||||||
|
|
||||||
afterEach(async () => {
|
|
||||||
await resetDatabase()
|
|
||||||
})
|
|
||||||
|
|
||||||
Log.init({ print: false })
|
|
||||||
|
|
||||||
describe("control-plane/workspace-server SSE", () => {
|
|
||||||
test("streams GlobalBus events and parseSSE reads them", async () => {
|
|
||||||
await using tmp = await tmpdir({ git: true })
|
|
||||||
const app = WorkspaceServer.App()
|
|
||||||
const stop = new AbortController()
|
|
||||||
const seen: unknown[] = []
|
|
||||||
try {
|
|
||||||
const response = await app.request("/event", {
|
|
||||||
signal: stop.signal,
|
|
||||||
headers: {
|
|
||||||
"x-opencode-workspace": "wrk_test_workspace",
|
|
||||||
"x-opencode-directory": tmp.path,
|
|
||||||
},
|
|
||||||
})
|
|
||||||
|
|
||||||
expect(response.status).toBe(200)
|
|
||||||
expect(response.body).toBeDefined()
|
|
||||||
|
|
||||||
const done = new Promise<void>((resolve, reject) => {
|
|
||||||
const timeout = setTimeout(() => {
|
|
||||||
reject(new Error("timed out waiting for workspace.test event"))
|
|
||||||
}, 3000)
|
|
||||||
|
|
||||||
void parseSSE(response.body!, stop.signal, (event) => {
|
|
||||||
seen.push(event)
|
|
||||||
const next = event as { type?: string }
|
|
||||||
if (next.type === "server.connected") {
|
|
||||||
GlobalBus.emit("event", {
|
|
||||||
payload: {
|
|
||||||
type: "workspace.test",
|
|
||||||
properties: { ok: true },
|
|
||||||
},
|
|
||||||
})
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if (next.type !== "workspace.test") return
|
|
||||||
clearTimeout(timeout)
|
|
||||||
resolve()
|
|
||||||
}).catch((error) => {
|
|
||||||
clearTimeout(timeout)
|
|
||||||
reject(error)
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
await done
|
|
||||||
|
|
||||||
expect(seen.some((event) => (event as { type?: string }).type === "server.connected")).toBe(true)
|
|
||||||
expect(seen).toContainEqual({
|
|
||||||
type: "workspace.test",
|
|
||||||
properties: { ok: true },
|
|
||||||
})
|
|
||||||
} finally {
|
|
||||||
stop.abort()
|
|
||||||
}
|
|
||||||
})
|
|
||||||
})
|
|
||||||
Loading…
Reference in New Issue