diff --git a/packages/opencode/src/config/config.ts b/packages/opencode/src/config/config.ts index 4b10665aca9..e44405f42e4 100644 --- a/packages/opencode/src/config/config.ts +++ b/packages/opencode/src/config/config.ts @@ -409,6 +409,16 @@ export const layer = Layer.effect( const loadGlobal = Effect.fnUntraced(function* () { let result: Info = {} + // Seed the default global config with the schema for editor completion, but avoid writing when the user + // explicitly routes config through env-provided paths or content. + if (!Flag.OPENCODE_CONFIG && !Flag.OPENCODE_CONFIG_DIR && !Flag.OPENCODE_CONFIG_CONTENT) { + const file = globalConfigFile() + if (!existsSync(file)) { + yield* fs + .writeWithDirs(file, JSON.stringify({ $schema: "https://opencode.ai/config.json" }, null, 2)) + .pipe(Effect.catch(() => Effect.void)) + } + } result = mergeConfig(result, yield* loadFile(path.join(Global.Path.config, "config.json"))) result = mergeConfig(result, yield* loadFile(path.join(Global.Path.config, "opencode.json"))) result = mergeConfig(result, yield* loadFile(path.join(Global.Path.config, "opencode.jsonc"))) diff --git a/packages/opencode/test/config/config.test.ts b/packages/opencode/test/config/config.test.ts index fa9fd332e83..90e78efcdba 100644 --- a/packages/opencode/test/config/config.test.ts +++ b/packages/opencode/test/config/config.test.ts @@ -141,6 +141,54 @@ test("loads config with defaults when no files exist", async () => { }) }) +test("creates global jsonc config with schema when no global configs exist", async () => { + await using tmp = await tmpdir() + const prev = Global.Path.config + ;(Global.Path as { config: string }).config = tmp.path + await clear(true) + + try { + await WithInstance.provide({ + directory: tmp.path, + fn: async () => { + await load() + }, + }) + + const content = await Filesystem.readText(path.join(tmp.path, "opencode.jsonc")) + expect(content).toContain('"$schema": "https://opencode.ai/config.json"') + } finally { + ;(Global.Path as { config: string }).config = prev + await clear(true) + } +}) + +test("does not create global config when OPENCODE_CONFIG_DIR is set", async () => { + await using tmp = await tmpdir() + await using custom = await tmpdir() + const prevConfig = Global.Path.config + const prevEnv = process.env.OPENCODE_CONFIG_DIR + ;(Global.Path as { config: string }).config = tmp.path + process.env.OPENCODE_CONFIG_DIR = custom.path + await clear(true) + + try { + await WithInstance.provide({ + directory: tmp.path, + fn: async () => { + await load() + }, + }) + + expect(await Filesystem.exists(path.join(tmp.path, "opencode.jsonc"))).toBe(false) + } finally { + ;(Global.Path as { config: string }).config = prevConfig + if (prevEnv === undefined) delete process.env.OPENCODE_CONFIG_DIR + else process.env.OPENCODE_CONFIG_DIR = prevEnv + await clear(true) + } +}) + test("loads JSON config file", async () => { await using tmp = await tmpdir({ init: async (dir) => {