Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion packages/opencode/src/cli/cmd/acp.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ export const AcpCommand = cmd({
const server = Server.listen(opts)

const sdk = createOpencodeClient({
baseUrl: `http://${server.hostname}:${server.port}`,
baseUrl: server.url.origin,
})

const input = new WritableStream<Uint8Array>({
Expand Down
2 changes: 1 addition & 1 deletion packages/opencode/src/cli/cmd/serve.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ export const ServeCommand = cmd({
}
const opts = await resolveNetworkOptions(args)
const server = Server.listen(opts)
console.log(`opencode server listening on http://${server.hostname}:${server.port}`)
console.log(`opencode server listening on ${server.url.origin}`)

await new Promise(() => {})
await server.stop()
Expand Down
2 changes: 1 addition & 1 deletion packages/opencode/src/cli/cmd/workspace-serve.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ export const WorkspaceServeCommand = cmd({
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`)
console.log(`workspace event server listening on ${new URL("/event", server.url).toString()}`)
await new Promise(() => {})
await server.stop()
},
Expand Down
23 changes: 22 additions & 1 deletion packages/opencode/src/server/server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -591,14 +591,33 @@ export namespace Server {
/** @deprecated do not use this dumb shit */
export let url: URL

/**
* Normalizes a hostname for safe interpolation into an HTTP URL authority.
*
* - Trims surrounding whitespace.
* - Removes IPv6 brackets if already present so we can normalize once.
* - Escapes stray `%` as `%25` to avoid invalid percent-encoding in URL parsing.
* - Re-wraps values containing `:` in `[]` so IPv6 literals are valid in `host:port`.
*
* @param hostname Hostname or IP literal (IPv4/IPv6), optionally already bracketed.
* @returns A normalized host string suitable for `http://${host}:${port}` construction.
*/
function host(hostname: string) {
const raw = hostname.trim()
const inner = raw.startsWith("[") && raw.endsWith("]") ? raw.slice(1, -1) : raw
const safe = inner.replace(/%(?!25)/g, "%25")
if (!safe.includes(":")) return safe
return `[${safe}]`
}

export function listen(opts: {
port: number
hostname: string
mdns?: boolean
mdnsDomain?: string
cors?: string[]
}) {
url = new URL(`http://${opts.hostname}:${opts.port}`)
url = new URL(`http://${host(opts.hostname)}:${opts.port}`)
const app = createApp(opts)
const args = {
hostname: opts.hostname,
Expand All @@ -616,6 +635,8 @@ export namespace Server {
const server = opts.port === 0 ? (tryServe(4096) ?? tryServe(0)) : tryServe(opts.port)
if (!server) throw new Error(`Failed to start server on port ${opts.port}`)

url.port = String(server.port)

const shouldPublishMDNS =
opts.mdns &&
server.port &&
Expand Down
29 changes: 29 additions & 0 deletions packages/opencode/test/server/url.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import { describe, expect, test } from "bun:test"
import { Server } from "../../src/server/server"
import { Log } from "../../src/util/log"

Log.init({ print: false })

function want(hostname: string, port: number) {
const raw = hostname.trim()
const host = raw.startsWith("[") && raw.endsWith("]") ? raw.slice(1, -1) : raw
if (!host.includes(":")) return `http://${host}:${port}`
return `http://[${host}]:${port}`
}

describe("Server.listen URL", () => {
test("accepts ipv4, ipv6, and domains", async () => {
const cases = ["127.0.0.1", "0.0.0.0", "localhost", "::", "::1", "[::]", "[::1]"]

for (const hostname of cases) {
const server = Server.listen({ hostname, port: 0 })
try {
expect(server.port).toBeGreaterThan(0)
expect(Server.url.origin).toBe(new URL(want(hostname, server.port!)).origin)
expect(new URL(Server.url.toString()).origin).toBe(Server.url.origin)
} finally {
await server.stop(true)
}
}
})
})
Loading