refactor(server): replace Bun serve with Hono node adapters
This commit is contained in:
parent
5dc8b4ef29
commit
406d216cd2
8 changed files with 549 additions and 503 deletions
|
|
@ -88,6 +88,8 @@
|
||||||
"@gitlab/gitlab-ai-provider": "3.6.0",
|
"@gitlab/gitlab-ai-provider": "3.6.0",
|
||||||
"@gitlab/opencode-gitlab-auth": "1.3.3",
|
"@gitlab/opencode-gitlab-auth": "1.3.3",
|
||||||
"@hono/standard-validator": "0.1.5",
|
"@hono/standard-validator": "0.1.5",
|
||||||
|
"@hono/node-server": "1.19.11",
|
||||||
|
"@hono/node-ws": "1.3.0",
|
||||||
"@hono/zod-validator": "catalog:",
|
"@hono/zod-validator": "catalog:",
|
||||||
"@modelcontextprotocol/sdk": "1.25.2",
|
"@modelcontextprotocol/sdk": "1.25.2",
|
||||||
"@npmcli/arborist": "9.4.0",
|
"@npmcli/arborist": "9.4.0",
|
||||||
|
|
|
||||||
|
|
@ -23,7 +23,7 @@ export const AcpCommand = cmd({
|
||||||
process.env.OPENCODE_CLIENT = "acp"
|
process.env.OPENCODE_CLIENT = "acp"
|
||||||
await bootstrap(process.cwd(), async () => {
|
await bootstrap(process.cwd(), async () => {
|
||||||
const opts = await resolveNetworkOptions(args)
|
const opts = await resolveNetworkOptions(args)
|
||||||
const server = Server.listen(opts)
|
const server = await Server.listen(opts)
|
||||||
|
|
||||||
const sdk = createOpencodeClient({
|
const sdk = createOpencodeClient({
|
||||||
baseUrl: `http://${server.hostname}:${server.port}`,
|
baseUrl: `http://${server.hostname}:${server.port}`,
|
||||||
|
|
|
||||||
|
|
@ -15,7 +15,7 @@ export const ServeCommand = cmd({
|
||||||
console.log("Warning: OPENCODE_SERVER_PASSWORD is not set; server is unsecured.")
|
console.log("Warning: OPENCODE_SERVER_PASSWORD is not set; server is unsecured.")
|
||||||
}
|
}
|
||||||
const opts = await resolveNetworkOptions(args)
|
const opts = await resolveNetworkOptions(args)
|
||||||
const server = Server.listen(opts)
|
const server = await Server.listen(opts)
|
||||||
console.log(`opencode server listening on http://${server.hostname}:${server.port}`)
|
console.log(`opencode server listening on http://${server.hostname}:${server.port}`)
|
||||||
|
|
||||||
await new Promise(() => {})
|
await new Promise(() => {})
|
||||||
|
|
|
||||||
|
|
@ -8,7 +8,6 @@ import { upgrade } from "@/cli/upgrade"
|
||||||
import { Config } from "@/config/config"
|
import { Config } from "@/config/config"
|
||||||
import { GlobalBus } from "@/bus/global"
|
import { GlobalBus } from "@/bus/global"
|
||||||
import { createOpencodeClient, type Event } from "@opencode-ai/sdk/v2"
|
import { createOpencodeClient, type Event } from "@opencode-ai/sdk/v2"
|
||||||
import type { BunWebSocketData } from "hono/bun"
|
|
||||||
import { Flag } from "@/flag/flag"
|
import { Flag } from "@/flag/flag"
|
||||||
import { setTimeout as sleep } from "node:timers/promises"
|
import { setTimeout as sleep } from "node:timers/promises"
|
||||||
|
|
||||||
|
|
@ -38,7 +37,7 @@ GlobalBus.on("event", (event) => {
|
||||||
Rpc.emit("global.event", event)
|
Rpc.emit("global.event", event)
|
||||||
})
|
})
|
||||||
|
|
||||||
let server: Bun.Server<BunWebSocketData> | undefined
|
let server: Awaited<ReturnType<typeof Server.listen>> | undefined
|
||||||
|
|
||||||
const eventStream = {
|
const eventStream = {
|
||||||
abort: undefined as AbortController | undefined,
|
abort: undefined as AbortController | undefined,
|
||||||
|
|
@ -120,7 +119,7 @@ export const rpc = {
|
||||||
},
|
},
|
||||||
async server(input: { port: number; hostname: string; mdns?: boolean; cors?: string[] }) {
|
async server(input: { port: number; hostname: string; mdns?: boolean; cors?: string[] }) {
|
||||||
if (server) await server.stop(true)
|
if (server) await server.stop(true)
|
||||||
server = Server.listen(input)
|
server = await Server.listen(input)
|
||||||
return { url: server.url.toString() }
|
return { url: server.url.toString() }
|
||||||
},
|
},
|
||||||
async checkUpgrade(input: { directory: string }) {
|
async checkUpgrade(input: { directory: string }) {
|
||||||
|
|
@ -143,7 +142,7 @@ export const rpc = {
|
||||||
Log.Default.info("worker shutting down")
|
Log.Default.info("worker shutting down")
|
||||||
if (eventStream.abort) eventStream.abort.abort()
|
if (eventStream.abort) eventStream.abort.abort()
|
||||||
await Instance.disposeAll()
|
await Instance.disposeAll()
|
||||||
if (server) server.stop(true)
|
if (server) await server.stop(true)
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -37,7 +37,7 @@ export const WebCommand = cmd({
|
||||||
UI.println(UI.Style.TEXT_WARNING_BOLD + "! " + "OPENCODE_SERVER_PASSWORD is not set; server is unsecured.")
|
UI.println(UI.Style.TEXT_WARNING_BOLD + "! " + "OPENCODE_SERVER_PASSWORD is not set; server is unsecured.")
|
||||||
}
|
}
|
||||||
const opts = await resolveNetworkOptions(args)
|
const opts = await resolveNetworkOptions(args)
|
||||||
const server = Server.listen(opts)
|
const server = await Server.listen(opts)
|
||||||
UI.empty()
|
UI.empty()
|
||||||
UI.println(UI.logo(" "))
|
UI.println(UI.logo(" "))
|
||||||
UI.empty()
|
UI.empty()
|
||||||
|
|
|
||||||
|
|
@ -23,6 +23,8 @@ export namespace Pty {
|
||||||
close: (code?: number, reason?: string) => void
|
close: (code?: number, reason?: string) => void
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const key = (ws: Socket) => (ws.data && typeof ws.data === "object" ? ws.data : ws)
|
||||||
|
|
||||||
// WebSocket control frame: 0x00 + UTF-8 JSON.
|
// WebSocket control frame: 0x00 + UTF-8 JSON.
|
||||||
const meta = (cursor: number) => {
|
const meta = (cursor: number) => {
|
||||||
const json = JSON.stringify({ cursor })
|
const json = JSON.stringify({ cursor })
|
||||||
|
|
@ -97,9 +99,9 @@ export namespace Pty {
|
||||||
try {
|
try {
|
||||||
session.process.kill()
|
session.process.kill()
|
||||||
} catch {}
|
} catch {}
|
||||||
for (const [key, ws] of session.subscribers.entries()) {
|
for (const [id, ws] of session.subscribers.entries()) {
|
||||||
try {
|
try {
|
||||||
if (ws.data === key) ws.close()
|
if (key(ws) === id) ws.close()
|
||||||
} catch {
|
} catch {
|
||||||
// ignore
|
// ignore
|
||||||
}
|
}
|
||||||
|
|
@ -170,21 +172,21 @@ export namespace Pty {
|
||||||
ptyProcess.onData((chunk) => {
|
ptyProcess.onData((chunk) => {
|
||||||
session.cursor += chunk.length
|
session.cursor += chunk.length
|
||||||
|
|
||||||
for (const [key, ws] of session.subscribers.entries()) {
|
for (const [id, ws] of session.subscribers.entries()) {
|
||||||
if (ws.readyState !== 1) {
|
if (ws.readyState !== 1) {
|
||||||
session.subscribers.delete(key)
|
session.subscribers.delete(id)
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
if (ws.data !== key) {
|
if (key(ws) !== id) {
|
||||||
session.subscribers.delete(key)
|
session.subscribers.delete(id)
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
ws.send(chunk)
|
ws.send(chunk)
|
||||||
} catch {
|
} catch {
|
||||||
session.subscribers.delete(key)
|
session.subscribers.delete(id)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -226,9 +228,9 @@ export namespace Pty {
|
||||||
try {
|
try {
|
||||||
session.process.kill()
|
session.process.kill()
|
||||||
} catch {}
|
} catch {}
|
||||||
for (const [key, ws] of session.subscribers.entries()) {
|
for (const [id, ws] of session.subscribers.entries()) {
|
||||||
try {
|
try {
|
||||||
if (ws.data === key) ws.close()
|
if (key(ws) === id) ws.close()
|
||||||
} catch {
|
} catch {
|
||||||
// ignore
|
// ignore
|
||||||
}
|
}
|
||||||
|
|
@ -259,16 +261,13 @@ export namespace Pty {
|
||||||
}
|
}
|
||||||
log.info("client connected to session", { id })
|
log.info("client connected to session", { id })
|
||||||
|
|
||||||
// Use ws.data as the unique key for this connection lifecycle.
|
const sub = key(ws)
|
||||||
// If ws.data is undefined, fallback to ws object.
|
|
||||||
const connectionKey = ws.data && typeof ws.data === "object" ? ws.data : ws
|
|
||||||
|
|
||||||
// Optionally cleanup if the key somehow exists
|
session.subscribers.delete(sub)
|
||||||
session.subscribers.delete(connectionKey)
|
session.subscribers.set(sub, ws)
|
||||||
session.subscribers.set(connectionKey, ws)
|
|
||||||
|
|
||||||
const cleanup = () => {
|
const cleanup = () => {
|
||||||
session.subscribers.delete(connectionKey)
|
session.subscribers.delete(sub)
|
||||||
}
|
}
|
||||||
|
|
||||||
const start = session.bufferCursor
|
const start = session.bufferCursor
|
||||||
|
|
|
||||||
|
|
@ -1,14 +1,13 @@
|
||||||
import { Hono } from "hono"
|
import { Hono } from "hono"
|
||||||
import { describeRoute, validator, resolver } from "hono-openapi"
|
import { describeRoute, validator, resolver } from "hono-openapi"
|
||||||
import { upgradeWebSocket } from "hono/bun"
|
import type { UpgradeWebSocket } from "hono/ws"
|
||||||
import z from "zod"
|
import z from "zod"
|
||||||
import { Pty } from "@/pty"
|
import { Pty } from "@/pty"
|
||||||
import { NotFoundError } from "../../storage/db"
|
import { NotFoundError } from "../../storage/db"
|
||||||
import { errors } from "../error"
|
import { errors } from "../error"
|
||||||
import { lazy } from "../../util/lazy"
|
|
||||||
|
|
||||||
export const PtyRoutes = lazy(() =>
|
export function PtyRoutes(upgradeWebSocket: UpgradeWebSocket) {
|
||||||
new Hono()
|
return new Hono()
|
||||||
.get(
|
.get(
|
||||||
"/",
|
"/",
|
||||||
describeRoute({
|
describeRoute({
|
||||||
|
|
@ -196,5 +195,5 @@ export const PtyRoutes = lazy(() =>
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
}),
|
}),
|
||||||
),
|
)
|
||||||
)
|
}
|
||||||
|
|
|
||||||
File diff suppressed because it is too large
Load diff
Loading…
Add table
Add a link
Reference in a new issue