diff --git a/bun.lock b/bun.lock index 796cd5661e4..818dee4f228 100644 --- a/bun.lock +++ b/bun.lock @@ -292,6 +292,7 @@ "@standard-schema/spec": "1.0.0", "@zip.js/zip.js": "2.7.62", "ai": "catalog:", + "bonjour-service": "1.3.0", "bun-pty": "0.4.2", "chokidar": "4.0.3", "clipboardy": "4.0.0", @@ -1081,6 +1082,8 @@ "@kurkle/color": ["@kurkle/color@0.3.4", "", {}, "sha512-M5UknZPHRu3DEDWoipU6sE8PdkZ6Z/S+v4dD+Ke8IaNlpdSQah50lz1KtcFBa2vsdOnwbbnxJwVM4wty6udA5w=="], + "@leichtgewicht/ip-codec": ["@leichtgewicht/ip-codec@2.0.5", "", {}, "sha512-Vo+PSpZG2/fmgmiNzYK9qWRh8h/CHrwD0mo1h1DzL4yzHNSfWYujGTYsWGreD000gcgmZ7K4Ys6Tx9TxtsKdDw=="], + "@mdx-js/mdx": ["@mdx-js/mdx@3.1.1", "", { "dependencies": { "@types/estree": "^1.0.0", "@types/estree-jsx": "^1.0.0", "@types/hast": "^3.0.0", "@types/mdx": "^2.0.0", "acorn": "^8.0.0", "collapse-white-space": "^2.0.0", "devlop": "^1.0.0", "estree-util-is-identifier-name": "^3.0.0", "estree-util-scope": "^1.0.0", "estree-walker": "^3.0.0", "hast-util-to-jsx-runtime": "^2.0.0", "markdown-extensions": "^2.0.0", "recma-build-jsx": "^1.0.0", "recma-jsx": "^1.0.0", "recma-stringify": "^1.0.0", "rehype-recma": "^1.0.0", "remark-mdx": "^3.0.0", "remark-parse": "^11.0.0", "remark-rehype": "^11.0.0", "source-map": "^0.7.0", "unified": "^11.0.0", "unist-util-position-from-estree": "^2.0.0", "unist-util-stringify-position": "^4.0.0", "unist-util-visit": "^5.0.0", "vfile": "^6.0.0" } }, "sha512-f6ZO2ifpwAQIpzGWaBQT2TXxPv6z3RBzQKpVftEWN78Vl/YweF1uwussDx8ECAXVtr3Rs89fKyG9YlzUs9DyGQ=="], "@mixmark-io/domino": ["@mixmark-io/domino@2.2.0", "", {}, "sha512-Y28PR25bHXUg88kCV7nivXrP2Nj2RueZ3/l/jdx6J9f8J4nsEGcgX0Qe6lt7Pa+J79+kPiJU3LguR6O/6zrLOw=="], @@ -2003,6 +2006,8 @@ "body-parser": ["body-parser@1.20.3", "", { "dependencies": { "bytes": "3.1.2", "content-type": "~1.0.5", "debug": "2.6.9", "depd": "2.0.0", "destroy": "1.2.0", "http-errors": "2.0.0", "iconv-lite": "0.4.24", "on-finished": "2.4.1", "qs": "6.13.0", "raw-body": "2.5.2", "type-is": "~1.6.18", "unpipe": "1.0.0" } }, "sha512-7rAxByjUMqQ3/bHJy7D6OGXvx/MMc4IqBn/X0fcM1QUcAItpZrBEYhWGem+tzXH90c+G01ypMcYJBO9Y30203g=="], + "bonjour-service": ["bonjour-service@1.3.0", "", { "dependencies": { "fast-deep-equal": "^3.1.3", "multicast-dns": "^7.2.5" } }, "sha512-3YuAUiSkWykd+2Azjgyxei8OWf8thdn8AITIog2M4UICzoqfjlqr64WIjEXZllf/W6vK1goqleSR6brGomxQqA=="], + "boolbase": ["boolbase@1.0.0", "", {}, "sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww=="], "bottleneck": ["bottleneck@2.19.5", "", {}, "sha512-VHiNCbI1lKdl44tGrhNfU3lup0Tj/ZBMJB5/2ZbNXRCPuRCO7ed2mgcK4r17y+KB2EfuYuRaVlwNbAeaWGSpbw=="], @@ -2247,6 +2252,8 @@ "dlv": ["dlv@1.1.3", "", {}, "sha512-+HlytyjlPKnIG8XuRG8WvmBP8xs8P71y+SKKS6ZXWoEgLuePxtDoUEiH7WkdePWrQ5JBpE6aoVqfZfJUQkjXwA=="], + "dns-packet": ["dns-packet@5.6.1", "", { "dependencies": { "@leichtgewicht/ip-codec": "^2.0.1" } }, "sha512-l4gcSouhcgIKRvyy99RNVOgxXiicE+2jZoNmaNmZ6JXiGajBOJAesk1OBlJuM5k2c+eudGdLxDqXuPCKIj6kpw=="], + "dom-serializer": ["dom-serializer@2.0.0", "", { "dependencies": { "domelementtype": "^2.3.0", "domhandler": "^5.0.2", "entities": "^4.2.0" } }, "sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg=="], "domelementtype": ["domelementtype@2.3.0", "", {}, "sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw=="], @@ -3023,6 +3030,8 @@ "ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="], + "multicast-dns": ["multicast-dns@7.2.5", "", { "dependencies": { "dns-packet": "^5.2.2", "thunky": "^1.0.2" }, "bin": { "multicast-dns": "cli.js" } }, "sha512-2eznPJP8z2BFLX50tf0LuODrpINqP1RVIm/CObbTcBRITQgmC/TjcREF1NeTBzIcR5XO/ukWo+YHOjBbFwIupg=="], + "mustache": ["mustache@4.2.0", "", { "bin": { "mustache": "bin/mustache" } }, "sha512-71ippSywq5Yb7/tVYyGbkBggbU8H3u5Rz56fH60jGFgr8uHwxs+aSKeqmluIVzM0m0kB7xQjKS6qPfd0b2ZoqQ=="], "mysql2": ["mysql2@3.14.4", "", { "dependencies": { "aws-ssl-profiles": "^1.1.1", "denque": "^2.1.0", "generate-function": "^2.3.1", "iconv-lite": "^0.7.0", "long": "^5.2.1", "lru.min": "^1.0.0", "named-placeholders": "^1.1.3", "seq-queue": "^0.0.5", "sqlstring": "^2.3.2" } }, "sha512-Cs/jx3WZPNrYHVz+Iunp9ziahaG5uFMvD2R8Zlmc194AqXNxt9HBNu7ZsPYrUtmJsF0egETCWIdMIYAwOGjL1w=="], @@ -3595,6 +3604,8 @@ "three": ["three@0.177.0", "", {}, "sha512-EiXv5/qWAaGI+Vz2A+JfavwYCMdGjxVsrn3oBwllUoqYeaBO75J63ZfyaQKoiLrqNHoTlUc6PFgMXnS0kI45zg=="], + "thunky": ["thunky@1.1.0", "", {}, "sha512-eHY7nBftgThBqOyHGVN+l8gF0BucP09fMo0oO/Lb0w1OF80dJv+lDVpXG60WMQvkcxAkNybKsrEIE3ZtKGmPrA=="], + "tiny-inflate": ["tiny-inflate@1.0.3", "", {}, "sha512-pkY1fj1cKHb2seWDy0B16HeWyczlJA9/WW3u3c4z/NiWDsO3DOU5D7nhTLE9CF0yXv/QZFY7sEJmj24dK+Rrqw=="], "tiny-invariant": ["tiny-invariant@1.3.3", "", {}, "sha512-+FbBPE1o9QAYvviau/qC5SE3caw21q3xkvWKBtja5vgqOWIHHJ3ioaq1VPfn/Szqctz2bU/oYeKd9/z5BL+PVg=="], diff --git a/flake.lock b/flake.lock index 4ff2c1d0e11..8bba6eeb3df 100644 --- a/flake.lock +++ b/flake.lock @@ -2,11 +2,11 @@ "nodes": { "nixpkgs": { "locked": { - "lastModified": 1766532406, - "narHash": "sha256-acLU/ag9VEoKkzOD202QASX25nG1eArXg5A0mHjKgxM=", + "lastModified": 1766747458, + "narHash": "sha256-m63jjuo/ygo8ztkCziYh5OOIbTSXUDkKbqw3Vuqu4a4=", "owner": "NixOS", "repo": "nixpkgs", - "rev": "8142186f001295e5a3239f485c8a49bf2de2695a", + "rev": "c633f572eded8c4f3c75b8010129854ed404a6ce", "type": "github" }, "original": { diff --git a/nix/hashes.json b/nix/hashes.json index 66c0baaf791..4363085de3b 100644 --- a/nix/hashes.json +++ b/nix/hashes.json @@ -1,3 +1,3 @@ { - "nodeModules": "sha256-hotsyeWJA6/dP6DvZTN1Ak2RSKcsyvXlXPI/jexBHME=" + "nodeModules": "sha256-okbViEKf1mRSmzbJgKdB9SJ875q84Bwu8d3ChHuaQ1g=" } diff --git a/packages/opencode/package.json b/packages/opencode/package.json index f04f0bd8715..ef2b822e86e 100644 --- a/packages/opencode/package.json +++ b/packages/opencode/package.json @@ -81,6 +81,7 @@ "@standard-schema/spec": "1.0.0", "@zip.js/zip.js": "2.7.62", "ai": "catalog:", + "bonjour-service": "1.3.0", "bun-pty": "0.4.2", "chokidar": "4.0.3", "clipboardy": "4.0.0", diff --git a/packages/opencode/src/cli/cmd/acp.ts b/packages/opencode/src/cli/cmd/acp.ts index c607e5f5bb7..2db64e3b1af 100644 --- a/packages/opencode/src/cli/cmd/acp.ts +++ b/packages/opencode/src/cli/cmd/acp.ts @@ -3,8 +3,10 @@ import { bootstrap } from "../bootstrap" import { cmd } from "./cmd" import { AgentSideConnection, ndJsonStream } from "@agentclientprotocol/sdk" import { ACP } from "@/acp/agent" +import { Config } from "@/config/config" import { Server } from "@/server/server" import { createOpencodeClient } from "@opencode-ai/sdk/v2" +import { withNetworkOptions, resolveNetworkOptions } from "../network" const log = Log.create({ service: "acp-command" }) @@ -19,29 +21,17 @@ export const AcpCommand = cmd({ command: "acp", describe: "start ACP (Agent Client Protocol) server", builder: (yargs) => { - return yargs - .option("cwd", { - describe: "working directory", - type: "string", - default: process.cwd(), - }) - .option("port", { - type: "number", - describe: "port to listen on", - default: 0, - }) - .option("hostname", { - type: "string", - describe: "hostname to listen on", - default: "127.0.0.1", - }) + return withNetworkOptions(yargs).option("cwd", { + describe: "working directory", + type: "string", + default: process.cwd(), + }) }, handler: async (args) => { await bootstrap(process.cwd(), async () => { - const server = Server.listen({ - port: args.port, - hostname: args.hostname, - }) + const config = await Config.get() + const opts = resolveNetworkOptions(args, config) + const server = Server.listen(opts) const sdk = createOpencodeClient({ baseUrl: `http://${server.hostname}:${server.port}`, diff --git a/packages/opencode/src/cli/cmd/serve.ts b/packages/opencode/src/cli/cmd/serve.ts index 3af3316a9d3..0fd7aa88f32 100644 --- a/packages/opencode/src/cli/cmd/serve.ts +++ b/packages/opencode/src/cli/cmd/serve.ts @@ -1,29 +1,16 @@ +import { Config } from "../../config/config" import { Server } from "../../server/server" import { cmd } from "./cmd" +import { withNetworkOptions, resolveNetworkOptions } from "../network" export const ServeCommand = cmd({ command: "serve", - builder: (yargs) => - yargs - .option("port", { - alias: ["p"], - type: "number", - describe: "port to listen on", - default: 0, - }) - .option("hostname", { - type: "string", - describe: "hostname to listen on", - default: "127.0.0.1", - }), + builder: (yargs) => withNetworkOptions(yargs), describe: "starts a headless opencode server", handler: async (args) => { - const hostname = args.hostname - const port = args.port - const server = Server.listen({ - port, - hostname, - }) + const config = await Config.get() + const opts = resolveNetworkOptions(args, config) + const server = Server.listen(opts) console.log(`opencode server listening on http://${server.hostname}:${server.port}`) await new Promise(() => {}) await server.stop() diff --git a/packages/opencode/src/cli/cmd/tui/spawn.ts b/packages/opencode/src/cli/cmd/tui/spawn.ts index fa679529890..7ab846428d7 100644 --- a/packages/opencode/src/cli/cmd/tui/spawn.ts +++ b/packages/opencode/src/cli/cmd/tui/spawn.ts @@ -1,33 +1,23 @@ import { cmd } from "@/cli/cmd/cmd" +import { Config } from "@/config/config" import { Instance } from "@/project/instance" import path from "path" import { Server } from "@/server/server" import { upgrade } from "@/cli/upgrade" +import { withNetworkOptions, resolveNetworkOptions } from "@/cli/network" export const TuiSpawnCommand = cmd({ command: "spawn [project]", builder: (yargs) => - yargs - .positional("project", { - type: "string", - describe: "path to start opencode in", - }) - .option("port", { - type: "number", - describe: "port to listen on", - default: 0, - }) - .option("hostname", { - type: "string", - describe: "hostname to listen on", - default: "127.0.0.1", - }), + withNetworkOptions(yargs).positional("project", { + type: "string", + describe: "path to start opencode in", + }), handler: async (args) => { upgrade() - const server = Server.listen({ - port: args.port, - hostname: "127.0.0.1", - }) + const config = await Config.get() + const opts = resolveNetworkOptions(args, config) + const server = Server.listen(opts) const bin = process.execPath const cmd = [] let cwd = process.cwd() diff --git a/packages/opencode/src/cli/cmd/tui/thread.ts b/packages/opencode/src/cli/cmd/tui/thread.ts index 3cf8937a974..f75e3bd6511 100644 --- a/packages/opencode/src/cli/cmd/tui/thread.ts +++ b/packages/opencode/src/cli/cmd/tui/thread.ts @@ -6,6 +6,8 @@ import path from "path" import { UI } from "@/cli/ui" import { iife } from "@/util/iife" import { Log } from "@/util/log" +import { withNetworkOptions, resolveNetworkOptions } from "@/cli/network" +import { Config } from "@/config/config" declare global { const OPENCODE_WORKER_PATH: string @@ -15,7 +17,7 @@ export const TuiThreadCommand = cmd({ command: "$0 [project]", describe: "start opencode tui", builder: (yargs) => - yargs + withNetworkOptions(yargs) .positional("project", { type: "string", describe: "path to start opencode in", @@ -36,23 +38,12 @@ export const TuiThreadCommand = cmd({ describe: "session id to continue", }) .option("prompt", { - alias: ["p"], type: "string", describe: "prompt to use", }) .option("agent", { type: "string", describe: "agent to use", - }) - .option("port", { - type: "number", - describe: "port to listen on", - default: 0, - }) - .option("hostname", { - type: "string", - describe: "hostname to listen on", - default: "127.0.0.1", }), handler: async (args) => { // Resolve relative paths against PWD to preserve behavior when using --cwd flag @@ -87,10 +78,9 @@ export const TuiThreadCommand = cmd({ process.on("unhandledRejection", (e) => { Log.Default.error(e) }) - const server = await client.call("server", { - port: args.port, - hostname: args.hostname, - }) + const config = await Config.get() + const networkOpts = resolveNetworkOptions(args, config) + const server = await client.call("server", networkOpts) const prompt = await iife(async () => { const piped = !process.stdin.isTTY ? await Bun.stdin.text() : undefined if (!args.prompt) return piped diff --git a/packages/opencode/src/cli/cmd/tui/worker.ts b/packages/opencode/src/cli/cmd/tui/worker.ts index 76f78f3faa8..3ffc45ae884 100644 --- a/packages/opencode/src/cli/cmd/tui/worker.ts +++ b/packages/opencode/src/cli/cmd/tui/worker.ts @@ -30,7 +30,7 @@ process.on("uncaughtException", (e) => { let server: Bun.Server export const rpc = { - async server(input: { port: number; hostname: string }) { + async server(input: { port: number; hostname: string; mdns?: boolean }) { if (server) await server.stop(true) try { server = Server.listen(input) diff --git a/packages/opencode/src/cli/cmd/web.ts b/packages/opencode/src/cli/cmd/web.ts index 3d3036b1b07..adede03859c 100644 --- a/packages/opencode/src/cli/cmd/web.ts +++ b/packages/opencode/src/cli/cmd/web.ts @@ -1,6 +1,8 @@ +import { Config } from "../../config/config" import { Server } from "../../server/server" import { UI } from "../ui" import { cmd } from "./cmd" +import { withNetworkOptions, resolveNetworkOptions } from "../network" import open from "open" import { networkInterfaces } from "os" @@ -28,32 +30,17 @@ function getNetworkIPs() { export const WebCommand = cmd({ command: "web", - builder: (yargs) => - yargs - .option("port", { - alias: ["p"], - type: "number", - describe: "port to listen on", - default: 0, - }) - .option("hostname", { - type: "string", - describe: "hostname to listen on", - default: "127.0.0.1", - }), + builder: (yargs) => withNetworkOptions(yargs), describe: "starts a headless opencode server", handler: async (args) => { - const hostname = args.hostname - const port = args.port - const server = Server.listen({ - port, - hostname, - }) + const config = await Config.get() + const opts = resolveNetworkOptions(args, config) + const server = Server.listen(opts) UI.empty() UI.println(UI.logo(" ")) UI.empty() - if (hostname === "0.0.0.0") { + if (opts.hostname === "0.0.0.0") { // Show localhost for local access const localhostUrl = `http://localhost:${server.port}` UI.println(UI.Style.TEXT_INFO_BOLD + " Local access: ", UI.Style.TEXT_NORMAL, localhostUrl) @@ -70,6 +57,10 @@ export const WebCommand = cmd({ } } + if (opts.mdns) { + UI.println(UI.Style.TEXT_INFO_BOLD + " mDNS: ", UI.Style.TEXT_NORMAL, "opencode.local") + } + // Open localhost in browser open(localhostUrl.toString()).catch(() => {}) } else { diff --git a/packages/opencode/src/cli/network.ts b/packages/opencode/src/cli/network.ts new file mode 100644 index 00000000000..661688572bd --- /dev/null +++ b/packages/opencode/src/cli/network.ts @@ -0,0 +1,42 @@ +import type { Argv, InferredOptionTypes } from "yargs" +import type { Config } from "../config/config" + +const options = { + port: { + type: "number" as const, + describe: "port to listen on", + default: 0, + }, + hostname: { + type: "string" as const, + describe: "hostname to listen on", + default: "127.0.0.1", + }, + mdns: { + type: "boolean" as const, + describe: "enable mDNS service discovery (defaults hostname to 0.0.0.0)", + default: false, + }, +} + +export type NetworkOptions = InferredOptionTypes + +export function withNetworkOptions(yargs: Argv) { + return yargs.options(options) +} + +export function resolveNetworkOptions(args: NetworkOptions, config?: Config.Info) { + const portExplicitlySet = process.argv.includes("--port") + const hostnameExplicitlySet = process.argv.includes("--hostname") + const mdnsExplicitlySet = process.argv.includes("--mdns") + + const mdns = mdnsExplicitlySet ? args.mdns : (config?.server?.mdns ?? args.mdns) + const port = portExplicitlySet ? args.port : (config?.server?.port ?? args.port) + const hostname = hostnameExplicitlySet + ? args.hostname + : mdns && !config?.server?.hostname + ? "0.0.0.0" + : (config?.server?.hostname ?? args.hostname) + + return { hostname, port, mdns } +} diff --git a/packages/opencode/src/config/config.ts b/packages/opencode/src/config/config.ts index ba9d1973025..9187ed6745a 100644 --- a/packages/opencode/src/config/config.ts +++ b/packages/opencode/src/config/config.ts @@ -587,6 +587,17 @@ export namespace Config { .describe("Control diff rendering style: 'auto' adapts to terminal width, 'stacked' always shows single column"), }) + export const Server = z + .object({ + port: z.number().int().positive().optional().describe("Port to listen on"), + hostname: z.string().optional().describe("Hostname to listen on"), + mdns: z.boolean().optional().describe("Enable mDNS service discovery"), + }) + .strict() + .meta({ + ref: "ServerConfig", + }) + export const Layout = z.enum(["auto", "stretch"]).meta({ ref: "LayoutConfig", }) @@ -634,6 +645,7 @@ export namespace Config { theme: z.string().optional().describe("Theme name to use for the interface"), keybinds: Keybinds.optional().describe("Custom keybind configurations"), tui: TUI.optional().describe("TUI specific settings"), + server: Server.optional().describe("Server configuration for opencode serve and web commands"), command: z .record(z.string(), Command) .optional() diff --git a/packages/opencode/src/server/mdns.ts b/packages/opencode/src/server/mdns.ts new file mode 100644 index 00000000000..45e61d361ac --- /dev/null +++ b/packages/opencode/src/server/mdns.ts @@ -0,0 +1,57 @@ +import { Log } from "@/util/log" +import Bonjour from "bonjour-service" + +const log = Log.create({ service: "mdns" }) + +export namespace MDNS { + let bonjour: Bonjour | undefined + let currentPort: number | undefined + + export function publish(port: number, name = "opencode") { + if (currentPort === port) return + if (bonjour) unpublish() + + try { + bonjour = new Bonjour() + const service = bonjour.publish({ + name, + type: "http", + port, + txt: { path: "/" }, + }) + + service.on("up", () => { + log.info("mDNS service published", { name, port }) + }) + + service.on("error", (err) => { + log.error("mDNS service error", { error: err }) + }) + + currentPort = port + } catch (err) { + log.error("mDNS publish failed", { error: err }) + if (bonjour) { + try { + bonjour.destroy() + } catch {} + } + bonjour = undefined + currentPort = undefined + } + } + + export function unpublish() { + if (bonjour) { + try { + bonjour.unpublishAll() + bonjour.destroy() + } catch (err) { + log.error("mDNS unpublish failed", { error: err }) + } + bonjour = undefined + currentPort = undefined + log.info("mDNS service unpublished") + } + } +} diff --git a/packages/opencode/src/server/server.ts b/packages/opencode/src/server/server.ts index c74dbbb41ef..65393e12897 100644 --- a/packages/opencode/src/server/server.ts +++ b/packages/opencode/src/server/server.ts @@ -45,9 +45,11 @@ import { Snapshot } from "@/snapshot" import { SessionSummary } from "@/session/summary" import { SessionStatus } from "@/session/status" import { upgradeWebSocket, websocket } from "hono/bun" +import type { BunWebSocketData } from "hono/bun" import { errors } from "./error" import { Pty } from "@/pty" import { Installation } from "@/installation" +import { MDNS } from "./mdns" // @ts-ignore This global is needed to prevent ai-sdk from logging warnings to stdout https://github.com/vercel/ai/blob/2dc67e0ef538307f21368db32d5a12345d98831b/packages/ai/src/logger/log-warnings.ts#L85 globalThis.AI_SDK_LOG_WARNINGS = false @@ -2623,20 +2625,41 @@ export namespace Server { return result } - export function listen(opts: { port: number; hostname: string }) { + export function listen(opts: { port: number; hostname: string; mdns?: boolean }) { const args = { hostname: opts.hostname, idleTimeout: 0, fetch: App().fetch, websocket: websocket, } as const - if (opts.port === 0) { + const tryServe = (port: number) => { try { - return Bun.serve({ ...args, port: 4096 }) + return Bun.serve({ ...args, port }) } catch { - // port 4096 not available, fall through to use port 0 + return undefined } } - return Bun.serve({ ...args, port: opts.port }) + 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}`) + + const shouldPublishMDNS = + opts.mdns && + server.port && + opts.hostname !== "127.0.0.1" && + opts.hostname !== "localhost" && + opts.hostname !== "::1" + if (shouldPublishMDNS) { + MDNS.publish(server.port!) + } else if (opts.mdns) { + log.warn("mDNS enabled but hostname is loopback; skipping mDNS publish") + } + + const originalStop = server.stop.bind(server) + server.stop = async (closeActiveConnections?: boolean) => { + if (shouldPublishMDNS) MDNS.unpublish() + return originalStop(closeActiveConnections) + } + + return server } } diff --git a/packages/web/src/content/docs/cli.mdx b/packages/web/src/content/docs/cli.mdx index e4e40ac7a4c..4a826e5b3ff 100644 --- a/packages/web/src/content/docs/cli.mdx +++ b/packages/web/src/content/docs/cli.mdx @@ -335,10 +335,11 @@ This starts an HTTP server that provides API access to opencode functionality wi #### Flags -| Flag | Short | Description | -| ------------ | ----- | --------------------- | -| `--port` | `-p` | Port to listen on | -| `--hostname` | | Hostname to listen on | +| Flag | Description | +| ------------ | --------------------- | +| `--port` | Port to listen on | +| `--hostname` | Hostname to listen on | +| `--mdns` | Enable mDNS discovery | --- @@ -428,10 +429,11 @@ This starts an HTTP server and opens a web browser to access OpenCode through a #### Flags -| Flag | Short | Description | -| ------------ | ----- | --------------------- | -| `--port` | `-p` | Port to listen on | -| `--hostname` | | Hostname to listen on | +| Flag | Description | +| ------------ | --------------------- | +| `--port` | Port to listen on | +| `--hostname` | Hostname to listen on | +| `--mdns` | Enable mDNS discovery | --- diff --git a/packages/web/src/content/docs/config.mdx b/packages/web/src/content/docs/config.mdx index ebaff36bb15..d7f8031782c 100644 --- a/packages/web/src/content/docs/config.mdx +++ b/packages/web/src/content/docs/config.mdx @@ -120,6 +120,31 @@ Available options: --- +### Server + +You can configure server settings for the `opencode serve` and `opencode web` commands through the `server` option. + +```json title="opencode.json" +{ + "$schema": "https://opencode.ai/config.json", + "server": { + "port": 4096, + "hostname": "0.0.0.0", + "mdns": true + } +} +``` + +Available options: + +- `port` - Port to listen on. +- `hostname` - Hostname to listen on. When `mdns` is enabled and no hostname is set, defaults to `0.0.0.0`. +- `mdns` - Enable mDNS service discovery. This allows other devices on the network to discover your OpenCode server. + +[Learn more about the server here](/docs/server). + +--- + ### Tools You can manage the tools an LLM can use through the `tools` option. diff --git a/packages/web/src/content/docs/server.mdx b/packages/web/src/content/docs/server.mdx index 427d8f505ff..c63917f792e 100644 --- a/packages/web/src/content/docs/server.mdx +++ b/packages/web/src/content/docs/server.mdx @@ -18,10 +18,11 @@ opencode serve [--port ] [--hostname ] #### Options -| Flag | Short | Description | Default | -| ------------ | ----- | --------------------- | ----------- | -| `--port` | `-p` | Port to listen on | `4096` | -| `--hostname` | `-h` | Hostname to listen on | `127.0.0.1` | +| Flag | Description | Default | +| ------------ | --------------------- | ----------- | +| `--port` | Port to listen on | `4096` | +| `--hostname` | Hostname to listen on | `127.0.0.1` | +| `--mdns` | Enable mDNS discovery | `false` | ---