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
120 changes: 38 additions & 82 deletions packages/opencode/src/config/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,6 @@ import { lazy } from "../util/lazy"
import { NamedError } from "@opencode-ai/util/error"
import { Flag } from "../flag/flag"
import { Auth } from "../auth"
import { Env } from "../env"
import {
type ParseError as JsoncParseError,
applyEdits,
Expand All @@ -33,11 +32,9 @@ import { Glob } from "../util/glob"
import { PackageRegistry } from "@/bun/registry"
import { proxied } from "@/util/proxied"
import { iife } from "@/util/iife"
import { Account } from "@/account"
import { Control } from "@/control"
import { ConfigPaths } from "./paths"
import { Filesystem } from "@/util/filesystem"
import { Process } from "@/util/process"
import { Lock } from "@/util/lock"

export namespace Config {
const ModelId = z.string().meta({ $ref: "https://models.dev/model-schema.json#/$defs/Model" })
Expand Down Expand Up @@ -89,12 +86,11 @@ export namespace Config {
let result: Info = {}
for (const [key, value] of Object.entries(auth)) {
if (value.type === "wellknown") {
const url = key.replace(/\/+$/, "")
process.env[value.key] = value.token
log.debug("fetching remote config", { url: `${url}/.well-known/opencode` })
const response = await fetch(`${url}/.well-known/opencode`)
log.debug("fetching remote config", { url: `${key}/.well-known/opencode` })
const response = await fetch(`${key}/.well-known/opencode`)
if (!response.ok) {
throw new Error(`failed to fetch remote config from ${url}: ${response.status}`)
throw new Error(`failed to fetch remote config from ${key}: ${response.status}`)
}
const wellknown = (await response.json()) as any
const remoteConfig = wellknown.config ?? {}
Expand All @@ -103,14 +99,18 @@ export namespace Config {
result = mergeConfigConcatArrays(
result,
await load(JSON.stringify(remoteConfig), {
dir: path.dirname(`${url}/.well-known/opencode`),
source: `${url}/.well-known/opencode`,
dir: path.dirname(`${key}/.well-known/opencode`),
source: `${key}/.well-known/opencode`,
}),
)
log.debug("loaded remote config from well-known", { url })
log.debug("loaded remote config from well-known", { url: key })
}
}

const token = await Control.token()
if (token) {
}

// Global user config overrides remote config.
result = mergeConfigConcatArrays(result, await global())

Expand Down Expand Up @@ -177,32 +177,6 @@ export namespace Config {
log.debug("loaded custom config from OPENCODE_CONFIG_CONTENT")
}

const active = await Account.active()
if (active?.active_org_id) {
try {
const [config, token] = await Promise.all([
Account.config(active.id, active.active_org_id),
Account.token(active.id),
])
if (token) {
process.env["OPENCODE_CONSOLE_TOKEN"] = token
Env.set("OPENCODE_CONSOLE_TOKEN", token)
}

if (config) {
result = mergeConfigConcatArrays(
result,
await load(JSON.stringify(config), {
dir: path.dirname(`${active.url}/api/config`),
source: `${active.url}/api/config`,
}),
)
}
} catch (err: any) {
log.debug("failed to fetch remote account config", { error: err?.message ?? err })
}
}

// Load managed config files last (highest priority) - enterprise admin-controlled
// Kept separate from directories array to avoid write operations when installing plugins
// which would fail on system directories requiring elevated permissions
Expand Down Expand Up @@ -290,37 +264,34 @@ export namespace Config {

// Install any additional dependencies defined in the package.json
// This allows local plugins and custom tools to use external packages
using _ = await Lock.write("bun-install")
await BunProc.run(
[
"install",
// TODO: get rid of this case (see: https://github.com/oven-sh/bun/issues/19936)
...(proxied() || process.env.CI ? ["--no-cache"] : []),
],
{ cwd: dir },
).catch((err) => {
if (err instanceof Process.RunFailedError) {
const detail = {
dir,
cmd: err.cmd,
code: err.code,
stdout: err.stdout.toString(),
stderr: err.stderr.toString(),
}
if (Flag.OPENCODE_STRICT_CONFIG_DEPS) {
log.error("failed to install dependencies", detail)
throw err
}
log.warn("failed to install dependencies", detail)
const args = [
"install",
// TODO: get rid of this case (see: https://github.com/oven-sh/bun/issues/19936)
...(proxied() || process.env.CI ? ["--no-cache"] : []),
]

const maxAttempts = process.platform === "win32" ? 2 : 1
for (let attempt = 1; attempt <= maxAttempts; attempt++) {
try {
await BunProc.run(args, { cwd: dir })
return
}
} catch (error) {
if (attempt >= maxAttempts) {
log.warn("config dependency install failed after retry", {
cwd: dir,
error: error instanceof Error ? error.message : String(error),
})
return
}

if (Flag.OPENCODE_STRICT_CONFIG_DEPS) {
log.error("failed to install dependencies", { dir, error: err })
throw err
log.warn("config dependency install failed, retrying", {
attempt,
cwd: dir,
error: error instanceof Error ? error.message : String(error),
})
await new Promise((resolve) => setTimeout(resolve, 350 * attempt))
}
log.warn("failed to install dependencies", { dir, error: err })
})
}
}

async function isWritable(dir: string) {
Expand Down Expand Up @@ -1018,14 +989,6 @@ export namespace Config {
.describe(
"Timeout in milliseconds for requests to this provider. Default is 300000 (5 minutes). Set to false to disable timeout.",
),
chunkTimeout: z
.number()
.int()
.positive()
.optional()
.describe(
"Timeout in milliseconds between streamed SSE chunks for this provider. If no chunk arrives within this window, the request is aborted.",
),
})
.catchall(z.any())
.optional(),
Expand All @@ -1052,12 +1015,7 @@ export namespace Config {
})
.optional(),
plugin: z.string().array().optional(),
snapshot: z
.boolean()
.optional()
.describe(
"Enable or disable snapshot tracking. When false, filesystem snapshots are not recorded and undoing or reverting will not undo/redo file changes. Defaults to true.",
),
snapshot: z.boolean().optional(),
share: z
.enum(["manual", "auto", "disabled"])
.optional()
Expand Down Expand Up @@ -1299,7 +1257,7 @@ export namespace Config {
if (!parsed.data.$schema && isFile) {
parsed.data.$schema = "https://opencode.ai/config.json"
const updated = original.replace(/^\s*\{/, '{\n "$schema": "https://opencode.ai/config.json",')
await Filesystem.write(options.path, updated).catch(() => {})
await Bun.write(options.path, updated).catch(() => {})
}
const data = parsed.data
if (data.plugin && isFile) {
Expand Down Expand Up @@ -1460,5 +1418,3 @@ export namespace Config {
return state().then((x) => x.directories)
}
}
Filesystem.write
Filesystem.write
Loading
Loading