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
35 changes: 27 additions & 8 deletions packages/opencode/src/config/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import os from "os"
import { Process } from "../util/process"
import z from "zod"
import { ModelsDev } from "../provider/models"
import { mergeDeep, pipe, unique } from "remeda"
import { mergeDeep, pipe } from "remeda"
import { Global } from "../global"
import fsNode from "fs/promises"
import { NamedError } from "@opencode-ai/util/error"
Expand Down Expand Up @@ -1257,6 +1257,13 @@ export namespace Config {
})

const loadInstanceState = Effect.fnUntraced(function* (ctx: InstanceContext) {
const settings = (() => {
const raw = Flag.OPENCODE_SETTINGS_SOURCES
if (raw === undefined) return undefined
if (raw === "") return [] as string[]
return raw.split(",")
})()
const enabled = (s: string) => !settings || settings.includes(s)
const auth = yield* authSvc.all().pipe(Effect.orDie)

let result: Info = {}
Expand Down Expand Up @@ -1288,6 +1295,7 @@ export namespace Config {
if (value.type === "wellknown") {
const url = key.replace(/\/+$/, "")
process.env[value.key] = value.token
if (!enabled("remote")) continue
log.debug("fetching remote config", { url: `${url}/.well-known/opencode` })
const response = yield* Effect.promise(() => fetch(`${url}/.well-known/opencode`))
if (!response.ok) {
Expand All @@ -1306,15 +1314,17 @@ export namespace Config {
}
}

const global = yield* getGlobal()
merge(Global.Path.config, global, "global")
if (enabled("global")) {
const global = yield* getGlobal()
merge(Global.Path.config, global, "global")
}

if (Flag.OPENCODE_CONFIG) {
merge(Flag.OPENCODE_CONFIG, yield* loadFile(Flag.OPENCODE_CONFIG))
log.debug("loaded custom config", { path: Flag.OPENCODE_CONFIG })
}

if (!Flag.OPENCODE_DISABLE_PROJECT_CONFIG) {
if (!Flag.OPENCODE_DISABLE_PROJECT_CONFIG && enabled("project")) {
for (const file of yield* Effect.promise(() =>
ConfigPaths.projectFiles("opencode", ctx.directory, ctx.worktree),
)) {
Expand All @@ -1326,15 +1336,24 @@ export namespace Config {
result.mode = result.mode || {}
result.plugin = result.plugin || []

const directories = yield* Effect.promise(() => ConfigPaths.directories(ctx.directory, ctx.worktree))
const tagged = yield* Effect.promise(() => ConfigPaths.directories(ctx.directory, ctx.worktree))
const seen = new Set<string>()
const directories: string[] = []

for (const entry of tagged) {
if (seen.has(entry.path)) continue
seen.add(entry.path)
if (entry.source !== "always" && !enabled(entry.source)) continue
directories.push(entry.path)
}

if (Flag.OPENCODE_CONFIG_DIR) {
log.debug("loading config from OPENCODE_CONFIG_DIR", { path: Flag.OPENCODE_CONFIG_DIR })
}

const deps: Promise<void>[] = []

for (const dir of unique(directories)) {
for (const dir of directories) {
if (dir.endsWith(".opencode") || dir === Flag.OPENCODE_CONFIG_DIR) {
for (const file of ["opencode.json", "opencode.jsonc"]) {
const source = path.join(dir, file)
Expand Down Expand Up @@ -1391,7 +1410,7 @@ export namespace Config {
dir: path.dirname(source),
source,
})
merge(source, next, "global")
if (enabled("remote")) merge(source, next, "global")
}
}).pipe(
Effect.catch((err) => {
Expand All @@ -1403,7 +1422,7 @@ export namespace Config {
)
}

if (existsSync(managedDir)) {
if (enabled("managed") && existsSync(managedDir)) {
for (const file of ["opencode.json", "opencode.jsonc"]) {
const source = path.join(managedDir, file)
merge(source, yield* loadFile(source), "global")
Expand Down
36 changes: 20 additions & 16 deletions packages/opencode/src/config/paths.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,24 +14,28 @@ export namespace ConfigPaths {

export async function directories(directory: string, worktree: string) {
return [
Global.Path.config,
{ path: Global.Path.config, source: "global" as const },
...(!Flag.OPENCODE_DISABLE_PROJECT_CONFIG
? await Array.fromAsync(
Filesystem.up({
targets: [".opencode"],
start: directory,
stop: worktree,
}),
)
? (
await Array.fromAsync(
Filesystem.up({
targets: [".opencode"],
start: directory,
stop: worktree,
}),
)
).map((item) => ({ path: item, source: "project" as const }))
: []),
...(await Array.fromAsync(
Filesystem.up({
targets: [".opencode"],
start: Global.Path.home,
stop: Global.Path.home,
}),
)),
...(Flag.OPENCODE_CONFIG_DIR ? [Flag.OPENCODE_CONFIG_DIR] : []),
...(
await Array.fromAsync(
Filesystem.up({
targets: [".opencode"],
start: Global.Path.home,
stop: Global.Path.home,
}),
)
).map((item) => ({ path: item, source: "global" as const })),
...(Flag.OPENCODE_CONFIG_DIR ? [{ path: Flag.OPENCODE_CONFIG_DIR, source: "always" as const }] : []),
]
}

Expand Down
2 changes: 1 addition & 1 deletion packages/opencode/src/config/tui.ts
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,7 @@ export namespace TuiConfig {
let projectFiles = Flag.OPENCODE_DISABLE_PROJECT_CONFIG
? []
: await ConfigPaths.projectFiles("tui", Instance.directory, Instance.worktree)
const directories = await ConfigPaths.directories(Instance.directory, Instance.worktree)
const directories = (await ConfigPaths.directories(Instance.directory, Instance.worktree)).map((item) => item.path)
const custom = customPath()
const managed = Config.managedConfigDir()
await migrateTuiConfig({ directories, custom, managed })
Expand Down
12 changes: 12 additions & 0 deletions packages/opencode/src/flag/flag.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ export namespace Flag {
export declare const OPENCODE_CONFIG_DIR: string | undefined
export declare const OPENCODE_PLUGIN_META_FILE: string | undefined
export const OPENCODE_CONFIG_CONTENT = process.env["OPENCODE_CONFIG_CONTENT"]
export declare const OPENCODE_SETTINGS_SOURCES: string | undefined
export const OPENCODE_DISABLE_AUTOUPDATE = truthy("OPENCODE_DISABLE_AUTOUPDATE")
export const OPENCODE_ALWAYS_NOTIFY_UPDATE = truthy("OPENCODE_ALWAYS_NOTIFY_UPDATE")
export const OPENCODE_DISABLE_PRUNE = truthy("OPENCODE_DISABLE_PRUNE")
Expand Down Expand Up @@ -152,3 +153,14 @@ Object.defineProperty(Flag, "OPENCODE_CLIENT", {
enumerable: true,
configurable: false,
})

// Dynamic getter for OPENCODE_SETTINGS_SOURCES
// This must be evaluated at access time, not module load time,
// because tests and external tooling set this env var at runtime
Object.defineProperty(Flag, "OPENCODE_SETTINGS_SOURCES", {
get() {
return process.env["OPENCODE_SETTINGS_SOURCES"]
},
enumerable: true,
configurable: false,
})
35 changes: 20 additions & 15 deletions packages/opencode/src/session/instruction.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,18 +22,6 @@ const FILES = [
"CONTEXT.md", // deprecated
]

function globalFiles() {
const files = []
if (Flag.OPENCODE_CONFIG_DIR) {
files.push(path.join(Flag.OPENCODE_CONFIG_DIR, "AGENTS.md"))
}
files.push(path.join(Global.Path.config, "AGENTS.md"))
if (!Flag.OPENCODE_DISABLE_CLAUDE_CODE_PROMPT) {
files.push(path.join(os.homedir(), ".claude", "CLAUDE.md"))
}
return files
}

function extract(messages: MessageV2.WithParts[]) {
const paths = new Set<string>()
for (const msg of messages) {
Expand Down Expand Up @@ -122,9 +110,11 @@ export namespace Instruction {
const systemPaths = Effect.fn("Instruction.systemPaths")(function* () {
const config = yield* cfg.get()
const paths = new Set<string>()
const raw = Flag.OPENCODE_SETTINGS_SOURCES
const sources = raw === undefined ? undefined : raw === "" ? ([] as string[]) : raw.split(",")

// The first project-level match wins so we don't stack AGENTS.md/CLAUDE.md from every ancestor.
if (!Flag.OPENCODE_DISABLE_PROJECT_CONFIG) {
if (!Flag.OPENCODE_DISABLE_PROJECT_CONFIG && (!sources || sources.includes("project"))) {
for (const file of FILES) {
const matches = yield* fs.findUp(file, Instance.directory, Instance.worktree)
if (matches.length > 0) {
Expand All @@ -134,10 +124,25 @@ export namespace Instruction {
}
}

for (const file of globalFiles()) {
let found = false
if (Flag.OPENCODE_CONFIG_DIR) {
const file = path.join(Flag.OPENCODE_CONFIG_DIR, "AGENTS.md")
if (yield* fs.existsSafe(file)) {
paths.add(path.resolve(file))
found = true
}
}
if (!found && (!sources || sources.includes("global"))) {
const file = path.join(Global.Path.config, "AGENTS.md")
if (yield* fs.existsSafe(file)) {
paths.add(path.resolve(file))
found = true
}
}
if (!found && (!sources || sources.includes("global")) && !Flag.OPENCODE_DISABLE_CLAUDE_CODE_PROMPT) {
const file = path.join(os.homedir(), ".claude", "CLAUDE.md")
if (yield* fs.existsSafe(file)) {
paths.add(path.resolve(file))
break
}
}

Expand Down
Loading
Loading