mirror of
https://gitee.com/zhangsken/opencode.git
synced 2026-10-16 10:30:01 +08:00
feat(cli): support inline config content (#49399)
This commit is contained in:
@@ -20,7 +20,7 @@ export interface Interface {
|
||||
export class Service extends Context.Service<Service, Interface>()("@opencode/cli/config/Config") {}
|
||||
|
||||
const decode = Schema.decodeUnknownOption(Info)
|
||||
const decodeRecord = Schema.decodeUnknownOption(Schema.Record(Schema.String, Schema.Any))
|
||||
const decodeRecord = Schema.decodeUnknownOption(Schema.Record(Schema.String, Schema.Unknown))
|
||||
const empty: Info = {}
|
||||
|
||||
export const layer = Layer.effect(
|
||||
@@ -29,14 +29,14 @@ export const layer = Layer.effect(
|
||||
const fs = yield* FileSystem.FileSystem
|
||||
const global = yield* Global.Service
|
||||
const file = path.join(global.config, "cli.json")
|
||||
const content = process.env.OPENCODE_CLI_CONFIG_CONTENT
|
||||
? Option.getOrUndefined(decode(parseRecord(process.env.OPENCODE_CLI_CONFIG_CONTENT)))
|
||||
: undefined
|
||||
|
||||
const readJson = Effect.fnUntraced(function* () {
|
||||
const text = yield* fs.readFileString(file).pipe(Effect.orElseSucceed(() => undefined))
|
||||
if (text === undefined) return undefined
|
||||
const errors: ParseError[] = []
|
||||
const value: any = parse(text, errors, { allowTrailingComma: true })
|
||||
if (errors.length) return undefined
|
||||
return Option.getOrUndefined(decodeRecord(value))
|
||||
return parseRecord(text)
|
||||
})
|
||||
|
||||
const write = Effect.fnUntraced(function* (text: string) {
|
||||
@@ -61,6 +61,9 @@ export const layer = Layer.effect(
|
||||
}),
|
||||
),
|
||||
)
|
||||
const load = Effect.fnUntraced(function* (migration?: Info) {
|
||||
return merge(migration ?? Option.getOrUndefined(decode(yield* readJson())), content)
|
||||
})
|
||||
|
||||
const get = Effect.fn("cli.config.get")(() =>
|
||||
withLock(
|
||||
@@ -72,8 +75,7 @@ export const layer = Layer.effect(
|
||||
)
|
||||
if (migration?.cause)
|
||||
yield* Effect.logWarning("failed to persist migrated cli config", { cause: migration.cause })
|
||||
if (migration?.info) return migration.info
|
||||
return Option.getOrElse(decode(yield* readJson()), () => empty)
|
||||
return yield* load(migration?.info)
|
||||
}),
|
||||
),
|
||||
)
|
||||
@@ -83,7 +85,7 @@ export const layer = Layer.effect(
|
||||
Effect.gen(function* () {
|
||||
const migration = yield* migrate
|
||||
if (migration?.cause) return yield* Effect.failCause(migration.cause)
|
||||
const current = migration?.info ?? Option.getOrElse(decode(yield* readJson()), () => empty)
|
||||
const current = yield* load(migration?.info)
|
||||
const next = produce(current, update)
|
||||
const edits = changes(current, next)
|
||||
if (!edits.length) return current
|
||||
@@ -102,7 +104,7 @@ export const layer = Layer.effect(
|
||||
const config = Option.getOrUndefined(decode(parse(updated, errors, { allowTrailingComma: true })))
|
||||
if (errors.length || config === undefined) return yield* Effect.fail(new Error("Invalid CLI config update"))
|
||||
yield* write(updated.endsWith("\n") ? updated : updated + "\n")
|
||||
return config
|
||||
return merge(config, content)
|
||||
}),
|
||||
).pipe(Effect.mapError((cause) => new Error("Failed to update CLI config", { cause }))),
|
||||
)
|
||||
@@ -113,6 +115,39 @@ export const layer = Layer.effect(
|
||||
|
||||
type Edit = { readonly path: (string | number)[]; readonly value: any }
|
||||
|
||||
function merge(...values: readonly (Info | undefined)[]) {
|
||||
return Option.getOrElse(
|
||||
decode(
|
||||
values.reduce<Record<string, unknown>>(
|
||||
(result, value) => mergeRecords(result, value ?? {}),
|
||||
{},
|
||||
),
|
||||
),
|
||||
() => empty,
|
||||
)
|
||||
}
|
||||
|
||||
function mergeRecords(base: object, overlay: object) {
|
||||
return Object.entries(overlay).reduce<Record<string, unknown>>(
|
||||
(result, [key, value]) => {
|
||||
result[key] = isRecord(result[key]) && isRecord(value) ? mergeRecords(result[key], value) : value
|
||||
return result
|
||||
},
|
||||
{ ...base },
|
||||
)
|
||||
}
|
||||
|
||||
function isRecord(value: unknown): value is Record<string, unknown> {
|
||||
return typeof value === "object" && value !== null && !Array.isArray(value)
|
||||
}
|
||||
|
||||
function parseRecord(text: string) {
|
||||
const errors: ParseError[] = []
|
||||
const value: unknown = parse(text, errors, { allowTrailingComma: true })
|
||||
if (errors.length) return undefined
|
||||
return Option.getOrUndefined(decodeRecord(value))
|
||||
}
|
||||
|
||||
function changes(before: any, after: any, path: (string | number)[] = []): Edit[] {
|
||||
if (Object.is(before, after)) return []
|
||||
if (
|
||||
|
||||
@@ -71,6 +71,51 @@ test("preserves the schema in an existing cli.json", async () => {
|
||||
expect(await Bun.file(file).json()).toEqual(config)
|
||||
})
|
||||
|
||||
test("merges inline CLI config content over the global config", async () => {
|
||||
await using directory = await tmpdir()
|
||||
const file = path.join(directory.path, "cli.json")
|
||||
const previous = process.env.OPENCODE_CLI_CONFIG_CONTENT
|
||||
await Bun.write(
|
||||
file,
|
||||
JSON.stringify({
|
||||
tabs: { enabled: true, scope: "global" },
|
||||
keybinds: { "app.exit": "ctrl+q" },
|
||||
plugins: ["global"],
|
||||
animations: true,
|
||||
}),
|
||||
)
|
||||
process.env.OPENCODE_CLI_CONFIG_CONTENT = JSON.stringify({
|
||||
tabs: { enabled: false },
|
||||
keybinds: { "help.show": false },
|
||||
plugins: ["inline"],
|
||||
animations: false,
|
||||
})
|
||||
|
||||
try {
|
||||
const result = await run(
|
||||
directory.path,
|
||||
Effect.gen(function* () {
|
||||
const service = yield* Config.Service
|
||||
const loaded = yield* service.get()
|
||||
const updated = yield* service.update((draft) => {
|
||||
draft.animations = true
|
||||
draft.mouse = false
|
||||
})
|
||||
return { loaded, updated }
|
||||
}),
|
||||
)
|
||||
|
||||
expect(result.loaded.tabs).toEqual({ enabled: false, scope: "global" })
|
||||
expect(result.loaded.keybinds).toEqual({ "app.exit": "ctrl+q", "help.show": false })
|
||||
expect(result.loaded.plugins).toEqual(["inline"])
|
||||
expect(result.updated).toMatchObject({ animations: false, mouse: false })
|
||||
expect(await Bun.file(file).json()).toMatchObject({ animations: true, mouse: false })
|
||||
} finally {
|
||||
if (previous === undefined) delete process.env.OPENCODE_CLI_CONFIG_CONTENT
|
||||
else process.env.OPENCODE_CLI_CONFIG_CONTENT = previous
|
||||
}
|
||||
})
|
||||
|
||||
test("migrates tui and kv config into cli.json", async () => {
|
||||
await using directory = await tmpdir()
|
||||
await Bun.write(
|
||||
|
||||
@@ -4,6 +4,7 @@ export function isolatedEnv(root: string, overrides: Record<string, string | und
|
||||
return {
|
||||
...process.env,
|
||||
HOME: root,
|
||||
OPENCODE_CLI_CONFIG_CONTENT: undefined,
|
||||
OPENCODE_CONFIG_CONTENT: "{}",
|
||||
OPENCODE_CONFIG_DIR: path.join(root, "config"),
|
||||
OPENCODE_DB: path.join(root, "opencode.db"),
|
||||
|
||||
@@ -42,8 +42,9 @@ that section.
|
||||
CLI and TUI preferences are separate from OpenCode's server and project
|
||||
configuration. They live in the global `~/.config/opencode/cli.json`, or
|
||||
`$XDG_CONFIG_HOME/opencode/cli.json` when `XDG_CONFIG_HOME` is set. There is no
|
||||
project-local CLI configuration. Most preferences can also be changed from the
|
||||
TUI by pressing `Ctrl+P` and selecting **Open settings**.
|
||||
project-local CLI configuration. Set `OPENCODE_CLI_CONFIG_CONTENT` to merge
|
||||
inline JSON over the global settings. Most preferences can also be changed from
|
||||
the TUI by pressing `Ctrl+P` and selecting **Open settings**.
|
||||
|
||||
Fetch the full [CLI configuration guide](https://opencode.ai/v2/docs/cli/config)
|
||||
before editing `cli.json`. It covers terminal-only settings such as themes,
|
||||
|
||||
@@ -28,6 +28,17 @@ Add the published JSON Schema for editor validation and autocomplete:
|
||||
|
||||
OpenCode includes this automatically when it creates or migrates your CLI config.
|
||||
|
||||
## Inline config
|
||||
|
||||
Set `OPENCODE_CLI_CONFIG_CONTENT` to apply CLI settings from inline JSON:
|
||||
|
||||
```sh
|
||||
OPENCODE_CLI_CONFIG_CONTENT='{"tabs":{"enabled":false}}' opencode
|
||||
```
|
||||
|
||||
OpenCode merges the inline settings over the global `cli.json`. Nested objects are merged, while arrays and scalar
|
||||
values from the environment replace global values. Inline settings remain authoritative while the variable is set.
|
||||
|
||||
## Theme
|
||||
|
||||
Set the theme and color mode:
|
||||
|
||||
Reference in New Issue
Block a user