mirror of
https://gitee.com/zhangsken/opencode.git
synced 2026-10-16 10:30:01 +08:00
feat(cli): publish generated CLI configuration schema (#45070)
Co-authored-by: neriousy <34747899+neriousy@users.noreply.github.com>
This commit is contained in:
committed by
GitHub
parent
db7837814c
commit
d4803ffe38
@@ -0,0 +1,27 @@
|
||||
import { Schema } from "effect"
|
||||
import { format } from "prettier"
|
||||
import { Info, SchemaURL } from "../src/config/schema"
|
||||
|
||||
const target = process.argv[2]
|
||||
if (!target) throw new Error("A schema output path is required")
|
||||
|
||||
const document = Schema.toJsonSchemaDocument(Info)
|
||||
const content = await format(
|
||||
JSON.stringify({
|
||||
$schema: "https://json-schema.org/draft/2020-12/schema",
|
||||
$id: SchemaURL,
|
||||
...document.schema,
|
||||
...(Object.keys(document.definitions).length ? { $defs: document.definitions } : {}),
|
||||
}),
|
||||
{ parser: "json", printWidth: 120 },
|
||||
)
|
||||
|
||||
if (process.argv.includes("--check")) {
|
||||
if ((await Bun.file(target).text()) !== content) {
|
||||
console.error("Generated CLI config schema is stale. Run `bun run generate` from packages/www.")
|
||||
process.exit(1)
|
||||
}
|
||||
process.exit(0)
|
||||
}
|
||||
|
||||
await Bun.write(target, content)
|
||||
@@ -7,7 +7,7 @@ import { produce, type Draft } from "immer"
|
||||
import { applyEdits, modify, parse, type ParseError } from "jsonc-parser"
|
||||
import path from "path"
|
||||
import { ConfigMigration } from "./migrate"
|
||||
import { Info } from "./schema"
|
||||
import { Info, SchemaURL } from "./schema"
|
||||
|
||||
export * from "./schema"
|
||||
|
||||
@@ -87,7 +87,9 @@ export const layer = Layer.effect(
|
||||
const next = produce(current, update)
|
||||
const edits = changes(current, next)
|
||||
if (!edits.length) return current
|
||||
const text = yield* fs.readFileString(file).pipe(Effect.orElseSucceed(() => "{}"))
|
||||
const text = yield* fs
|
||||
.readFileString(file)
|
||||
.pipe(Effect.orElseSucceed(() => JSON.stringify({ $schema: SchemaURL }, null, 2)))
|
||||
const updated = edits.reduce(
|
||||
(text, edit) =>
|
||||
applyEdits(
|
||||
|
||||
@@ -7,7 +7,7 @@ import { Effect, FileSystem, Option, Schema } from "effect"
|
||||
import { randomUUID } from "crypto"
|
||||
import { applyEdits, createScanner, modify, parse, parseTree, type Node, type ParseError } from "jsonc-parser"
|
||||
import path from "path"
|
||||
import { Info } from "./schema"
|
||||
import { Info, SchemaURL } from "./schema"
|
||||
|
||||
const decodeV1 = Schema.decodeUnknownOption(TuiConfigV1.Info)
|
||||
const decodeInfo = Schema.decodeUnknownOption(Info)
|
||||
@@ -100,8 +100,9 @@ export const run = Effect.fn("cli.config.migrate")(function* (input: {
|
||||
const legacyValue = yield* readJson(path.join(input.config, "tui.json"))
|
||||
const legacy = Option.getOrUndefined(decodeV1(legacyValue))
|
||||
const kv = yield* readJson(path.join(input.state, "kv.json"))
|
||||
const migrated = migrateV1(legacy, kv ?? {})
|
||||
if (!Object.keys(migrated).length) return
|
||||
const values = migrateV1(legacy, kv ?? {})
|
||||
if (!Object.keys(values).length) return
|
||||
const migrated = { $schema: SchemaURL, ...values }
|
||||
|
||||
const result = yield* persist(JSON.stringify(migrated, null, 2) + "\n", migrated)
|
||||
if (result.cause === undefined)
|
||||
|
||||
@@ -1,5 +1,10 @@
|
||||
import { Config } from "@opencode-ai/tui/config"
|
||||
import { Schema } from "effect"
|
||||
|
||||
export const Info = Schema.Struct({ ...Config.Info.fields })
|
||||
export const SchemaURL = "https://opencode.ai/v2/cli.json"
|
||||
|
||||
export const Info = Schema.Struct({
|
||||
$schema: Schema.optional(Schema.String).annotate({ description: "JSON Schema for CLI configuration" }),
|
||||
...Config.Info.fields,
|
||||
})
|
||||
export type Info = Schema.Schema.Type<typeof Info>
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { NodeFileSystem } from "@effect/platform-node"
|
||||
import { Flock } from "@opencode-ai/util/flock"
|
||||
import { Global } from "@opencode-ai/util/global"
|
||||
import { Effect, FileSystem, Option } from "effect"
|
||||
import { Effect, FileSystem, Option, Schema } from "effect"
|
||||
import { expect, test } from "bun:test"
|
||||
import { parse } from "jsonc-parser"
|
||||
import path from "path"
|
||||
@@ -17,6 +17,67 @@ function run<A, E>(directory: string, effect: Effect.Effect<A, E, Config.Service
|
||||
)
|
||||
}
|
||||
|
||||
test("generates reusable keybind schemas and preserves descriptions and numeric constraints", () => {
|
||||
const document = Schema.toJsonSchemaDocument(Config.Info)
|
||||
expect(document).toHaveProperty(["definitions", "TuiKeybind.BindingValue"])
|
||||
expect(document).toHaveProperty(["schema", "properties", "keybinds", "anyOf", 0, "properties", "app.exit"], {
|
||||
anyOf: [{ $ref: "#/$defs/TuiKeybind.BindingValue" }, { type: "null" }],
|
||||
description: "Exit the application",
|
||||
})
|
||||
expect(document).toHaveProperty(["schema", "properties", "scroll", "anyOf", 0, "properties", "speed", "anyOf", 0], {
|
||||
type: "number",
|
||||
minimum: 0.001,
|
||||
})
|
||||
expect(document).toHaveProperty(
|
||||
["schema", "properties", "attention", "anyOf", 0, "properties", "volume", "anyOf", 0],
|
||||
{ type: "number", minimum: 0, maximum: 1 },
|
||||
)
|
||||
})
|
||||
|
||||
test("includes the published schema when creating cli.json", async () => {
|
||||
const directory = await Bun.$`mktemp -d`.text().then((value) => value.trim())
|
||||
|
||||
try {
|
||||
const config = await run(
|
||||
directory,
|
||||
Effect.gen(function* () {
|
||||
const service = yield* Config.Service
|
||||
return yield* service.update((draft) => {
|
||||
draft.animations = false
|
||||
})
|
||||
}),
|
||||
)
|
||||
|
||||
expect(config).toEqual({ $schema: "https://opencode.ai/v2/cli.json", animations: false })
|
||||
expect(await Bun.file(path.join(directory, "cli.json")).json()).toEqual(config)
|
||||
} finally {
|
||||
await Bun.$`rm -rf ${directory}`
|
||||
}
|
||||
})
|
||||
|
||||
test("preserves the schema in an existing cli.json", async () => {
|
||||
const directory = await Bun.$`mktemp -d`.text().then((value) => value.trim())
|
||||
const file = path.join(directory, "cli.json")
|
||||
await Bun.write(file, JSON.stringify({ $schema: "https://opencode.ai/v2/cli.json", animations: true }))
|
||||
|
||||
try {
|
||||
const config = await run(
|
||||
directory,
|
||||
Effect.gen(function* () {
|
||||
const service = yield* Config.Service
|
||||
return yield* service.update((draft) => {
|
||||
draft.animations = false
|
||||
})
|
||||
}),
|
||||
)
|
||||
|
||||
expect(config).toEqual({ $schema: "https://opencode.ai/v2/cli.json", animations: false })
|
||||
expect(await Bun.file(file).json()).toEqual(config)
|
||||
} finally {
|
||||
await Bun.$`rm -rf ${directory}`
|
||||
}
|
||||
})
|
||||
|
||||
test("migrates tui and kv config into cli.json", async () => {
|
||||
const directory = await Bun.$`mktemp -d`.text().then((value) => value.trim())
|
||||
await Bun.write(
|
||||
@@ -73,6 +134,7 @@ test("migrates tui and kv config into cli.json", async () => {
|
||||
)
|
||||
|
||||
expect(config).toMatchObject({
|
||||
$schema: "https://opencode.ai/v2/cli.json",
|
||||
theme: { name: "legacy", mode: "light" },
|
||||
keybinds: {
|
||||
leader: "ctrl+o",
|
||||
@@ -128,8 +190,14 @@ test("migrates before the first update and does not remigrate afterward", async
|
||||
}),
|
||||
)
|
||||
|
||||
expect(config).toEqual({ theme: { name: "legacy" }, animations: false, mouse: false })
|
||||
expect(config).toEqual({
|
||||
$schema: "https://opencode.ai/v2/cli.json",
|
||||
theme: { name: "legacy" },
|
||||
animations: false,
|
||||
mouse: false,
|
||||
})
|
||||
expect(await Bun.file(path.join(directory, "cli.json")).json()).toEqual({
|
||||
$schema: "https://opencode.ai/v2/cli.json",
|
||||
theme: { name: "legacy" },
|
||||
animations: false,
|
||||
mouse: false,
|
||||
|
||||
@@ -66,7 +66,7 @@ export const Info = Schema.Struct({
|
||||
).annotate({ description: "Leader key behavior" }),
|
||||
scroll: Schema.optional(
|
||||
Schema.Struct({
|
||||
speed: Schema.optional(Schema.Number.check(Schema.isGreaterThanOrEqualTo(0.001))).annotate({
|
||||
speed: Schema.optional(Schema.Finite.check(Schema.isGreaterThanOrEqualTo(0.001))).annotate({
|
||||
description: "Distance scrolled per input tick",
|
||||
}),
|
||||
acceleration: Schema.optional(Schema.Boolean).annotate({
|
||||
@@ -80,7 +80,7 @@ export const Info = Schema.Struct({
|
||||
notifications: Schema.optional(Schema.Boolean).annotate({ description: "Show system notifications" }),
|
||||
sound: Schema.optional(Schema.Boolean).annotate({ description: "Play attention sounds" }),
|
||||
volume: Schema.optional(
|
||||
Schema.Number.check(Schema.isGreaterThanOrEqualTo(0), Schema.isLessThanOrEqualTo(1)),
|
||||
Schema.Finite.check(Schema.isGreaterThanOrEqualTo(0), Schema.isLessThanOrEqualTo(1)),
|
||||
).annotate({ description: "Attention sound volume from 0 to 1" }),
|
||||
sound_pack: Schema.optional(Schema.String).annotate({ description: "Active attention sound pack ID" }),
|
||||
sounds: Schema.optional(Schema.Record(AttentionSoundName, Schema.optionalKey(Schema.String))).annotate({
|
||||
|
||||
@@ -30,7 +30,7 @@ export const BindingValueSchema = Schema.Union([
|
||||
Schema.Literal("none"),
|
||||
BindingItem,
|
||||
Schema.Array(BindingItem),
|
||||
])
|
||||
]).annotate({ identifier: "TuiKeybind.BindingValue" })
|
||||
export type BindingValueSchema = Schema.Schema.Type<typeof BindingValueSchema>
|
||||
|
||||
type Definition = {
|
||||
|
||||
@@ -7,8 +7,8 @@
|
||||
"dev": "bun run generate && astro dev --host --port 3000",
|
||||
"build": "bun run generate && astro build && bun script/pagefind.ts && bun run validate && bun script/prepare-cloudflare.ts",
|
||||
"deploy": "wrangler deploy --config dist/server/wrangler.json",
|
||||
"generate": "bun script/generate-openapi.ts",
|
||||
"check:generated": "bun script/generate-openapi.ts --check",
|
||||
"generate": "bun script/generate-openapi.ts && bun --cwd ../cli script/schema.ts ../www/public/cli.json",
|
||||
"check:generated": "bun script/generate-openapi.ts --check && bun --cwd ../cli script/schema.ts ../www/public/cli.json --check",
|
||||
"validate": "bun script/validate-links.ts",
|
||||
"typecheck": "astro check"
|
||||
},
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -18,6 +18,16 @@ Set `$XDG_CONFIG_HOME` to use a different config directory:
|
||||
$XDG_CONFIG_HOME/opencode/cli.json
|
||||
```
|
||||
|
||||
Add the published JSON Schema for editor validation and autocomplete:
|
||||
|
||||
```json title="cli.json"
|
||||
{
|
||||
"$schema": "https://opencode.ai/v2/cli.json"
|
||||
}
|
||||
```
|
||||
|
||||
OpenCode includes this automatically when it creates or migrates your CLI config.
|
||||
|
||||
## Theme
|
||||
|
||||
Set the theme and color mode:
|
||||
|
||||
Reference in New Issue
Block a user