diff --git a/packages/opencode/src/cli/cmd/workspace-serve.ts b/packages/opencode/src/cli/cmd/workspace-serve.ts new file mode 100644 index 000000000..9b47defd3 --- /dev/null +++ b/packages/opencode/src/cli/cmd/workspace-serve.ts @@ -0,0 +1,59 @@ +import { cmd } from "./cmd" +import { withNetworkOptions, resolveNetworkOptions } from "../network" +import { Installation } from "../../installation" + +export const WorkspaceServeCommand = cmd({ + command: "workspace-serve", + builder: (yargs) => withNetworkOptions(yargs), + describe: "starts a remote workspace websocket server", + handler: async (args) => { + const opts = await resolveNetworkOptions(args) + const server = Bun.serve<{ id: string }>({ + hostname: opts.hostname, + port: opts.port, + fetch(req, server) { + const url = new URL(req.url) + if (url.pathname === "/ws") { + const id = Bun.randomUUIDv7() + if (server.upgrade(req, { data: { id } })) return + return new Response("Upgrade failed", { status: 400 }) + } + + if (url.pathname === "/health") { + return new Response("ok", { + status: 200, + headers: { + "content-type": "text/plain; charset=utf-8", + }, + }) + } + + return new Response( + JSON.stringify({ + service: "workspace-server", + ws: `ws://${server.hostname}:${server.port}/ws`, + }), + { + status: 200, + headers: { + "content-type": "application/json; charset=utf-8", + }, + }, + ) + }, + websocket: { + open(ws) { + ws.send(JSON.stringify({ type: "ready", id: ws.data.id })) + }, + message(ws, msg) { + const text = typeof msg === "string" ? msg : msg.toString() + ws.send(JSON.stringify({ type: "message", id: ws.data.id, text })) + }, + close() {}, + }, + }) + + console.log(`workspace websocket server listening on ws://${server.hostname}:${server.port}/ws`) + await new Promise(() => {}) + }, +}) diff --git a/packages/opencode/src/index.ts b/packages/opencode/src/index.ts index 655156588..9af79278c 100644 --- a/packages/opencode/src/index.ts +++ b/packages/opencode/src/index.ts @@ -13,6 +13,7 @@ import { Installation } from "./installation" import { NamedError } from "@opencode-ai/util/error" import { FormatError } from "./cli/error" import { ServeCommand } from "./cli/cmd/serve" +import { WorkspaceServeCommand } from "./cli/cmd/workspace-serve" import { Filesystem } from "./util/filesystem" import { DebugCommand } from "./cli/cmd/debug" import { StatsCommand } from "./cli/cmd/stats" @@ -45,7 +46,7 @@ process.on("uncaughtException", (e) => { }) }) -const cli = yargs(hideBin(process.argv)) +let cli = yargs(hideBin(process.argv)) .parserConfiguration({ "populate--": true }) .scriptName("opencode") .wrap(100) @@ -141,6 +142,12 @@ const cli = yargs(hideBin(process.argv)) .command(PrCommand) .command(SessionCommand) .command(DbCommand) + +if (Installation.isLocal()) { + cli = cli.command(WorkspaceServeCommand) +} + +cli = cli .fail((msg, err) => { if ( msg?.startsWith("Unknown argument") ||