summaryrefslogtreecommitdiffhomepage
path: root/packages
diff options
context:
space:
mode:
authorDax <[email protected]>2026-04-17 02:44:01 -0400
committerGitHub <[email protected]>2026-04-17 06:44:01 +0000
commitd9950598d0da16fc0f8e6c289050a9d3da055af7 (patch)
tree7030c84e69be7ddfc3923be2b31226f630de9140 /packages
parent81f0885879f1e89b21774fc1fc6c603bd0ecd967 (diff)
downloadopencode-d9950598d0da16fc0f8e6c289050a9d3da055af7.tar.gz
opencode-d9950598d0da16fc0f8e6c289050a9d3da055af7.zip
core: migrate config loading to Effect framework (#23032)
Diffstat (limited to 'packages')
-rw-r--r--packages/opencode/src/cli/cmd/tui/app.tsx2
-rw-r--r--packages/opencode/src/cli/cmd/tui/config/tui-migrate.ts6
-rw-r--r--packages/opencode/src/cli/cmd/tui/config/tui.ts27
-rw-r--r--packages/opencode/src/config/config.ts437
-rw-r--r--packages/opencode/src/config/paths.ts46
5 files changed, 265 insertions, 253 deletions
diff --git a/packages/opencode/src/cli/cmd/tui/app.tsx b/packages/opencode/src/cli/cmd/tui/app.tsx
index 74eca9a0f..f8ffd27dc 100644
--- a/packages/opencode/src/cli/cmd/tui/app.tsx
+++ b/packages/opencode/src/cli/cmd/tui/app.tsx
@@ -135,7 +135,9 @@ export function tui(input: {
await TuiPluginRuntime.dispose()
}
+ console.log("starting renderer")
const renderer = await createCliRenderer(rendererConfig(input.config))
+ console.log("renderer started")
await render(() => {
return (
diff --git a/packages/opencode/src/cli/cmd/tui/config/tui-migrate.ts b/packages/opencode/src/cli/cmd/tui/config/tui-migrate.ts
index 9323dd979..a7f50ddf9 100644
--- a/packages/opencode/src/cli/cmd/tui/config/tui-migrate.ts
+++ b/packages/opencode/src/cli/cmd/tui/config/tui-migrate.ts
@@ -132,8 +132,10 @@ async function backupAndStripLegacy(file: string, source: string) {
}
async function opencodeFiles(input: { directories: string[]; cwd: string }) {
- const project = Flag.OPENCODE_DISABLE_PROJECT_CONFIG ? [] : await ConfigPaths.projectFiles("opencode", input.cwd)
- const files = [...project, ...ConfigPaths.fileInDirectory(Global.Path.config, "opencode")]
+ const files = [
+ ...ConfigPaths.fileInDirectory(Global.Path.config, "opencode"),
+ ...(await Filesystem.findUp(["opencode.json", "opencode.jsonc"], input.cwd, undefined, { rootFirst: true })),
+ ]
for (const dir of unique(input.directories)) {
files.push(...ConfigPaths.fileInDirectory(dir, "opencode"))
}
diff --git a/packages/opencode/src/cli/cmd/tui/config/tui.ts b/packages/opencode/src/cli/cmd/tui/config/tui.ts
index 1a5e49bad..a5c9ae043 100644
--- a/packages/opencode/src/cli/cmd/tui/config/tui.ts
+++ b/packages/opencode/src/cli/cmd/tui/config/tui.ts
@@ -89,15 +89,13 @@ async function mergeFile(acc: Acc, file: string, ctx: { directory: string }) {
acc.result.plugin_origins = plugins
}
-async function loadState(ctx: { directory: string }) {
+const loadState = Effect.fn("TuiConfig.loadState")(function* (ctx: { directory: string }) {
// Every config dir we may read from: global config dir, any `.opencode`
// folders between cwd and home, and OPENCODE_CONFIG_DIR.
- const directories = await ConfigPaths.directories(ctx.directory)
- // One-time migration: extract tui keys (theme/keybinds/tui) from existing
- // opencode.json files into sibling tui.json files.
- await migrateTuiConfig({ directories, cwd: ctx.directory })
+ const directories = yield* ConfigPaths.directories(ctx.directory)
+ yield* Effect.promise(() => migrateTuiConfig({ directories, cwd: ctx.directory }))
- const projectFiles = Flag.OPENCODE_DISABLE_PROJECT_CONFIG ? [] : await ConfigPaths.projectFiles("tui", ctx.directory)
+ const projectFiles = Flag.OPENCODE_DISABLE_PROJECT_CONFIG ? [] : yield* ConfigPaths.files("tui", ctx.directory)
const acc: Acc = {
result: {},
@@ -105,18 +103,19 @@ async function loadState(ctx: { directory: string }) {
// 1. Global tui config (lowest precedence).
for (const file of ConfigPaths.fileInDirectory(Global.Path.config, "tui")) {
- await mergeFile(acc, file, ctx)
+ yield* Effect.promise(() => mergeFile(acc, file, ctx)).pipe(Effect.orDie)
}
// 2. Explicit OPENCODE_TUI_CONFIG override, if set.
if (Flag.OPENCODE_TUI_CONFIG) {
- await mergeFile(acc, Flag.OPENCODE_TUI_CONFIG, ctx)
- log.debug("loaded custom tui config", { path: Flag.OPENCODE_TUI_CONFIG })
+ const configFile = Flag.OPENCODE_TUI_CONFIG
+ yield* Effect.promise(() => mergeFile(acc, configFile, ctx)).pipe(Effect.orDie)
+ log.debug("loaded custom tui config", { path: configFile })
}
// 3. Project tui files, applied root-first so the closest file wins.
for (const file of projectFiles) {
- await mergeFile(acc, file, ctx)
+ yield* Effect.promise(() => mergeFile(acc, file, ctx)).pipe(Effect.orDie)
}
// 4. `.opencode` directories (and OPENCODE_CONFIG_DIR) discovered while
@@ -127,7 +126,7 @@ async function loadState(ctx: { directory: string }) {
for (const dir of dirs) {
if (!dir.endsWith(".opencode") && dir !== Flag.OPENCODE_CONFIG_DIR) continue
for (const file of ConfigPaths.fileInDirectory(dir, "tui")) {
- await mergeFile(acc, file, ctx)
+ yield* Effect.promise(() => mergeFile(acc, file, ctx)).pipe(Effect.orDie)
}
}
@@ -146,14 +145,14 @@ async function loadState(ctx: { directory: string }) {
config: acc.result,
dirs: acc.result.plugin?.length ? dirs : [],
}
-}
+})
export const layer = Layer.effect(
Service,
Effect.gen(function* () {
const directory = yield* CurrentWorkingDirectory
const npm = yield* Npm.Service
- const data = yield* Effect.promise(() => loadState({ directory }))
+ const data = yield* loadState({ directory })
const deps = yield* Effect.forEach(
data.dirs,
(dir) =>
@@ -176,7 +175,7 @@ export const layer = Layer.effect(
}).pipe(Effect.withSpan("TuiConfig.layer")),
)
-export const defaultLayer = layer.pipe(Layer.provide(Npm.defaultLayer))
+export const defaultLayer = layer.pipe(Layer.provide(Npm.defaultLayer), Layer.provide(AppFileSystem.defaultLayer))
const { runPromise } = makeRuntime(Service, defaultLayer)
diff --git a/packages/opencode/src/config/config.ts b/packages/opencode/src/config/config.ts
index 7598aa92f..ebd4a41fc 100644
--- a/packages/opencode/src/config/config.ts
+++ b/packages/opencode/src/config/config.ts
@@ -413,260 +413,261 @@ export const layer = Layer.effect(
}
})
- const loadInstanceState = Effect.fn("Config.loadInstanceState")(function* (ctx: InstanceContext) {
- const auth = yield* authSvc.all().pipe(Effect.orDie)
-
- let result: Info = {}
- const consoleManagedProviders = new Set<string>()
- let activeOrgName: string | undefined
-
- const pluginScopeForSource = Effect.fnUntraced(function* (source: string) {
- if (source.startsWith("http://") || source.startsWith("https://")) return "global"
- if (source === "OPENCODE_CONFIG_CONTENT") return "local"
- if (yield* InstanceRef.use((ctx) => Effect.succeed(Instance.containsPath(source, ctx)))) return "local"
- return "global"
- })
+ const loadInstanceState = Effect.fn("Config.loadInstanceState")(
+ function* (ctx: InstanceContext) {
+ const auth = yield* authSvc.all().pipe(Effect.orDie)
+
+ let result: Info = {}
+ const consoleManagedProviders = new Set<string>()
+ let activeOrgName: string | undefined
+
+ const pluginScopeForSource = Effect.fnUntraced(function* (source: string) {
+ if (source.startsWith("http://") || source.startsWith("https://")) return "global"
+ if (source === "OPENCODE_CONFIG_CONTENT") return "local"
+ if (yield* InstanceRef.use((ctx) => Effect.succeed(Instance.containsPath(source, ctx)))) return "local"
+ return "global"
+ })
- const mergePluginOrigins = Effect.fnUntraced(function* (
- source: string,
- // mergePluginOrigins receives raw Specs from one config source, before provenance for this merge step
- // is attached.
- list: ConfigPlugin.Spec[] | undefined,
- // Scope can be inferred from the source path, but some callers already know whether the config should
- // behave as global or local and can pass that explicitly.
- kind?: ConfigPlugin.Scope,
- ) {
- if (!list?.length) return
- const hit = kind ?? (yield* pluginScopeForSource(source))
- // Merge newly seen plugin origins with previously collected ones, then dedupe by plugin identity while
- // keeping the winning source/scope metadata for downstream installs, writes, and diagnostics.
- const plugins = ConfigPlugin.deduplicatePluginOrigins([
- ...(result.plugin_origins ?? []),
- ...list.map((spec) => ({ spec, source, scope: hit })),
- ])
- result.plugin = plugins.map((item) => item.spec)
- result.plugin_origins = plugins
- })
+ const mergePluginOrigins = Effect.fnUntraced(function* (
+ source: string,
+ // mergePluginOrigins receives raw Specs from one config source, before provenance for this merge step
+ // is attached.
+ list: ConfigPlugin.Spec[] | undefined,
+ // Scope can be inferred from the source path, but some callers already know whether the config should
+ // behave as global or local and can pass that explicitly.
+ kind?: ConfigPlugin.Scope,
+ ) {
+ if (!list?.length) return
+ const hit = kind ?? (yield* pluginScopeForSource(source))
+ // Merge newly seen plugin origins with previously collected ones, then dedupe by plugin identity while
+ // keeping the winning source/scope metadata for downstream installs, writes, and diagnostics.
+ const plugins = ConfigPlugin.deduplicatePluginOrigins([
+ ...(result.plugin_origins ?? []),
+ ...list.map((spec) => ({ spec, source, scope: hit })),
+ ])
+ result.plugin = plugins.map((item) => item.spec)
+ result.plugin_origins = plugins
+ })
- const merge = (source: string, next: Info, kind?: ConfigPlugin.Scope) => {
- result = mergeConfigConcatArrays(result, next)
- return mergePluginOrigins(source, next.plugin, kind)
- }
+ const merge = (source: string, next: Info, kind?: ConfigPlugin.Scope) => {
+ result = mergeConfigConcatArrays(result, next)
+ return mergePluginOrigins(source, next.plugin, kind)
+ }
- 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 = yield* Effect.promise(() => fetch(`${url}/.well-known/opencode`))
- if (!response.ok) {
- throw new Error(`failed to fetch remote config from ${url}: ${response.status}`)
+ 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 = yield* Effect.promise(() => fetch(`${url}/.well-known/opencode`))
+ if (!response.ok) {
+ throw new Error(`failed to fetch remote config from ${url}: ${response.status}`)
+ }
+ const wellknown = (yield* Effect.promise(() => response.json())) as { config?: Record<string, unknown> }
+ const remoteConfig = wellknown.config ?? {}
+ if (!remoteConfig.$schema) remoteConfig.$schema = "https://opencode.ai/config.json"
+ const source = `${url}/.well-known/opencode`
+ const next = yield* loadConfig(JSON.stringify(remoteConfig), {
+ dir: path.dirname(source),
+ source,
+ })
+ yield* merge(source, next, "global")
+ log.debug("loaded remote config from well-known", { url })
}
- const wellknown = (yield* Effect.promise(() => response.json())) as { config?: Record<string, unknown> }
- const remoteConfig = wellknown.config ?? {}
- if (!remoteConfig.$schema) remoteConfig.$schema = "https://opencode.ai/config.json"
- const source = `${url}/.well-known/opencode`
- const next = yield* loadConfig(JSON.stringify(remoteConfig), {
- dir: path.dirname(source),
- source,
- })
- yield* merge(source, next, "global")
- log.debug("loaded remote config from well-known", { url })
}
- }
-
- const global = yield* getGlobal()
- yield* merge(Global.Path.config, global, "global")
- if (Flag.OPENCODE_CONFIG) {
- yield* merge(Flag.OPENCODE_CONFIG, yield* loadFile(Flag.OPENCODE_CONFIG))
- log.debug("loaded custom config", { path: Flag.OPENCODE_CONFIG })
- }
+ const global = yield* getGlobal()
+ yield* merge(Global.Path.config, global, "global")
- if (!Flag.OPENCODE_DISABLE_PROJECT_CONFIG) {
- for (const file of yield* Effect.promise(() =>
- ConfigPaths.projectFiles("opencode", ctx.directory, ctx.worktree),
- )) {
- yield* merge(file, yield* loadFile(file), "local")
+ if (Flag.OPENCODE_CONFIG) {
+ yield* merge(Flag.OPENCODE_CONFIG, yield* loadFile(Flag.OPENCODE_CONFIG))
+ log.debug("loaded custom config", { path: Flag.OPENCODE_CONFIG })
}
- }
- result.agent = result.agent || {}
- result.mode = result.mode || {}
- result.plugin = result.plugin || []
+ if (!Flag.OPENCODE_DISABLE_PROJECT_CONFIG) {
+ for (const file of yield* ConfigPaths.files("opencode", ctx.directory, ctx.worktree).pipe(Effect.orDie)) {
+ yield* merge(file, yield* loadFile(file), "local")
+ }
+ }
- const directories = yield* Effect.promise(() => ConfigPaths.directories(ctx.directory, ctx.worktree))
+ result.agent = result.agent || {}
+ result.mode = result.mode || {}
+ result.plugin = result.plugin || []
- if (Flag.OPENCODE_CONFIG_DIR) {
- log.debug("loading config from OPENCODE_CONFIG_DIR", { path: Flag.OPENCODE_CONFIG_DIR })
- }
+ const directories = yield* ConfigPaths.directories(ctx.directory, ctx.worktree)
- const deps: Fiber.Fiber<void, never>[] = []
+ if (Flag.OPENCODE_CONFIG_DIR) {
+ log.debug("loading config from OPENCODE_CONFIG_DIR", { path: Flag.OPENCODE_CONFIG_DIR })
+ }
- 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)
- log.debug(`loading config from ${source}`)
- yield* merge(source, yield* loadFile(source))
- result.agent ??= {}
- result.mode ??= {}
- result.plugin ??= []
+ const deps: Fiber.Fiber<void, never>[] = []
+
+ 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)
+ log.debug(`loading config from ${source}`)
+ yield* merge(source, yield* loadFile(source))
+ result.agent ??= {}
+ result.mode ??= {}
+ result.plugin ??= []
+ }
}
- }
- yield* ensureGitignore(dir).pipe(Effect.orDie)
+ yield* ensureGitignore(dir).pipe(Effect.orDie)
+
+ const dep = yield* npmSvc
+ .install(dir, {
+ add: ["@opencode-ai/plugin" + (InstallationLocal ? "" : "@" + InstallationVersion)],
+ })
+ .pipe(
+ Effect.exit,
+ Effect.tap((exit) =>
+ Exit.isFailure(exit)
+ ? Effect.sync(() => {
+ log.warn("background dependency install failed", { dir, error: String(exit.cause) })
+ })
+ : Effect.void,
+ ),
+ Effect.asVoid,
+ Effect.forkDetach,
+ )
+ deps.push(dep)
+
+ result.command = mergeDeep(result.command ?? {}, yield* Effect.promise(() => ConfigCommand.load(dir)))
+ result.agent = mergeDeep(result.agent ?? {}, yield* Effect.promise(() => ConfigAgent.load(dir)))
+ result.agent = mergeDeep(result.agent ?? {}, yield* Effect.promise(() => ConfigAgent.loadMode(dir)))
+ // Auto-discovered plugins under `.opencode/plugin(s)` are already local files, so ConfigPlugin.load
+ // returns normalized Specs and we only need to attach origin metadata here.
+ const list = yield* Effect.promise(() => ConfigPlugin.load(dir))
+ yield* mergePluginOrigins(dir, list)
+ }
- const dep = yield* npmSvc
- .install(dir, {
- add: ["@opencode-ai/plugin" + (InstallationLocal ? "" : "@" + InstallationVersion)],
+ if (process.env.OPENCODE_CONFIG_CONTENT) {
+ const source = "OPENCODE_CONFIG_CONTENT"
+ const next = yield* loadConfig(process.env.OPENCODE_CONFIG_CONTENT, {
+ dir: ctx.directory,
+ source,
})
- .pipe(
- Effect.exit,
- Effect.tap((exit) =>
- Exit.isFailure(exit)
- ? Effect.sync(() => {
- log.warn("background dependency install failed", { dir, error: String(exit.cause) })
- })
- : Effect.void,
- ),
- Effect.asVoid,
- Effect.forkDetach,
- )
- deps.push(dep)
-
- result.command = mergeDeep(result.command ?? {}, yield* Effect.promise(() => ConfigCommand.load(dir)))
- result.agent = mergeDeep(result.agent ?? {}, yield* Effect.promise(() => ConfigAgent.load(dir)))
- result.agent = mergeDeep(result.agent ?? {}, yield* Effect.promise(() => ConfigAgent.loadMode(dir)))
- // Auto-discovered plugins under `.opencode/plugin(s)` are already local files, so ConfigPlugin.load
- // returns normalized Specs and we only need to attach origin metadata here.
- const list = yield* Effect.promise(() => ConfigPlugin.load(dir))
- yield* mergePluginOrigins(dir, list)
- }
+ yield* merge(source, next, "local")
+ log.debug("loaded custom config from OPENCODE_CONFIG_CONTENT")
+ }
- if (process.env.OPENCODE_CONFIG_CONTENT) {
- const source = "OPENCODE_CONFIG_CONTENT"
- const next = yield* loadConfig(process.env.OPENCODE_CONFIG_CONTENT, {
- dir: ctx.directory,
- source,
- })
- yield* merge(source, next, "local")
- log.debug("loaded custom config from OPENCODE_CONFIG_CONTENT")
- }
+ const activeAccount = Option.getOrUndefined(
+ yield* accountSvc.active().pipe(Effect.catch(() => Effect.succeed(Option.none()))),
+ )
+ if (activeAccount?.active_org_id) {
+ const accountID = activeAccount.id
+ const orgID = activeAccount.active_org_id
+ const url = activeAccount.url
+ yield* Effect.gen(function* () {
+ const [configOpt, tokenOpt] = yield* Effect.all(
+ [accountSvc.config(accountID, orgID), accountSvc.token(accountID)],
+ { concurrency: 2 },
+ )
+ if (Option.isSome(tokenOpt)) {
+ process.env["OPENCODE_CONSOLE_TOKEN"] = tokenOpt.value
+ yield* env.set("OPENCODE_CONSOLE_TOKEN", tokenOpt.value)
+ }
- const activeAccount = Option.getOrUndefined(
- yield* accountSvc.active().pipe(Effect.catch(() => Effect.succeed(Option.none()))),
- )
- if (activeAccount?.active_org_id) {
- const accountID = activeAccount.id
- const orgID = activeAccount.active_org_id
- const url = activeAccount.url
- yield* Effect.gen(function* () {
- const [configOpt, tokenOpt] = yield* Effect.all(
- [accountSvc.config(accountID, orgID), accountSvc.token(accountID)],
- { concurrency: 2 },
+ if (Option.isSome(configOpt)) {
+ const source = `${url}/api/config`
+ const next = yield* loadConfig(JSON.stringify(configOpt.value), {
+ dir: path.dirname(source),
+ source,
+ })
+ for (const providerID of Object.keys(next.provider ?? {})) {
+ consoleManagedProviders.add(providerID)
+ }
+ yield* merge(source, next, "global")
+ }
+ }).pipe(
+ Effect.withSpan("Config.loadActiveOrgConfig"),
+ Effect.catch((err) => {
+ log.debug("failed to fetch remote account config", {
+ error: err instanceof Error ? err.message : String(err),
+ })
+ return Effect.void
+ }),
)
- if (Option.isSome(tokenOpt)) {
- process.env["OPENCODE_CONSOLE_TOKEN"] = tokenOpt.value
- yield* env.set("OPENCODE_CONSOLE_TOKEN", tokenOpt.value)
- }
+ }
- if (Option.isSome(configOpt)) {
- const source = `${url}/api/config`
- const next = yield* loadConfig(JSON.stringify(configOpt.value), {
- dir: path.dirname(source),
- source,
- })
- for (const providerID of Object.keys(next.provider ?? {})) {
- consoleManagedProviders.add(providerID)
- }
- yield* merge(source, next, "global")
+ const managedDir = ConfigManaged.managedConfigDir()
+ if (existsSync(managedDir)) {
+ for (const file of ["opencode.json", "opencode.jsonc"]) {
+ const source = path.join(managedDir, file)
+ yield* merge(source, yield* loadFile(source), "global")
}
- }).pipe(
- Effect.withSpan("Config.loadActiveOrgConfig"),
- Effect.catch((err) => {
- log.debug("failed to fetch remote account config", {
- error: err instanceof Error ? err.message : String(err),
- })
- return Effect.void
- }),
- )
- }
-
- const managedDir = ConfigManaged.managedConfigDir()
- if (existsSync(managedDir)) {
- for (const file of ["opencode.json", "opencode.jsonc"]) {
- const source = path.join(managedDir, file)
- yield* merge(source, yield* loadFile(source), "global")
}
- }
- // macOS managed preferences (.mobileconfig deployed via MDM) override everything
- const managed = yield* Effect.promise(() => ConfigManaged.readManagedPreferences())
- if (managed) {
- result = mergeConfigConcatArrays(
- result,
- yield* loadConfig(managed.text, {
- dir: path.dirname(managed.source),
- source: managed.source,
- }),
- )
- }
+ // macOS managed preferences (.mobileconfig deployed via MDM) override everything
+ const managed = yield* Effect.promise(() => ConfigManaged.readManagedPreferences())
+ if (managed) {
+ result = mergeConfigConcatArrays(
+ result,
+ yield* loadConfig(managed.text, {
+ dir: path.dirname(managed.source),
+ source: managed.source,
+ }),
+ )
+ }
- for (const [name, mode] of Object.entries(result.mode ?? {})) {
- result.agent = mergeDeep(result.agent ?? {}, {
- [name]: {
- ...mode,
- mode: "primary" as const,
- },
- })
- }
+ for (const [name, mode] of Object.entries(result.mode ?? {})) {
+ result.agent = mergeDeep(result.agent ?? {}, {
+ [name]: {
+ ...mode,
+ mode: "primary" as const,
+ },
+ })
+ }
- if (Flag.OPENCODE_PERMISSION) {
- result.permission = mergeDeep(result.permission ?? {}, JSON.parse(Flag.OPENCODE_PERMISSION))
- }
+ if (Flag.OPENCODE_PERMISSION) {
+ result.permission = mergeDeep(result.permission ?? {}, JSON.parse(Flag.OPENCODE_PERMISSION))
+ }
- if (result.tools) {
- const perms: Record<string, ConfigPermission.Action> = {}
- for (const [tool, enabled] of Object.entries(result.tools)) {
- const action: ConfigPermission.Action = enabled ? "allow" : "deny"
- if (tool === "write" || tool === "edit" || tool === "patch" || tool === "multiedit") {
- perms.edit = action
- continue
+ if (result.tools) {
+ const perms: Record<string, ConfigPermission.Action> = {}
+ for (const [tool, enabled] of Object.entries(result.tools)) {
+ const action: ConfigPermission.Action = enabled ? "allow" : "deny"
+ if (tool === "write" || tool === "edit" || tool === "patch" || tool === "multiedit") {
+ perms.edit = action
+ continue
+ }
+ perms[tool] = action
}
- perms[tool] = action
+ result.permission = mergeDeep(perms, result.permission ?? {})
}
- result.permission = mergeDeep(perms, result.permission ?? {})
- }
- if (!result.username) result.username = os.userInfo().username
+ if (!result.username) result.username = os.userInfo().username
- if (result.autoshare === true && !result.share) {
- result.share = "auto"
- }
+ if (result.autoshare === true && !result.share) {
+ result.share = "auto"
+ }
- if (Flag.OPENCODE_DISABLE_AUTOCOMPACT) {
- result.compaction = { ...result.compaction, auto: false }
- }
- if (Flag.OPENCODE_DISABLE_PRUNE) {
- result.compaction = { ...result.compaction, prune: false }
- }
+ if (Flag.OPENCODE_DISABLE_AUTOCOMPACT) {
+ result.compaction = { ...result.compaction, auto: false }
+ }
+ if (Flag.OPENCODE_DISABLE_PRUNE) {
+ result.compaction = { ...result.compaction, prune: false }
+ }
- return {
- config: result,
- directories,
- deps,
- consoleState: {
- consoleManagedProviders: Array.from(consoleManagedProviders),
- activeOrgName,
- switchableOrgCount: 0,
- },
- }
- })
+ return {
+ config: result,
+ directories,
+ deps,
+ consoleState: {
+ consoleManagedProviders: Array.from(consoleManagedProviders),
+ activeOrgName,
+ switchableOrgCount: 0,
+ },
+ }
+ },
+ Effect.provideService(AppFileSystem.Service, fs),
+ )
const state = yield* InstanceState.make<State>(
Effect.fn("Config.state")(function* (ctx) {
- return yield* loadInstanceState(ctx)
+ return yield* loadInstanceState(ctx).pipe(Effect.orDie)
}),
)
diff --git a/packages/opencode/src/config/paths.ts b/packages/opencode/src/config/paths.ts
index dcf0c940f..db4b914f7 100644
--- a/packages/opencode/src/config/paths.ts
+++ b/packages/opencode/src/config/paths.ts
@@ -6,33 +6,41 @@ import { Flag } from "@/flag/flag"
import { Global } from "@/global"
import { unique } from "remeda"
import { JsonError } from "./error"
+import * as Effect from "effect/Effect"
+import { AppFileSystem } from "@opencode-ai/shared/filesystem"
-export async function projectFiles(name: string, directory: string, worktree?: string) {
- return Filesystem.findUp([`${name}.json`, `${name}.jsonc`], directory, worktree, { rootFirst: true })
-}
+export const files = Effect.fn("ConfigPaths.projectFiles")(function* (
+ name: string,
+ directory: string,
+ worktree?: string,
+) {
+ const afs = yield* AppFileSystem.Service
+ return (yield* afs.up({
+ targets: [`${name}.jsonc`, `${name}.json`],
+ start: directory,
+ stop: worktree,
+ })).toReversed()
+})
-export async function directories(directory: string, worktree?: string) {
+export const directories = Effect.fn("ConfigPaths.directories")(function* (directory: string, worktree?: string) {
+ const afs = yield* AppFileSystem.Service
return unique([
Global.Path.config,
...(!Flag.OPENCODE_DISABLE_PROJECT_CONFIG
- ? await Array.fromAsync(
- Filesystem.up({
- targets: [".opencode"],
- start: directory,
- stop: worktree,
- }),
- )
+ ? yield* afs.up({
+ targets: [".opencode"],
+ start: directory,
+ stop: worktree,
+ })
: []),
- ...(await Array.fromAsync(
- Filesystem.up({
- targets: [".opencode"],
- start: Global.Path.home,
- stop: Global.Path.home,
- }),
- )),
+ ...(yield* afs.up({
+ targets: [".opencode"],
+ start: Global.Path.home,
+ stop: Global.Path.home,
+ })),
...(Flag.OPENCODE_CONFIG_DIR ? [Flag.OPENCODE_CONFIG_DIR] : []),
])
-}
+})
export function fileInDirectory(dir: string, name: string) {
return [path.join(dir, `${name}.json`), path.join(dir, `${name}.jsonc`)]