Skip to content

Commit e31cef3

Browse files
committed
feat(desktop): move local server config into opencode config
1 parent da0115a commit e31cef3

5 files changed

Lines changed: 98 additions & 47 deletions

File tree

packages/app/src/i18n/en.ts

Lines changed: 0 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -790,23 +790,6 @@ export const dict = {
790790
"settings.general.row.wayland.description": "Disable X11 fallback on Wayland. Requires restart.",
791791
"settings.general.row.wayland.tooltip":
792792
"On Linux with mixed refresh-rate monitors, native Wayland can be more stable.",
793-
"settings.general.row.inboundAccess.title": "Allow inbound connections",
794-
"settings.general.row.inboundAccess.description":
795-
"Allow OpenCode server access from other devices on your network. Requires restart.",
796-
"settings.general.row.inboundUsername.title": "Inbound username",
797-
"settings.general.row.inboundUsername.description": "Username for inbound clients. Leave blank to use the default.",
798-
"settings.general.row.inboundPassword.title": "Inbound password",
799-
"settings.general.row.inboundPassword.description": "Password for inbound clients. Leave blank to generate one automatically.",
800-
"settings.general.row.inboundPassword.placeholder": "Enter password",
801-
"settings.general.row.inboundPort.title": "Inbound port",
802-
"settings.general.row.inboundPort.description": "Leave blank to pick a free port automatically.",
803-
"settings.general.row.inboundPort.placeholder": "Auto",
804-
"settings.general.row.inboundPort.invalid": "Enter a port between 1 and 65535.",
805-
"settings.general.row.inbound.restartRequired": "Saved. Restart OpenCode to apply inbound changes.",
806-
"settings.general.row.inbound.save": "Save",
807-
"settings.general.row.inbound.missingCredentials":
808-
"Set both inbound username and password before enabling inbound connections.",
809-
810793
"settings.general.row.releaseNotes.title": "Release notes",
811794
"settings.general.row.releaseNotes.description": "Show What's New popups after updates",
812795

packages/desktop-electron/src/main/constants.ts

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,4 @@ export const CHANNEL: Channel = raw === "dev" || raw === "beta" || raw === "prod
77
export const SETTINGS_STORE = "opencode.settings"
88
export const DEFAULT_SERVER_URL_KEY = "defaultServerUrl"
99
export const WSL_ENABLED_KEY = "wslEnabled"
10-
export const INBOUND_ENABLED_KEY = "inboundEnabled"
11-
export const INBOUND_USERNAME_KEY = "inboundUsername"
12-
export const INBOUND_PASSWORD_KEY = "inboundPassword"
13-
export const INBOUND_PORT_KEY = "inboundPort"
1410
export const UPDATER_ENABLED = app.isPackaged && CHANNEL !== "dev"

packages/desktop-electron/src/main/server.ts

Lines changed: 77 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,8 @@
11
import { app } from "electron"
2-
import {
3-
DEFAULT_SERVER_URL_KEY,
4-
INBOUND_ENABLED_KEY,
5-
INBOUND_PASSWORD_KEY,
6-
INBOUND_PORT_KEY,
7-
INBOUND_USERNAME_KEY,
8-
WSL_ENABLED_KEY,
9-
} from "./constants"
2+
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs"
3+
import os from "node:os"
4+
import path from "node:path"
5+
import { DEFAULT_SERVER_URL_KEY, WSL_ENABLED_KEY } from "./constants"
106
import { getUserShell, loadShellEnv } from "./shell-env"
117
import { getStore } from "./store"
128

@@ -16,6 +12,7 @@ export type InboundServerConfig = { enabled: boolean; username: string; password
1612
export type HealthCheck = { wait: Promise<void> }
1713

1814
let runtimeInboundServerConfig: InboundServerConfig | null = null
15+
const emptyInboundServerConfig = { enabled: false, username: "", password: "", port: null } satisfies InboundServerConfig
1916

2017
export function setRuntimeInboundServerConfig(config: InboundServerConfig) {
2118
runtimeInboundServerConfig = config
@@ -44,33 +41,87 @@ export function setWslConfig(config: WslConfig) {
4441
getStore().set(WSL_ENABLED_KEY, config.enabled)
4542
}
4643

47-
export function getInboundServerConfig(): InboundServerConfig {
48-
const enabled = getStore().get(INBOUND_ENABLED_KEY)
49-
const username = getStore().get(INBOUND_USERNAME_KEY)
50-
const password = getStore().get(INBOUND_PASSWORD_KEY)
51-
const port = getStore().get(INBOUND_PORT_KEY)
44+
function configDir() {
45+
if (process.env.OPENCODE_CONFIG_DIR?.trim()) return process.env.OPENCODE_CONFIG_DIR.trim()
46+
return path.join(process.env.XDG_CONFIG_HOME?.trim() || path.join(os.homedir(), ".config"), "opencode")
47+
}
48+
49+
function configFile() {
50+
const dir = configDir()
51+
for (const name of ["opencode.jsonc", "opencode.json", "config.json"]) {
52+
const file = path.join(dir, name)
53+
if (existsSync(file)) return file
54+
}
55+
return path.join(dir, "opencode.jsonc")
56+
}
57+
58+
function sanitizeInboundServerConfig(value: unknown): InboundServerConfig | undefined {
59+
if (!value || typeof value !== "object" || Array.isArray(value)) return
60+
const record = value as Record<string, unknown>
5261
return {
53-
enabled: typeof enabled === "boolean" ? enabled : false,
54-
username: typeof username === "string" ? username : "",
55-
password: typeof password === "string" ? password : "",
56-
port: typeof port === "number" && Number.isInteger(port) && port > 0 && port <= 65535 ? port : null,
62+
enabled: typeof record.enabled === "boolean" ? record.enabled : false,
63+
username: typeof record.username === "string" ? record.username : "",
64+
password: typeof record.password === "string" ? record.password : "",
65+
port:
66+
typeof record.port === "number" && Number.isInteger(record.port) && record.port > 0 && record.port <= 65535
67+
? record.port
68+
: null,
5769
}
5870
}
5971

60-
export function getInboundRuntimeServerConfig(): InboundServerConfig {
61-
return runtimeInboundServerConfig ?? { enabled: false, username: "opencode", password: "", port: null }
72+
function parseConfigText(text: string) {
73+
try {
74+
return JSON.parse(text) as Record<string, unknown>
75+
} catch {}
76+
try {
77+
return JSON.parse(
78+
text
79+
.replace(/\/\*[\s\S]*?\*\//g, "")
80+
.replace(/^\s*\/\/.*$/gm, "")
81+
.replace(/,\s*([}\]])/g, "$1"),
82+
) as Record<string, unknown>
83+
} catch {
84+
return
85+
}
6286
}
6387

64-
export function setInboundServerConfig(config: InboundServerConfig) {
65-
getStore().set(INBOUND_ENABLED_KEY, config.enabled)
66-
getStore().set(INBOUND_USERNAME_KEY, config.username)
67-
getStore().set(INBOUND_PASSWORD_KEY, config.password)
68-
if (config.port === null) {
69-
getStore().delete(INBOUND_PORT_KEY)
88+
function readInboundServerConfigFromConfigFile() {
89+
try {
90+
const value = parseConfigText(readFileSync(configFile(), "utf8"))
91+
if (!value) return
92+
return sanitizeInboundServerConfig(value.localServer)
93+
} catch {
7094
return
7195
}
96+
}
97+
98+
function writableInboundServerConfig(config: InboundServerConfig) {
99+
return {
100+
enabled: config.enabled,
101+
...(config.port !== null ? { port: config.port } : {}),
102+
...(config.username ? { username: config.username } : {}),
103+
...(config.password ? { password: config.password } : {}),
104+
}
105+
}
106+
107+
function writeInboundServerConfigToConfigFile(config: InboundServerConfig) {
108+
const file = configFile()
109+
mkdirSync(path.dirname(file), { recursive: true })
110+
const next = writableInboundServerConfig(config)
111+
const current = existsSync(file) ? (parseConfigText(readFileSync(file, "utf8")) ?? {}) : {}
112+
writeFileSync(file, `${JSON.stringify({ ...current, localServer: next }, null, 2)}\n`)
113+
}
72114

73-
getStore().set(INBOUND_PORT_KEY, config.port)
115+
export function getInboundServerConfig(): InboundServerConfig {
116+
return readInboundServerConfigFromConfigFile() ?? emptyInboundServerConfig
117+
}
118+
119+
export function getInboundRuntimeServerConfig(): InboundServerConfig {
120+
return runtimeInboundServerConfig ?? emptyInboundServerConfig
121+
}
122+
123+
export function setInboundServerConfig(config: InboundServerConfig) {
124+
writeInboundServerConfigToConfigFile(config)
74125
}
75126

76127
export async function spawnLocalServer(hostname: string, port: number, username: string, password: string) {

packages/opencode/src/config/config.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -105,6 +105,9 @@ export const Info = Schema.Struct({
105105
server: Schema.optional(ConfigServer.Server).annotate({
106106
description: "Server configuration for opencode serve and web commands",
107107
}),
108+
localServer: Schema.optional(ConfigServer.LocalServer).annotate({
109+
description: "Desktop local server remote access configuration",
110+
}),
108111
command: Schema.optional(Schema.Record(Schema.String, ConfigCommand.Info)).annotate({
109112
description: "Command configuration, see https://opencode.ai/docs/commands",
110113
}),

packages/opencode/src/config/server.ts

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,4 +19,22 @@ export const Server = Schema.Struct({
1919
.pipe(withStatics((s) => ({ zod: zod(s) })))
2020
export type Server = Schema.Schema.Type<typeof Server>
2121

22+
export const LocalServer = Schema.Struct({
23+
enabled: Schema.optional(Schema.Boolean).annotate({
24+
description: "Enable remote access for the desktop local server",
25+
}),
26+
port: Schema.optional(PositiveInt).annotate({
27+
description: "Port to listen on for desktop local server remote access",
28+
}),
29+
username: Schema.optional(Schema.String).annotate({
30+
description: "Username for desktop local server remote access",
31+
}),
32+
password: Schema.optional(Schema.String).annotate({
33+
description: "Password for desktop local server remote access",
34+
}),
35+
})
36+
.annotate({ identifier: "LocalServerConfig" })
37+
.pipe(withStatics((s) => ({ zod: zod(s) })))
38+
export type LocalServer = Schema.Schema.Type<typeof LocalServer>
39+
2240
export * as ConfigServer from "./server"

0 commit comments

Comments
 (0)