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
6 changes: 5 additions & 1 deletion packages/opencode/src/cli/cmd/serve.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,11 @@ 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}`)
if (opts.unix) {
console.log(`opencode server listening on unix socket: ${opts.unix}`)
} else {
console.log(`opencode server listening on http://${server.hostname}:${server.port}`)
}
await new Promise(() => {})
await server.stop()
},
Expand Down
12 changes: 12 additions & 0 deletions packages/opencode/src/cli/cmd/tui/app.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -103,6 +103,7 @@ export function tui(input: {
url: string
args: Args
directory?: string
unix?: string
fetch?: typeof fetch
events?: EventSource
onExit?: () => Promise<void>
Expand All @@ -129,6 +130,7 @@ export function tui(input: {
<SDKProvider
url={input.url}
directory={input.directory}
unix={input.unix}
fetch={input.fetch}
events={input.events}
>
Expand Down Expand Up @@ -491,7 +493,17 @@ function App() {
{
title: "Open WebUI",
value: "webui.open",
hidden: sdk.url.startsWith("unix://"),
onSelect: () => {
if (sdk.url.startsWith("unix://")) {
toast.show({
variant: "warning",
message: "Cannot open WebUI for Unix socket connections",
duration: 3000,
})
dialog.clear()
return
}
open(sdk.url).catch(() => {})
dialog.clear()
},
Expand Down
15 changes: 13 additions & 2 deletions packages/opencode/src/cli/cmd/tui/attach.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { cmd } from "../cmd"
import { tui } from "./app"
import { parseAttachUrl } from "./parse-url"

export const AttachCommand = cmd({
command: "attach <url>",
Expand All @@ -8,7 +9,7 @@ export const AttachCommand = cmd({
yargs
.positional("url", {
type: "string",
describe: "http://localhost:4096",
describe: "Server URL: http://localhost:4096 or unix:///path/to/socket",
demandOption: true,
})
.option("dir", {
Expand All @@ -22,10 +23,20 @@ export const AttachCommand = cmd({
}),
handler: async (args) => {
if (args.dir) process.chdir(args.dir)

let parsed
try {
parsed = parseAttachUrl(args.url)
} catch (error) {
console.error(`Error: ${error instanceof Error ? error.message : error}`)
process.exit(1)
}

await tui({
url: args.url,
url: parsed.baseUrl,
args: { sessionID: args.session },
directory: args.dir ? process.cwd() : undefined,
unix: parsed.unix,
})
},
})
7 changes: 5 additions & 2 deletions packages/opencode/src/cli/cmd/tui/context/sdk.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,12 +9,13 @@ export type EventSource = {

export const { use: useSDK, provider: SDKProvider } = createSimpleContext({
name: "SDK",
init: (props: { url: string; directory?: string; fetch?: typeof fetch; events?: EventSource }) => {
init: (props: { url: string; directory?: string; unix?: string; fetch?: typeof fetch; events?: EventSource }) => {
const abort = new AbortController()
const sdk = createOpencodeClient({
baseUrl: props.url,
signal: abort.signal,
directory: props.directory,
unix: props.unix,
fetch: props.fetch,
})

Expand Down Expand Up @@ -89,6 +90,8 @@ export const { use: useSDK, provider: SDKProvider } = createSimpleContext({
if (timer) clearTimeout(timer)
})

return { client: sdk, event: emitter, url: props.url }
const displayUrl = props.unix ? `unix://${props.unix}` : props.url

return { client: sdk, event: emitter, url: displayUrl }
},
})
28 changes: 28 additions & 0 deletions packages/opencode/src/cli/cmd/tui/parse-url.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
export interface ParsedAttachUrl {
baseUrl: string
unix?: string
}

export function parseAttachUrl(url: string): ParsedAttachUrl {
if (!url.startsWith("http://") && !url.startsWith("https://") && !url.startsWith("unix://")) {
throw new Error("URL must start with http://, https://, or unix://")
}

if (url.startsWith("unix://")) {
const socketPath = url.substring(7) // Remove "unix://" prefix
if (!socketPath || socketPath.trim().length === 0) {
throw new Error("Unix socket path cannot be empty")
}

if (!socketPath.startsWith("/")) {
throw new Error("Unix socket path must be absolute (start with /)")
}

return {
baseUrl: "http://localhost:4096", // Placeholder for SDK URL building
unix: socketPath,
}
}

return { baseUrl: url }
}
8 changes: 7 additions & 1 deletion packages/opencode/src/cli/network.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,10 @@ const options = {
describe: "additional domains to allow for CORS",
default: [] as string[],
},
unix: {
type: "string" as const,
describe: "unix socket path to bind to (overrides port/hostname)",
},
}

export type NetworkOptions = InferredOptionTypes<typeof options>
Expand All @@ -37,7 +41,9 @@ export async function resolveNetworkOptions(args: NetworkOptions) {
const hostnameExplicitlySet = process.argv.includes("--hostname")
const mdnsExplicitlySet = process.argv.includes("--mdns")
const corsExplicitlySet = process.argv.includes("--cors")
const unixExplicitlySet = process.argv.includes("--unix")

const unix = unixExplicitlySet ? args.unix : (config?.server?.unix ?? args.unix)
const mdns = mdnsExplicitlySet ? args.mdns : (config?.server?.mdns ?? args.mdns)
const port = portExplicitlySet ? args.port : (config?.server?.port ?? args.port)
const hostname = hostnameExplicitlySet
Expand All @@ -49,5 +55,5 @@ export async function resolveNetworkOptions(args: NetworkOptions) {
const argsCors = Array.isArray(args.cors) ? args.cors : args.cors ? [args.cors] : []
const cors = [...configCors, ...argsCors]

return { hostname, port, mdns, cors }
return { hostname, port, mdns, cors, unix }
}
1 change: 1 addition & 0 deletions packages/opencode/src/config/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -812,6 +812,7 @@ export namespace Config {
hostname: z.string().optional().describe("Hostname to listen on"),
mdns: z.boolean().optional().describe("Enable mDNS service discovery"),
cors: z.array(z.string()).optional().describe("Additional domains to allow for CORS"),
unix: z.string().optional().describe("Unix socket path to bind to (overrides port/hostname)"),
})
.strict()
.meta({
Expand Down
36 changes: 25 additions & 11 deletions packages/opencode/src/server/server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -529,24 +529,38 @@ export namespace Server {
return result
}

export function listen(opts: { port: number; hostname: string; mdns?: boolean; cors?: string[] }) {
export function listen(opts: { port?: number; hostname?: string; mdns?: boolean; cors?: string[]; unix?: string }) {
_corsWhitelist = opts.cors ?? []

const args = {
hostname: opts.hostname,
const baseArgs: any = {
idleTimeout: 0,
fetch: App().fetch,
websocket: websocket,
} as const
const tryServe = (port: number) => {
try {
return Bun.serve({ ...args, port })
} catch {
return undefined
}

let server: ReturnType<typeof Bun.serve>

if (opts.unix) {
// Unix socket mode
server = Bun.serve({ ...baseArgs, unix: opts.unix })
} else {
// TCP socket mode
const args = {
...baseArgs,
hostname: opts.hostname ?? "127.0.0.1",
} as const
const tryServe = (port: number) => {
try {
return Bun.serve({ ...args, port })
} catch {
return undefined
}
}
const port = opts.port ?? 0
const result = port === 0 ? (tryServe(4096) ?? tryServe(0)) : tryServe(port)
if (!result) throw new Error(`Failed to start server on port ${port}`)
server = result
}
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 = server.url

Expand Down
49 changes: 45 additions & 4 deletions packages/sdk/js/src/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,53 @@ import { type Config } from "./gen/client/types.gen.js"
import { OpencodeClient } from "./gen/sdk.gen.js"
export { type Config as OpencodeClientConfig, OpencodeClient }

export function createOpencodeClient(config?: Config & { directory?: string }) {
export function createOpencodeClient(config?: Config & { directory?: string; unix?: string }) {
if (!config?.fetch) {
// Handle Unix domain socket if specified
if (config?.unix) {
const unixPath = config.unix
const customFetch: any = (req: any) => {
return fetch(
req.url,
{
method: req.method,
headers: req.headers,
body: req.body,
unix: unixPath,
timeout: false,
} as any,
)
}
config = {
...config,
fetch: customFetch,
}
} else {
const customFetch: any = (req: any) => {
// @ts-ignore
req.timeout = false
return fetch(req)
}
config = {
...config,
fetch: customFetch,
}
}
} else if (config?.unix) {
// Handle Unix domain socket when fetch is already provided
const unixPath = config.unix
const originalFetch: any = config.fetch
const customFetch: any = (req: any) => {
// @ts-ignore
req.timeout = false
return fetch(req)
return originalFetch(
req.url,
{
method: req.method,
headers: req.headers,
body: req.body,
unix: unixPath,
timeout: false,
} as any,
)
}
config = {
...config,
Expand Down
47 changes: 43 additions & 4 deletions packages/sdk/js/src/v2/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,51 @@ import { type Config } from "./gen/client/types.gen.js"
import { OpencodeClient } from "./gen/sdk.gen.js"
export { type Config as OpencodeClientConfig, OpencodeClient }

export function createOpencodeClient(config?: Config & { directory?: string }) {
export function createOpencodeClient(config?: Config & { directory?: string; unix?: string }) {
if (!config?.fetch) {
if (config?.unix) {
const unixPath = config.unix
const customFetch: any = (req: any) => {
return fetch(
req.url,
{
method: req.method,
headers: req.headers,
body: req.body,
unix: unixPath,
timeout: false,
} as any,
)
}
config = {
...config,
fetch: customFetch,
}
} else {
const customFetch: any = (req: any) => {
// @ts-ignore
req.timeout = false
return fetch(req)
}
config = {
...config,
fetch: customFetch,
}
}
} else if (config?.unix) {
const unixPath = config.unix
const originalFetch: any = config.fetch
const customFetch: any = (req: any) => {
// @ts-ignore
req.timeout = false
return fetch(req)
return originalFetch(
req.url,
{
method: req.method,
headers: req.headers,
body: req.body,
unix: unixPath,
timeout: false,
} as any,
)
}
config = {
...config,
Expand Down