diff --git a/packages/core/src/config.ts b/packages/core/src/config.ts index e6cd3c5a16..f289a68e2f 100644 --- a/packages/core/src/config.ts +++ b/packages/core/src/config.ts @@ -139,7 +139,7 @@ export const layer = Layer.effect( const global = yield* Global.Service const location = yield* Location.Service const policy = yield* Policy.Service - const names = ["config.json", "opencode.json", "opencode.jsonc"] + const names = ["opencode.json", "opencode.jsonc"] const decodeOptions = { errors: "all", onExcessProperty: "ignore", propertyOrder: "original" } as const const decodeInfo = Schema.decodeUnknownOption(Info, decodeOptions) const decodeV1Info = Schema.decodeUnknownOption(ConfigV1.Info, decodeOptions) diff --git a/packages/core/test/config/config.test.ts b/packages/core/test/config/config.test.ts index a9e5cfc3d7..c3c42cab30 100644 --- a/packages/core/test/config/config.test.ts +++ b/packages/core/test/config/config.test.ts @@ -161,7 +161,7 @@ describe("Config", () => { ), ) - it.live("loads JSON and JSONC files from lowest to highest priority", () => + it.live("loads opencode JSON and JSONC files from lowest to highest priority", () => Effect.acquireRelease( Effect.promise(() => tmpdir()), (tmp) => Effect.promise(() => tmp[Symbol.asyncDispose]()), @@ -170,13 +170,9 @@ describe("Config", () => { Effect.gen(function* () { yield* Effect.promise(() => Promise.all([ - fs.writeFile( - path.join(tmp.path, "config.json"), - JSON.stringify({ $schema: "base", providers: { base: provider } }), - ), fs.writeFile( path.join(tmp.path, "opencode.json"), - JSON.stringify({ $schema: "middle", providers: { middle: provider } }), + JSON.stringify({ $schema: "base", providers: { base: provider } }), ), fs.writeFile( path.join(tmp.path, "opencode.jsonc"), @@ -192,12 +188,12 @@ describe("Config", () => { const config = yield* Config.Service const documents = (yield* config.entries()).filter((entry) => entry.type === "document") - expect(documents).toHaveLength(3) - expect(documents.map((document) => document.type)).toEqual(["document", "document", "document"]) - expect(documents.map((document) => document.info.$schema)).toEqual(["base", "middle", "last"]) + expect(documents).toHaveLength(2) + expect(documents.map((document) => document.type)).toEqual(["document", "document"]) + expect(documents.map((document) => document.info.$schema)).toEqual(["base", "last"]) expect(documents[0]).toBeInstanceOf(Config.Document) - expect(documents[0]?.path).toBe(path.join(tmp.path, "config.json")) - expect(documents[2]?.info.providers?.last).toBeInstanceOf(ConfigProvider.Info) + expect(documents[0]?.path).toBe(path.join(tmp.path, "opencode.json")) + expect(documents[1]?.info.providers?.last).toBeInstanceOf(ConfigProvider.Info) yield* Effect.promise(() => fs.writeFile(path.join(tmp.path, "opencode.jsonc"), JSON.stringify({ $schema: "changed" })), @@ -206,7 +202,29 @@ describe("Config", () => { (yield* config.entries()) .filter((entry) => entry.type === "document") .map((document) => document.info.$schema), - ).toEqual(["base", "middle", "last"]) + ).toEqual(["base", "last"]) + }).pipe(Effect.provide(testLayer(tmp.path))) + }), + ), + ), + ) + + it.live("does not load legacy config.json files", () => + Effect.acquireRelease( + Effect.promise(() => tmpdir()), + (tmp) => Effect.promise(() => tmp[Symbol.asyncDispose]()), + ).pipe( + Effect.flatMap((tmp) => + Effect.gen(function* () { + yield* Effect.promise(() => + fs.writeFile(path.join(tmp.path, "config.json"), JSON.stringify({ $schema: "legacy" })), + ) + + return yield* Effect.gen(function* () { + const config = yield* Config.Service + const documents = (yield* config.entries()).filter((entry) => entry.type === "document") + + expect(documents).toHaveLength(0) }).pipe(Effect.provide(testLayer(tmp.path))) }), ), @@ -648,7 +666,7 @@ describe("Config", () => { ), ) - it.live("ignores invalid files while loading valid config values", () => + it.live("ignores an invalid file while loading valid config values", () => Effect.acquireRelease( Effect.promise(() => tmpdir()), (tmp) => Effect.promise(() => tmp[Symbol.asyncDispose]()), @@ -657,9 +675,8 @@ describe("Config", () => { Effect.gen(function* () { yield* Effect.promise(() => Promise.all([ - fs.writeFile(path.join(tmp.path, "config.json"), JSON.stringify({ $schema: "base" })), - fs.writeFile(path.join(tmp.path, "opencode.json"), "{ invalid"), - fs.writeFile(path.join(tmp.path, "opencode.jsonc"), JSON.stringify({ providers: { invalid: true } })), + fs.writeFile(path.join(tmp.path, "opencode.json"), JSON.stringify({ $schema: "base" })), + fs.writeFile(path.join(tmp.path, "opencode.jsonc"), "{ invalid"), ]), ) return yield* Effect.gen(function* () { @@ -728,7 +745,7 @@ describe("Config", () => { fs.writeFile(path.join(global, "opencode.json"), JSON.stringify({ $schema: "global" })), fs.writeFile(path.join(root, "opencode.json"), JSON.stringify({ $schema: "root" })), fs.writeFile(path.join(parent, "opencode.jsonc"), JSON.stringify({ $schema: "parent" })), - fs.writeFile(path.join(directory, "config.json"), JSON.stringify({ $schema: "directory" })), + fs.writeFile(path.join(directory, "opencode.json"), JSON.stringify({ $schema: "directory" })), fs.writeFile(path.join(root, ".opencode", "opencode.json"), JSON.stringify({ $schema: "root-dot" })), fs.writeFile( path.join(directory, ".opencode", "opencode.jsonc"), diff --git a/packages/core/test/config/plugin.test.ts b/packages/core/test/config/plugin.test.ts index e6944368c4..2e134c55e1 100644 --- a/packages/core/test/config/plugin.test.ts +++ b/packages/core/test/config/plugin.test.ts @@ -25,7 +25,7 @@ describe("ConfigExternalPlugin", () => { const location = yield* Location.Service const npm = yield* Npm.Service const host = yield* PluginHost.make(plugins) - const document = path.join(import.meta.dir, "config.json") + const document = path.join(import.meta.dir, "opencode.json") yield* ConfigExternalPlugin.Plugin.effect(host).pipe( Effect.provideService(PluginV2.Service, plugins), @@ -82,7 +82,7 @@ describe("ConfigExternalPlugin", () => { Effect.succeed([ new Config.Document({ type: "document", - path: path.join(import.meta.dir, "config.json"), + path: path.join(import.meta.dir, "opencode.json"), info: decode({ plugins: [ { @@ -125,7 +125,7 @@ describe("ConfigExternalPlugin", () => { Effect.succeed([ new Config.Document({ type: "document", - path: path.join(import.meta.dir, "config.json"), + path: path.join(import.meta.dir, "opencode.json"), info: decode({ plugins: [ "../plugin/fixtures/missing-plugin.ts", diff --git a/specs/v2/config.md b/specs/v2/config.md index 9254af2094..96b0f3ac90 100644 --- a/specs/v2/config.md +++ b/specs/v2/config.md @@ -13,6 +13,8 @@ This document breaks the legacy configuration schema into small review groups. W Use one v2 config schema for now. Some fields, such as `autoupdate`, are intended for global/user configuration, but there is not yet enough benefit to enforce that with separate global and location schemas. Revisit this if more scope-sensitive fields survive the review. +V2 core discovers config documents named `opencode.json` or `opencode.jsonc` in the global config directory, ancestor project directories, and `.opencode` config directories. The legacy `config.json` filename is not supported in V2. + ## Group 1: File Metadata Small fields describing the config file itself rather than application behavior.