mirror of
https://github.com/anomalyco/opencode.git
synced 2026-08-08 10:09:52 -04:00
Compare commits
13 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 08e4d4fe0c | |||
| 342d02aefe | |||
| 12c19d627d | |||
| 41f0f7d407 | |||
| 91d148d95f | |||
| 3caba56bd9 | |||
| cb45de6fd2 | |||
| d1f44d4cd2 | |||
| 25400f48df | |||
| a2008478f9 | |||
| 971fe35e81 | |||
| e3cbfa1c05 | |||
| 2719140b56 |
@@ -6,15 +6,26 @@ import { Flag } from "@opencode-ai/core/flag/flag"
|
|||||||
|
|
||||||
export const ServeCommand = effectCmd({
|
export const ServeCommand = effectCmd({
|
||||||
command: "serve",
|
command: "serve",
|
||||||
builder: (yargs) => withNetworkOptions(yargs),
|
builder: (yargs) =>
|
||||||
|
withNetworkOptions(yargs)
|
||||||
|
.option("socket", {
|
||||||
|
type: "string",
|
||||||
|
describe: "Unix socket path or Windows named pipe name/path to listen on",
|
||||||
|
}),
|
||||||
describe: "starts a headless opencode server",
|
describe: "starts a headless opencode server",
|
||||||
// Server loads instances per-request via x-opencode-directory header — no
|
|
||||||
// need for an ambient project InstanceContext at startup.
|
|
||||||
instance: false,
|
instance: false,
|
||||||
handler: Effect.fn("Cli.serve")(function* (args) {
|
handler: Effect.fn("Cli.serve")(function* (args) {
|
||||||
if (!Flag.OPENCODE_SERVER_PASSWORD) {
|
if (!Flag.OPENCODE_SERVER_PASSWORD) {
|
||||||
console.log("Warning: OPENCODE_SERVER_PASSWORD is not set; server is unsecured.")
|
console.log("Warning: OPENCODE_SERVER_PASSWORD is not set; server is unsecured.")
|
||||||
}
|
}
|
||||||
|
if (args.socket) {
|
||||||
|
const server = yield* Effect.promise(() =>
|
||||||
|
Server.listen({ type: "socket", socket: resolveSocketPath(args.socket) }),
|
||||||
|
)
|
||||||
|
console.log(`opencode server listening on socket ${server.socket}`)
|
||||||
|
yield* Effect.never
|
||||||
|
}
|
||||||
|
|
||||||
const opts = yield* resolveNetworkOptions(args)
|
const opts = yield* resolveNetworkOptions(args)
|
||||||
const server = yield* Effect.promise(() => Server.listen(opts))
|
const server = yield* Effect.promise(() => 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}`)
|
||||||
@@ -22,3 +33,16 @@ export const ServeCommand = effectCmd({
|
|||||||
yield* Effect.never
|
yield* Effect.never
|
||||||
}),
|
}),
|
||||||
})
|
})
|
||||||
|
|
||||||
|
function resolveSocketPath(input: string) {
|
||||||
|
if (process.platform !== "win32") return input
|
||||||
|
const lower = input.toLowerCase()
|
||||||
|
if (lower.startsWith("\\\\.\\pipe\\") || lower.startsWith("\\\\?\\pipe\\")) return input
|
||||||
|
const name = input
|
||||||
|
.replace(/^[a-zA-Z]:/, (drive) => drive.slice(0, 1))
|
||||||
|
.replace(/[\\/:]+/g, "-")
|
||||||
|
.replace(/^-+|-+$/g, "")
|
||||||
|
return `\\\\.\\pipe\\${name || "opencode"}`
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -72,7 +72,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 = await Server.listen(input)
|
server = await Server.listen({ type: "tcp", ...input, mdns: input.mdns ? true : undefined })
|
||||||
return { url: server.url.toString() }
|
return { url: server.url.toString() }
|
||||||
},
|
},
|
||||||
async checkUpgrade(input: { directory: string }) {
|
async checkUpgrade(input: { directory: string }) {
|
||||||
|
|||||||
@@ -67,7 +67,7 @@ export const WebCommand = effectCmd({
|
|||||||
UI.println(
|
UI.println(
|
||||||
UI.Style.TEXT_INFO_BOLD + " mDNS: ",
|
UI.Style.TEXT_INFO_BOLD + " mDNS: ",
|
||||||
UI.Style.TEXT_NORMAL,
|
UI.Style.TEXT_NORMAL,
|
||||||
`${opts.mdnsDomain}:${server.port}`,
|
`${opts.mdns === true ? "opencode.local" : opts.mdns.domain}:${server.port}`,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -47,7 +47,7 @@ export function resolveNetworkOptionsNoConfig(args: NetworkOptions, config?: Con
|
|||||||
const mdnsExplicitlySet = process.argv.includes("--mdns")
|
const mdnsExplicitlySet = process.argv.includes("--mdns")
|
||||||
const mdnsDomainExplicitlySet = process.argv.includes("--mdns-domain")
|
const mdnsDomainExplicitlySet = process.argv.includes("--mdns-domain")
|
||||||
const mdns = mdnsExplicitlySet ? args.mdns : (config?.server?.mdns ?? args.mdns)
|
const mdns = mdnsExplicitlySet ? args.mdns : (config?.server?.mdns ?? args.mdns)
|
||||||
const mdnsDomain = mdnsDomainExplicitlySet ? args["mdns-domain"] : (config?.server?.mdnsDomain ?? args["mdns-domain"])
|
const mdnsDomain = mdnsDomainExplicitlySet ? args["mdns-domain"] : config?.server?.mdnsDomain
|
||||||
const port = portExplicitlySet ? args.port : (config?.server?.port ?? args.port)
|
const port = portExplicitlySet ? args.port : (config?.server?.port ?? args.port)
|
||||||
const hostname = hostnameExplicitlySet
|
const hostname = hostnameExplicitlySet
|
||||||
? args.hostname
|
? args.hostname
|
||||||
@@ -58,5 +58,5 @@ export function resolveNetworkOptionsNoConfig(args: NetworkOptions, config?: Con
|
|||||||
const argsCors = Array.isArray(args.cors) ? args.cors : args.cors ? [args.cors] : []
|
const argsCors = Array.isArray(args.cors) ? args.cors : args.cors ? [args.cors] : []
|
||||||
const cors = [...configCors, ...argsCors]
|
const cors = [...configCors, ...argsCors]
|
||||||
|
|
||||||
return { hostname, port, mdns, mdnsDomain, cors }
|
return { type: "tcp" as const, hostname, port, mdns: mdns ? (mdnsDomain ? { domain: mdnsDomain } : true) : undefined, cors }
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -19,33 +19,54 @@ globalThis.AI_SDK_LOG_WARNINGS = false
|
|||||||
|
|
||||||
const log = Log.create({ service: "server" })
|
const log = Log.create({ service: "server" })
|
||||||
|
|
||||||
export type Listener = {
|
export type TcpListener = {
|
||||||
|
type: "tcp"
|
||||||
hostname: string
|
hostname: string
|
||||||
port: number
|
port: number
|
||||||
url: URL
|
url: URL
|
||||||
stop: (close?: boolean) => Promise<void>
|
stop: (close?: boolean) => Promise<void>
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export type SocketListener = {
|
||||||
|
type: "socket"
|
||||||
|
socket: string
|
||||||
|
url: URL
|
||||||
|
stop: (close?: boolean) => Promise<void>
|
||||||
|
}
|
||||||
|
|
||||||
|
export type Listener = TcpListener | SocketListener
|
||||||
|
|
||||||
type ServerApp = {
|
type ServerApp = {
|
||||||
fetch(request: Request): Response | Promise<Response>
|
fetch(request: Request): Response | Promise<Response>
|
||||||
request(input: string | URL | Request, init?: RequestInit): Response | Promise<Response>
|
request(input: string | URL | Request, init?: RequestInit): Response | Promise<Response>
|
||||||
}
|
}
|
||||||
|
|
||||||
type ListenOptions = CorsOptions & {
|
export type TcpListenOptions = CorsOptions & {
|
||||||
|
type: "tcp"
|
||||||
port: number
|
port: number
|
||||||
hostname: string
|
hostname: string
|
||||||
mdns?: boolean
|
mdns?: true | { domain: string }
|
||||||
mdnsDomain?: string
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export type SocketListenOptions = CorsOptions & {
|
||||||
|
type: "socket"
|
||||||
|
socket: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export type ListenOptions = TcpListenOptions | SocketListenOptions
|
||||||
type ListenerState = {
|
type ListenerState = {
|
||||||
scope: Scope.Scope
|
scope: Scope.Scope
|
||||||
server: Context.Service.Shape<typeof HttpServer.HttpServer>
|
server: Context.Service.Shape<typeof HttpServer.HttpServer>
|
||||||
http: ListenerServer
|
http: ListenerServer
|
||||||
websockets: WebSocketTracker.Interface
|
websockets: WebSocketTracker.Interface
|
||||||
}
|
}
|
||||||
type EffectListener = Omit<Listener, "stop"> & {
|
type EffectTcpListener = Omit<TcpListener, "stop"> & {
|
||||||
stop: (close?: boolean) => Effect.Effect<void>
|
stop: (close?: boolean) => Effect.Effect<void>
|
||||||
}
|
}
|
||||||
|
type EffectSocketListener = Omit<SocketListener, "stop"> & {
|
||||||
|
stop: (close?: boolean) => Effect.Effect<void>
|
||||||
|
}
|
||||||
|
type EffectListener = EffectTcpListener | EffectSocketListener
|
||||||
|
|
||||||
interface ListenerServer {
|
interface ListenerServer {
|
||||||
readonly closeAll: Effect.Effect<void>
|
readonly closeAll: Effect.Effect<void>
|
||||||
@@ -72,26 +93,50 @@ export async function openapi() {
|
|||||||
|
|
||||||
export let url: URL
|
export let url: URL
|
||||||
|
|
||||||
|
export function listen(opts: TcpListenOptions): Promise<TcpListener>
|
||||||
|
export function listen(opts: SocketListenOptions): Promise<SocketListener>
|
||||||
export async function listen(opts: ListenOptions): Promise<Listener> {
|
export async function listen(opts: ListenOptions): Promise<Listener> {
|
||||||
const listener = await Effect.runPromise(listenEffect(opts))
|
const listener = await Effect.runPromise(listenEffect(opts))
|
||||||
|
const stop = (close?: boolean) => Effect.runPromiseExit(listener.stop(close)).then(() => undefined)
|
||||||
|
if (listener.type === "socket") {
|
||||||
|
return {
|
||||||
|
type: "socket" as const,
|
||||||
|
socket: listener.socket,
|
||||||
|
url: listener.url,
|
||||||
|
stop,
|
||||||
|
}
|
||||||
|
}
|
||||||
return {
|
return {
|
||||||
|
type: "tcp" as const,
|
||||||
hostname: listener.hostname,
|
hostname: listener.hostname,
|
||||||
port: listener.port,
|
port: listener.port,
|
||||||
url: listener.url,
|
url: listener.url,
|
||||||
stop: (close?: boolean) => Effect.runPromiseExit(listener.stop(close)).then(() => undefined),
|
stop,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const listenEffect: (opts: ListenOptions) => Effect.Effect<EffectListener, unknown> = Effect.fn("Server.listen")(
|
const listenEffect: (opts: ListenOptions) => Effect.Effect<EffectListener, unknown> = Effect.fn("Server.listen")(
|
||||||
function* (opts: ListenOptions) {
|
function* (opts: ListenOptions) {
|
||||||
const state = yield* startWithPortFallback(opts)
|
const state = yield* startWithPortFallback(opts)
|
||||||
|
if (opts.type === "socket") {
|
||||||
|
return {
|
||||||
|
type: "socket" as const,
|
||||||
|
socket: opts.socket,
|
||||||
|
url: new URL(`socket:${encodeURIComponent(opts.socket)}`),
|
||||||
|
stop: yield* makeStop(state, Effect.void),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const address = yield* tcpAddress(state)
|
const address = yield* tcpAddress(state)
|
||||||
const listenerUrl = makeURL(opts.hostname, address.port)
|
const listenerUrl = new URL("http://localhost")
|
||||||
|
listenerUrl.hostname = opts.hostname
|
||||||
|
listenerUrl.port = String(address.port)
|
||||||
url = listenerUrl
|
url = listenerUrl
|
||||||
|
|
||||||
const unpublishMdns = yield* setupMdns(opts, address.port, state.scope)
|
const unpublishMdns = yield* setupMdns(opts, address.port, state.scope)
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
type: "tcp" as const,
|
||||||
hostname: opts.hostname,
|
hostname: opts.hostname,
|
||||||
port: address.port,
|
port: address.port,
|
||||||
url: listenerUrl,
|
url: listenerUrl,
|
||||||
@@ -100,14 +145,14 @@ const listenEffect: (opts: ListenOptions) => Effect.Effect<EffectListener, unkno
|
|||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
function listenerLayer(opts: ListenOptions, port: number) {
|
function listenerLayer(opts: ListenOptions) {
|
||||||
return HttpRouter.serve(HttpApiApp.createRoutes(opts), {
|
return HttpRouter.serve(HttpApiApp.createRoutes(opts), {
|
||||||
middleware: disposeMiddleware,
|
middleware: disposeMiddleware,
|
||||||
disableLogger: true,
|
disableLogger: true,
|
||||||
disableListenLog: true,
|
disableListenLog: true,
|
||||||
}).pipe(
|
}).pipe(
|
||||||
Layer.provideMerge(WebSocketTracker.layer),
|
Layer.provideMerge(WebSocketTracker.layer),
|
||||||
Layer.provideMerge(serverLayer({ port, hostname: opts.hostname })),
|
Layer.provideMerge(serverLayer(opts)),
|
||||||
// Install a fresh `ConfigProvider` per listener so `Config.string(...)`
|
// Install a fresh `ConfigProvider` per listener so `Config.string(...)`
|
||||||
// reads reflect the current `process.env`. Effect's default
|
// reads reflect the current `process.env`. Effect's default
|
||||||
// `ConfigProvider` snapshots `process.env` on first read and caches the
|
// `ConfigProvider` snapshots `process.env` on first read and caches the
|
||||||
@@ -118,15 +163,15 @@ function listenerLayer(opts: ListenOptions, port: number) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function startWithPortFallback(opts: ListenOptions) {
|
function startWithPortFallback(opts: ListenOptions) {
|
||||||
if (opts.port !== 0) return startListener(opts, opts.port)
|
if (opts.type === "socket" || opts.port !== 0) return startListener(opts)
|
||||||
// Match the legacy listener port-resolution behavior: explicit `0` prefers
|
// Match the legacy listener port-resolution behavior: explicit `0` prefers
|
||||||
// 4096 first, then any free port.
|
// 4096 first, then any free port.
|
||||||
return startListener(opts, 4096).pipe(Effect.catch(() => startListener(opts, 0)))
|
return startListener({ ...opts, port: 4096 }).pipe(Effect.catch(() => startListener(opts)))
|
||||||
}
|
}
|
||||||
|
|
||||||
function startListener(opts: ListenOptions, port: number) {
|
function startListener(opts: ListenOptions) {
|
||||||
const scope = Scope.makeUnsafe()
|
const scope = Scope.makeUnsafe()
|
||||||
return Layer.buildWithMemoMap(listenerLayer(opts, port), Layer.makeMemoMapUnsafe(), scope).pipe(
|
return Layer.buildWithMemoMap(listenerLayer(opts), Layer.makeMemoMapUnsafe(), scope).pipe(
|
||||||
Effect.provide(HttpApiApp.context),
|
Effect.provide(HttpApiApp.context),
|
||||||
Effect.onError(() => Scope.close(scope, Exit.void).pipe(Effect.ignore)),
|
Effect.onError(() => Scope.close(scope, Exit.void).pipe(Effect.ignore)),
|
||||||
Effect.map(
|
Effect.map(
|
||||||
@@ -148,20 +193,13 @@ function tcpAddress(state: ListenerState) {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
function makeURL(hostname: string, port: number) {
|
function setupMdns(opts: TcpListenOptions, port: number, scope: Scope.Scope) {
|
||||||
const result = new URL("http://localhost")
|
|
||||||
result.hostname = hostname
|
|
||||||
result.port = String(port)
|
|
||||||
return result
|
|
||||||
}
|
|
||||||
|
|
||||||
function setupMdns(opts: ListenOptions, port: number, scope: Scope.Scope) {
|
|
||||||
return Effect.gen(function* () {
|
return Effect.gen(function* () {
|
||||||
const publish =
|
const publish =
|
||||||
opts.mdns && port && opts.hostname !== "127.0.0.1" && opts.hostname !== "localhost" && opts.hostname !== "::1"
|
opts.mdns && port && opts.hostname !== "127.0.0.1" && opts.hostname !== "localhost" && opts.hostname !== "::1"
|
||||||
if (publish) {
|
if (publish) {
|
||||||
const unpublish = yield* Effect.cached(Effect.sync(() => MDNS.unpublish()))
|
const unpublish = yield* Effect.cached(Effect.sync(() => MDNS.unpublish()))
|
||||||
yield* Effect.sync(() => MDNS.publish(port, opts.mdnsDomain))
|
yield* Effect.sync(() => MDNS.publish(port, opts.mdns === true ? undefined : opts.mdns.domain))
|
||||||
yield* Scope.addFinalizer(scope, unpublish)
|
yield* Scope.addFinalizer(scope, unpublish)
|
||||||
return unpublish
|
return unpublish
|
||||||
}
|
}
|
||||||
@@ -188,7 +226,7 @@ function forceClose(state: ListenerState) {
|
|||||||
return Effect.all([state.http.closeAll, state.websockets.closeAll], { concurrency: "unbounded", discard: true })
|
return Effect.all([state.http.closeAll, state.websockets.closeAll], { concurrency: "unbounded", discard: true })
|
||||||
}
|
}
|
||||||
|
|
||||||
function serverLayer(opts: { port: number; hostname: string }) {
|
function serverLayer(opts: ListenOptions) {
|
||||||
const server = createServer()
|
const server = createServer()
|
||||||
const serverRef = { closeStarted: false, forceStop: false }
|
const serverRef = { closeStarted: false, forceStop: false }
|
||||||
const close = server.close.bind(server)
|
const close = server.close.bind(server)
|
||||||
@@ -203,7 +241,10 @@ function serverLayer(opts: { port: number; hostname: string }) {
|
|||||||
}) as typeof server.close
|
}) as typeof server.close
|
||||||
|
|
||||||
return Layer.mergeAll(
|
return Layer.mergeAll(
|
||||||
NodeHttpServer.layer(() => server, { port: opts.port, host: opts.hostname, gracefulShutdownTimeout: "1 second" }),
|
NodeHttpServer.layer(() => server, {
|
||||||
|
...(opts.type === "socket" ? { path: opts.socket } : { port: opts.port, host: opts.hostname }),
|
||||||
|
gracefulShutdownTimeout: "1 second",
|
||||||
|
}),
|
||||||
Layer.succeed(ListenerServerService)(
|
Layer.succeed(ListenerServerService)(
|
||||||
ListenerServerService.of({
|
ListenerServerService.of({
|
||||||
closeAll: Effect.sync(() => {
|
closeAll: Effect.sync(() => {
|
||||||
|
|||||||
@@ -85,7 +85,9 @@ describe("HttpApi CORS", () => {
|
|||||||
it.live("uses custom CORS origins passed to the server", () =>
|
it.live("uses custom CORS origins passed to the server", () =>
|
||||||
Effect.gen(function* () {
|
Effect.gen(function* () {
|
||||||
const listener = yield* Effect.acquireRelease(
|
const listener = yield* Effect.acquireRelease(
|
||||||
Effect.promise(() => Server.listen({ hostname: "127.0.0.1", port: 0, cors: ["https://custom.example"] })),
|
Effect.promise(() =>
|
||||||
|
Server.listen({ type: "tcp", hostname: "127.0.0.1", port: 0, cors: ["https://custom.example"] }),
|
||||||
|
),
|
||||||
(listener) => Effect.promise(() => listener.stop(true)),
|
(listener) => Effect.promise(() => listener.stop(true)),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
import { afterEach, describe, expect, test } from "bun:test"
|
import { afterEach, describe, expect, test } from "bun:test"
|
||||||
|
import http from "node:http"
|
||||||
import net from "node:net"
|
import net from "node:net"
|
||||||
|
import path from "node:path"
|
||||||
import { Flag } from "@opencode-ai/core/flag/flag"
|
import { Flag } from "@opencode-ai/core/flag/flag"
|
||||||
import * as Log from "@opencode-ai/core/util/log"
|
import * as Log from "@opencode-ai/core/util/log"
|
||||||
import { Server } from "../../src/server/server"
|
import { Server } from "../../src/server/server"
|
||||||
@@ -35,7 +37,7 @@ async function startListener() {
|
|||||||
Flag.OPENCODE_SERVER_USERNAME = auth.username
|
Flag.OPENCODE_SERVER_USERNAME = auth.username
|
||||||
process.env.OPENCODE_SERVER_PASSWORD = auth.password
|
process.env.OPENCODE_SERVER_PASSWORD = auth.password
|
||||||
process.env.OPENCODE_SERVER_USERNAME = auth.username
|
process.env.OPENCODE_SERVER_USERNAME = auth.username
|
||||||
return Server.listen({ hostname: "127.0.0.1", port: 0 })
|
return Server.listen({ type: "tcp", hostname: "127.0.0.1", port: 0 })
|
||||||
}
|
}
|
||||||
|
|
||||||
async function startNoAuthListener() {
|
async function startNoAuthListener() {
|
||||||
@@ -43,7 +45,7 @@ async function startNoAuthListener() {
|
|||||||
Flag.OPENCODE_SERVER_USERNAME = auth.username
|
Flag.OPENCODE_SERVER_USERNAME = auth.username
|
||||||
delete process.env.OPENCODE_SERVER_PASSWORD
|
delete process.env.OPENCODE_SERVER_PASSWORD
|
||||||
process.env.OPENCODE_SERVER_USERNAME = auth.username
|
process.env.OPENCODE_SERVER_USERNAME = auth.username
|
||||||
return Server.listen({ hostname: "127.0.0.1", port: 0 })
|
return Server.listen({ type: "tcp", hostname: "127.0.0.1", port: 0 })
|
||||||
}
|
}
|
||||||
|
|
||||||
function authorization() {
|
function authorization() {
|
||||||
@@ -134,7 +136,7 @@ async function expectSocketRejected(url: URL, init?: { headers?: Record<string,
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
function stop(listener: Awaited<ReturnType<typeof startListener>>, label: string) {
|
function stop(listener: { stop(close?: boolean): Promise<void> }, label: string) {
|
||||||
return withTimeout(listener.stop(true), 10_000, label)
|
return withTimeout(listener.stop(true), 10_000, label)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -285,6 +287,26 @@ describe("HttpApi Server.listen", () => {
|
|||||||
).rejects.toThrow()
|
).rejects.toThrow()
|
||||||
})
|
})
|
||||||
|
|
||||||
|
test("listens on a socket path", async () => {
|
||||||
|
await using tmp = await tmpdir({ git: true, config: { formatter: false, lsp: false } })
|
||||||
|
const socket =
|
||||||
|
process.platform === "win32"
|
||||||
|
? `\\\\.\\pipe\\opencode-test-${process.pid}-${Date.now()}`
|
||||||
|
: path.join(tmp.path, "opencode.sock")
|
||||||
|
const listener = await Server.listen({ type: "socket", socket })
|
||||||
|
try {
|
||||||
|
expect(listener.type).toBe("socket")
|
||||||
|
expect(listener.socket).toBe(socket)
|
||||||
|
expect(listener.url.href).toBe(`socket:${encodeURIComponent(socket)}`)
|
||||||
|
|
||||||
|
const response = await requestSocketRoot(socket)
|
||||||
|
expect(response.statusCode).toBe(200)
|
||||||
|
expect(response.body).toContain("OpenCode")
|
||||||
|
} finally {
|
||||||
|
await stop(listener, "timed out cleaning up socket listener")
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
test("default in-process handler does not emit Effect HTTP response logs", async () => {
|
test("default in-process handler does not emit Effect HTTP response logs", async () => {
|
||||||
let output = ""
|
let output = ""
|
||||||
// oxlint-disable-next-line typescript-eslint/unbound-method -- restored in finally after temporarily capturing stderr.
|
// oxlint-disable-next-line typescript-eslint/unbound-method -- restored in finally after temporarily capturing stderr.
|
||||||
@@ -433,3 +455,18 @@ function occupyPort(port: number) {
|
|||||||
server.listen(port, "127.0.0.1", () => resolve(server))
|
server.listen(port, "127.0.0.1", () => resolve(server))
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function requestSocketRoot(socket: string) {
|
||||||
|
return new Promise<{ statusCode?: number; body: string }>((resolve, reject) => {
|
||||||
|
const request = http.request(
|
||||||
|
{ socketPath: socket, path: "/", method: "GET" },
|
||||||
|
(response) => {
|
||||||
|
const chunks: Buffer[] = []
|
||||||
|
response.on("data", (chunk: Buffer) => chunks.push(chunk))
|
||||||
|
response.on("end", () => resolve({ statusCode: response.statusCode, body: Buffer.concat(chunks).toString() }))
|
||||||
|
},
|
||||||
|
)
|
||||||
|
request.on("error", reject)
|
||||||
|
request.end()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|||||||
@@ -45,7 +45,7 @@ describe("HttpApi Server.listen mDNS", () => {
|
|||||||
test("skips publish for loopback hostnames", async () => {
|
test("skips publish for loopback hostnames", async () => {
|
||||||
Flag.OPENCODE_SERVER_PASSWORD = "mdns-secret"
|
Flag.OPENCODE_SERVER_PASSWORD = "mdns-secret"
|
||||||
Flag.OPENCODE_SERVER_USERNAME = "opencode"
|
Flag.OPENCODE_SERVER_USERNAME = "opencode"
|
||||||
const listener = await Server.listen({ hostname: "127.0.0.1", port: 0, mdns: true })
|
const listener = await Server.listen({ type: "tcp", hostname: "127.0.0.1", port: 0, mdns: true })
|
||||||
try {
|
try {
|
||||||
expect(events.filter((e) => e.kind === "publish")).toEqual([])
|
expect(events.filter((e) => e.kind === "publish")).toEqual([])
|
||||||
} finally {
|
} finally {
|
||||||
@@ -57,7 +57,7 @@ describe("HttpApi Server.listen mDNS", () => {
|
|||||||
test("publishes for non-loopback hostnames and unpublishes on stop", async () => {
|
test("publishes for non-loopback hostnames and unpublishes on stop", async () => {
|
||||||
Flag.OPENCODE_SERVER_PASSWORD = "mdns-secret"
|
Flag.OPENCODE_SERVER_PASSWORD = "mdns-secret"
|
||||||
Flag.OPENCODE_SERVER_USERNAME = "opencode"
|
Flag.OPENCODE_SERVER_USERNAME = "opencode"
|
||||||
const listener = await Server.listen({ hostname: "0.0.0.0", port: 0, mdns: true })
|
const listener = await Server.listen({ type: "tcp", hostname: "0.0.0.0", port: 0, mdns: true })
|
||||||
try {
|
try {
|
||||||
const published = events.filter((e) => e.kind === "publish")
|
const published = events.filter((e) => e.kind === "publish")
|
||||||
expect(published.length).toBe(1)
|
expect(published.length).toBe(1)
|
||||||
@@ -73,7 +73,7 @@ describe("HttpApi Server.listen mDNS", () => {
|
|||||||
test("scope finalizer unpublishes even if stop() is not called for force-close", async () => {
|
test("scope finalizer unpublishes even if stop() is not called for force-close", async () => {
|
||||||
Flag.OPENCODE_SERVER_PASSWORD = "mdns-secret"
|
Flag.OPENCODE_SERVER_PASSWORD = "mdns-secret"
|
||||||
Flag.OPENCODE_SERVER_USERNAME = "opencode"
|
Flag.OPENCODE_SERVER_USERNAME = "opencode"
|
||||||
const listener = await Server.listen({ hostname: "0.0.0.0", port: 0, mdns: true })
|
const listener = await Server.listen({ type: "tcp", hostname: "0.0.0.0", port: 0, mdns: true })
|
||||||
expect(events.filter((e) => e.kind === "publish").length).toBe(1)
|
expect(events.filter((e) => e.kind === "publish").length).toBe(1)
|
||||||
// Plain (graceful) stop without close=true should still unpublish.
|
// Plain (graceful) stop without close=true should still unpublish.
|
||||||
await withTimeout(listener.stop(), 10_000, "timed out stopping graceful mdns listener")
|
await withTimeout(listener.stop(), 10_000, "timed out stopping graceful mdns listener")
|
||||||
|
|||||||
Reference in New Issue
Block a user