refactor(core): consolidate filesystem services (#30447)

This commit is contained in:
Dax
2026-06-02 16:09:26 -04:00
committed by GitHub
parent b93963e462
commit 604a5f781f
153 changed files with 2553 additions and 4312 deletions

View File

@@ -12,7 +12,7 @@ import type { InstanceContext } from "../../src/project/instance-context"
import { Auth } from "../../src/auth"
import { Account } from "../../src/account/account"
import { AccessToken, AccountID, OrgID } from "../../src/account/schema"
import { AppFileSystem } from "@opencode-ai/core/filesystem"
import { FSUtil } from "@opencode-ai/core/fs-util"
import { Env } from "../../src/env"
import {
provideTmpdirInstance,
@@ -100,7 +100,7 @@ const configLayer = (
Layer.provideMerge(infra),
Layer.provide(NpmTest.noop),
Layer.provide(Layer.succeed(HttpClient.HttpClient, options.client ?? unexpectedHttp)),
Layer.provideMerge(AppFileSystem.defaultLayer),
Layer.provideMerge(FSUtil.defaultLayer),
)
const layer = configLayer()
@@ -145,14 +145,14 @@ afterEach(async () => {
})
const writeManagedSettingsEffect = (settings: object, filename?: string) =>
AppFileSystem.use.writeWithDirs(path.join(managedConfigDir, filename ?? "opencode.json"), JSON.stringify(settings))
FSUtil.use.writeWithDirs(path.join(managedConfigDir, filename ?? "opencode.json"), JSON.stringify(settings))
async function writeConfig(dir: string, config: object, name = "opencode.json") {
await Filesystem.write(path.join(dir, name), JSON.stringify(config))
}
const writeConfigEffect = (dir: string, config: object, name = "opencode.json") =>
AppFileSystem.use.writeWithDirs(path.join(dir, name), JSON.stringify(config))
FSUtil.use.writeWithDirs(path.join(dir, name), JSON.stringify(config))
const withInstanceDir = <A, E, R>(dir: string, effect: Effect.Effect<A, E, R>) =>
effect.pipe(
@@ -201,9 +201,7 @@ const withConfigTree = <A, E, R>(
input.global ? writeConfigEffect(global, schemaConfig(input.global)) : undefined,
input.project ? writeConfigEffect(directory, schemaConfig(input.project)) : undefined,
input.local ? writeConfigEffect(path.join(directory, ".opencode"), schemaConfig(input.local)) : undefined,
].filter(
(effect): effect is Effect.Effect<void, AppFileSystem.Error, AppFileSystem.Service> => effect !== undefined,
),
].filter((effect): effect is Effect.Effect<void, FSUtil.Error, FSUtil.Service> => effect !== undefined),
{ concurrency: "unbounded" },
)
return yield* withGlobalConfigDir(global, withInstanceDir(directory, effect))
@@ -311,7 +309,7 @@ it.effect("creates global jsonc config with schema when no global configs exist"
Effect.gen(function* () {
yield* Config.use.get().pipe(provideInstanceEffect(dir))
const content = yield* AppFileSystem.use.readFileString(path.join(dir, "opencode.jsonc"))
const content = yield* FSUtil.use.readFileString(path.join(dir, "opencode.jsonc"))
expect(content).toContain('"$schema": "https://opencode.ai/config.json"')
}).pipe(Effect.provide(testInstanceStoreLayer), Effect.provide(CrossSpawnSpawner.defaultLayer)),
),
@@ -327,7 +325,7 @@ it.effect("does not create global config when OPENCODE_CONFIG_DIR is set", () =>
Effect.gen(function* () {
yield* Config.use.get().pipe(provideInstanceEffect(dir))
expect(yield* AppFileSystem.use.existsSafe(path.join(dir, "opencode.jsonc"))).toBe(false)
expect(yield* FSUtil.use.existsSafe(path.join(dir, "opencode.jsonc"))).toBe(false)
}).pipe(Effect.provide(testInstanceStoreLayer), Effect.provide(CrossSpawnSpawner.defaultLayer)),
),
)
@@ -364,7 +362,7 @@ it.instance("updates config and preserves empty shell sentinel", () =>
yield* Config.Service.use((svc) => svc.update(ConfigParse.schema(Config.Info, { shell: "" }, "test:config")))
const writtenConfig = yield* AppFileSystem.use.readJson(path.join(test.directory, "config.json"))
const writtenConfig = yield* FSUtil.use.readJson(path.join(test.directory, "config.json"))
expect(writtenConfig).toMatchObject({ shell: "" })
}),
)
@@ -374,7 +372,7 @@ it.effect("updates global config and omits empty shell key in json", () =>
Effect.gen(function* () {
yield* Config.use.updateGlobal({ shell: "" })
const writtenConfig = yield* AppFileSystem.use.readJson(path.join(dir, "opencode.json"))
const writtenConfig = yield* FSUtil.use.readJson(path.join(dir, "opencode.json"))
expect(writtenConfig).not.toHaveProperty("shell")
}),
),
@@ -386,7 +384,7 @@ it.effect("updates global config and omits empty shell key in jsonc", () =>
yield* Config.use.updateGlobal({ shell: "" })
const file = path.join(dir, "opencode.jsonc")
const writtenConfig = yield* AppFileSystem.use.readFileString(file)
const writtenConfig = yield* FSUtil.use.readFileString(file)
const parsed = ConfigParse.schema(Config.Info, ConfigParse.jsonc(writtenConfig, file), file)
expect(writtenConfig).not.toContain('"shell"')
expect(parsed.shell).toBeUndefined()
@@ -450,7 +448,7 @@ it.instance("ignores legacy tui keys in opencode config", () =>
it.instance("loads JSONC config file", () =>
Effect.gen(function* () {
const test = yield* TestInstance
yield* AppFileSystem.use.writeWithDirs(
yield* FSUtil.use.writeWithDirs(
path.join(test.directory, "opencode.jsonc"),
`{
// This is a comment
@@ -510,7 +508,7 @@ it.instance("preserves env variables when adding $schema to config", () =>
Effect.gen(function* () {
const test = yield* TestInstance
// Config without $schema - should trigger auto-add
yield* AppFileSystem.use.writeWithDirs(
yield* FSUtil.use.writeWithDirs(
path.join(test.directory, "opencode.json"),
JSON.stringify({ username: "{env:PRESERVE_VAR}" }),
)
@@ -518,7 +516,7 @@ it.instance("preserves env variables when adding $schema to config", () =>
expect(config.username).toBe("secret_value")
// Read the file to verify the env variable was preserved
const content = yield* AppFileSystem.use.readFileString(path.join(test.directory, "opencode.json"))
const content = yield* FSUtil.use.readFileString(path.join(test.directory, "opencode.json"))
expect(content).toContain("{env:PRESERVE_VAR}")
expect(content).not.toContain("secret_value")
expect(content).toContain("$schema")
@@ -529,7 +527,7 @@ it.instance("preserves env variables when adding $schema to config", () =>
it.instance("handles file inclusion substitution", () =>
Effect.gen(function* () {
const test = yield* TestInstance
yield* AppFileSystem.use.writeWithDirs(path.join(test.directory, "included.txt"), "test-user")
yield* FSUtil.use.writeWithDirs(path.join(test.directory, "included.txt"), "test-user")
yield* writeConfigEffect(test.directory, {
$schema: "https://opencode.ai/config.json",
username: "{file:included.txt}",
@@ -542,7 +540,7 @@ it.instance("handles file inclusion substitution", () =>
it.instance("handles file inclusion with replacement tokens", () =>
Effect.gen(function* () {
const test = yield* TestInstance
yield* AppFileSystem.use.writeWithDirs(path.join(test.directory, "included.md"), "const out = await Bun.$`echo hi`")
yield* FSUtil.use.writeWithDirs(path.join(test.directory, "included.md"), "const out = await Bun.$`echo hi`")
yield* writeConfigEffect(test.directory, {
$schema: "https://opencode.ai/config.json",
username: "{file:included.md}",
@@ -610,7 +608,7 @@ it.instance("validates config schema and throws on invalid fields", () =>
it.instance("throws error for invalid JSON", () =>
Effect.gen(function* () {
const test = yield* TestInstance
yield* AppFileSystem.use.writeWithDirs(path.join(test.directory, "opencode.json"), "{ invalid json }")
yield* FSUtil.use.writeWithDirs(path.join(test.directory, "opencode.json"), "{ invalid json }")
const exit = yield* Config.use.get().pipe(Effect.exit)
expect(Exit.isFailure(exit)).toBe(true)
}),
@@ -725,7 +723,7 @@ it.instance("migrates mode field to agent field", () =>
it.instance("loads config from .opencode directory", () =>
Effect.gen(function* () {
const test = yield* TestInstance
yield* AppFileSystem.use.writeWithDirs(
yield* FSUtil.use.writeWithDirs(
path.join(test.directory, ".opencode", "agent", "test.md"),
`---
model: test/model
@@ -747,7 +745,7 @@ Test agent prompt`,
it.instance("agent markdown permission config preserves user key order", () =>
Effect.gen(function* () {
const test = yield* TestInstance
yield* AppFileSystem.use.writeWithDirs(
yield* FSUtil.use.writeWithDirs(
path.join(test.directory, ".opencode", "agent", "ordered.md"),
`---
permission:
@@ -766,7 +764,7 @@ Ordered permissions`,
it.instance("loads agents from .opencode/agents (plural)", () =>
Effect.gen(function* () {
const test = yield* TestInstance
yield* AppFileSystem.use.writeWithDirs(
yield* FSUtil.use.writeWithDirs(
path.join(test.directory, ".opencode", "agents", "helper.md"),
`---
model: test/model
@@ -775,7 +773,7 @@ mode: subagent
Helper agent prompt`,
)
yield* AppFileSystem.use.writeWithDirs(
yield* FSUtil.use.writeWithDirs(
path.join(test.directory, ".opencode", "agents", "nested", "child.md"),
`---
model: test/model
@@ -805,7 +803,7 @@ Nested agent prompt`,
it.instance("loads commands from .opencode/command (singular)", () =>
Effect.gen(function* () {
const test = yield* TestInstance
yield* AppFileSystem.use.writeWithDirs(
yield* FSUtil.use.writeWithDirs(
path.join(test.directory, ".opencode", "command", "hello.md"),
`---
description: Test command
@@ -813,7 +811,7 @@ description: Test command
Hello from singular command`,
)
yield* AppFileSystem.use.writeWithDirs(
yield* FSUtil.use.writeWithDirs(
path.join(test.directory, ".opencode", "command", "nested", "child.md"),
`---
description: Nested command
@@ -838,7 +836,7 @@ Nested command template`,
it.instance("loads commands from .opencode/commands (plural)", () =>
Effect.gen(function* () {
const test = yield* TestInstance
yield* AppFileSystem.use.writeWithDirs(
yield* FSUtil.use.writeWithDirs(
path.join(test.directory, ".opencode", "commands", "hello.md"),
`---
description: Test command
@@ -846,7 +844,7 @@ description: Test command
Hello from plural commands`,
)
yield* AppFileSystem.use.writeWithDirs(
yield* FSUtil.use.writeWithDirs(
path.join(test.directory, ".opencode", "commands", "nested", "child.md"),
`---
description: Nested command
@@ -875,7 +873,7 @@ it.instance("updates config and writes to file", () =>
svc.update(ConfigParse.schema(Config.Info, { model: "updated/model" }, "test:config")),
)
const writtenConfig = yield* AppFileSystem.use.readJson(path.join(test.directory, "config.json"))
const writtenConfig = yield* FSUtil.use.readJson(path.join(test.directory, "config.json"))
expect(writtenConfig).toMatchObject({ model: "updated/model" })
}),
)
@@ -893,9 +891,9 @@ it.effect("does not try to install dependencies in read-only OPENCODE_CONFIG_DIR
const dir = yield* tmpdirScoped()
const readonly = path.join(dir, "readonly")
yield* AppFileSystem.use.ensureDir(readonly)
yield* AppFileSystem.use.chmod(readonly, 0o555)
yield* Effect.addFinalizer(() => AppFileSystem.use.chmod(readonly, 0o755).pipe(Effect.ignore))
yield* FSUtil.use.ensureDir(readonly)
yield* FSUtil.use.chmod(readonly, 0o555)
yield* Effect.addFinalizer(() => FSUtil.use.chmod(readonly, 0o755).pipe(Effect.ignore))
yield* withProcessEnv("OPENCODE_CONFIG_DIR", readonly, Config.use.get().pipe(provideInstanceEffect(dir)))
}).pipe(Effect.provide(testInstanceStoreLayer), Effect.provide(CrossSpawnSpawner.defaultLayer)),
@@ -905,7 +903,7 @@ it.effect("installs dependencies in writable OPENCODE_CONFIG_DIR", () =>
Effect.gen(function* () {
const dir = yield* tmpdirScoped()
const configDir = path.join(dir, "configdir")
yield* AppFileSystem.use.ensureDir(configDir)
yield* FSUtil.use.ensureDir(configDir)
yield* withProcessEnv(
"OPENCODE_CONFIG_DIR",
@@ -915,7 +913,7 @@ it.effect("installs dependencies in writable OPENCODE_CONFIG_DIR", () =>
),
)
expect(yield* AppFileSystem.use.readFileString(path.join(configDir, ".gitignore"))).toContain("package-lock.json")
expect(yield* FSUtil.use.readFileString(path.join(configDir, ".gitignore"))).toContain("package-lock.json")
}).pipe(Effect.provide(testInstanceStoreLayer), Effect.provide(CrossSpawnSpawner.defaultLayer)),
)
@@ -927,11 +925,11 @@ it.instance("resolves scoped npm plugins in config", () =>
Effect.gen(function* () {
const test = yield* TestInstance
const pluginDir = path.join(test.directory, "node_modules", "@scope", "plugin")
yield* AppFileSystem.use.writeWithDirs(
yield* FSUtil.use.writeWithDirs(
path.join(test.directory, "package.json"),
JSON.stringify({ name: "config-fixture", version: "1.0.0", type: "module" }, null, 2),
)
yield* AppFileSystem.use.writeWithDirs(
yield* FSUtil.use.writeWithDirs(
path.join(pluginDir, "package.json"),
JSON.stringify(
{
@@ -944,7 +942,7 @@ it.instance("resolves scoped npm plugins in config", () =>
2,
),
)
yield* AppFileSystem.use.writeWithDirs(path.join(pluginDir, "index.js"), "export default {}\n")
yield* FSUtil.use.writeWithDirs(path.join(pluginDir, "index.js"), "export default {}\n")
yield* writeConfigEffect(test.directory, { plugin: ["@scope/plugin"] })
const config = yield* Config.use.get()
@@ -993,7 +991,7 @@ it.effect("global config remains global when project config is disabled", () =>
it.instance("does not error when only custom agent is a subagent", () =>
Effect.gen(function* () {
const test = yield* TestInstance
yield* AppFileSystem.use.writeWithDirs(
yield* FSUtil.use.writeWithDirs(
path.join(test.directory, ".opencode", "agent", "helper.md"),
`---
model: test/model
@@ -1414,7 +1412,7 @@ it.instance("local .opencode config can override MCP from project config", () =>
},
},
})
yield* AppFileSystem.use.ensureDir(path.join(test.directory, ".opencode"))
yield* FSUtil.use.ensureDir(path.join(test.directory, ".opencode"))
yield* writeConfigEffect(
path.join(test.directory, ".opencode"),
{
@@ -1503,7 +1501,7 @@ test("remote well-known config can use FetchHttpClient layer", async () => {
Layer.mergeAll(
Config.layer.pipe(
Layer.provide(testFlock),
Layer.provide(AppFileSystem.defaultLayer),
Layer.provide(FSUtil.defaultLayer),
Layer.provide(Env.defaultLayer),
Layer.provide(wellKnownAuth(server.url.origin)),
Layer.provide(AccountTest.empty),
@@ -1736,7 +1734,7 @@ describe("deduplicatePluginOrigins", () => {
{ global: { plugin: ["my-plugin@1.0.0"] } },
Effect.gen(function* () {
const test = yield* TestInstance
yield* AppFileSystem.use.writeWithDirs(
yield* FSUtil.use.writeWithDirs(
path.join(test.directory, ".opencode", "plugin", "my-plugin.js"),
"export default {}",
)
@@ -1771,7 +1769,7 @@ describe("OPENCODE_DISABLE_PROJECT_CONFIG", () => {
"true",
Effect.gen(function* () {
const test = yield* TestInstance
yield* AppFileSystem.use.writeWithDirs(
yield* FSUtil.use.writeWithDirs(
path.join(test.directory, ".opencode", "command", "test-cmd.md"),
"# Test Command\nThis is a test command.",
)
@@ -1800,7 +1798,7 @@ describe("OPENCODE_DISABLE_PROJECT_CONFIG", () => {
{ OPENCODE_CONFIG_DIR: undefined, OPENCODE_DISABLE_PROJECT_CONFIG: "true" },
Effect.gen(function* () {
const test = yield* TestInstance
yield* AppFileSystem.use.writeWithDirs(path.join(test.directory, "CUSTOM.md"), "# Custom Instructions")
yield* FSUtil.use.writeWithDirs(path.join(test.directory, "CUSTOM.md"), "# Custom Instructions")
// The relative instruction should be skipped without error
const config = yield* Config.use.get()
expect(config).toBeDefined()
@@ -1865,7 +1863,7 @@ describe("OPENCODE_CONFIG_CONTENT token substitution", () => {
it.instance("substitutes {file:} tokens in OPENCODE_CONFIG_CONTENT", () =>
Effect.gen(function* () {
const test = yield* TestInstance
yield* AppFileSystem.use.writeWithDirs(path.join(test.directory, "api_key.txt"), "secret_key_from_file")
yield* FSUtil.use.writeWithDirs(path.join(test.directory, "api_key.txt"), "secret_key_from_file")
yield* withProcessEnv(
"OPENCODE_CONFIG_CONTENT",
JSON.stringify({

View File

@@ -2,7 +2,7 @@ import { expect } from "bun:test"
import path from "path"
import { pathToFileURL } from "url"
import { Effect, Layer } from "effect"
import { AppFileSystem } from "@opencode-ai/core/filesystem"
import { FSUtil } from "@opencode-ai/core/fs-util"
import { Global } from "@opencode-ai/core/global"
import { Config } from "@/config/config"
import { ConfigPlugin } from "@/config/plugin"
@@ -11,7 +11,7 @@ import { TuiConfig } from "../../src/cli/cmd/tui/config/tui"
import { TestInstance } from "../fixture/fixture"
import { testEffect } from "../lib/effect"
const it = testEffect(Layer.mergeAll(Config.defaultLayer, AppFileSystem.defaultLayer))
const it = testEffect(Layer.mergeAll(Config.defaultLayer, FSUtil.defaultLayer))
const winIt = process.platform === "win32" ? it.instance : it.instance.skip
const globalConfigFiles = ["opencode.json", "opencode.jsonc", "tui.json", "tui.jsonc"].map((file) =>
@@ -19,7 +19,7 @@ const globalConfigFiles = ["opencode.json", "opencode.jsonc", "tui.json", "tui.j
)
const cleanState = Effect.gen(function* () {
const fs = yield* AppFileSystem.Service
const fs = yield* FSUtil.Service
delete process.env.OPENCODE_CONFIG
delete process.env.OPENCODE_TUI_CONFIG
yield* Effect.forEach(globalConfigFiles, (file) => fs.remove(file, { force: true }).pipe(Effect.ignore), {
@@ -75,7 +75,7 @@ const getTuiConfig = (directory: string) =>
it.instance("keeps server and tui plugin merge semantics aligned", () =>
withCleanState(
Effect.gen(function* () {
const fs = yield* AppFileSystem.Service
const fs = yield* FSUtil.Service
const test = yield* TestInstance
const local = path.join(test.directory, ".opencode")
yield* fs.makeDirectory(local, { recursive: true })
@@ -114,7 +114,7 @@ it.instance("keeps server and tui plugin merge semantics aligned", () =>
it.instance("loads tui config with the same precedence order as server config paths", () =>
withCleanState(
Effect.gen(function* () {
const fs = yield* AppFileSystem.Service
const fs = yield* FSUtil.Service
const test = yield* TestInstance
yield* fs.writeJson(path.join(Global.Path.config, "tui.json"), { theme: "global" })
yield* fs.writeJson(path.join(test.directory, "tui.json"), { theme: "project" })
@@ -133,7 +133,7 @@ it.instance("loads tui config with the same precedence order as server config pa
it.instance("resolves attention config defaults and overrides", () =>
withCleanState(
Effect.gen(function* () {
const fs = yield* AppFileSystem.Service
const fs = yield* FSUtil.Service
const test = yield* TestInstance
expect((yield* getTuiConfig(test.directory)).attention).toEqual({
@@ -181,7 +181,7 @@ it.instance("resolves attention config defaults and overrides", () =>
it.instance("migrates tui-specific keys from opencode.json when tui.json does not exist", () =>
withCleanState(
Effect.gen(function* () {
const fs = yield* AppFileSystem.Service
const fs = yield* FSUtil.Service
const test = yield* TestInstance
const source = path.join(test.directory, "opencode.json")
yield* fs.writeJson(source, {
@@ -211,7 +211,7 @@ it.instance("migrates tui-specific keys from opencode.json when tui.json does no
it.instance("migrates project legacy tui keys even when global tui.json already exists", () =>
withCleanState(
Effect.gen(function* () {
const fs = yield* AppFileSystem.Service
const fs = yield* FSUtil.Service
const test = yield* TestInstance
yield* fs.writeJson(path.join(Global.Path.config, "tui.json"), { theme: "global" })
yield* fs.writeJson(path.join(test.directory, "opencode.json"), {
@@ -234,7 +234,7 @@ it.instance("migrates project legacy tui keys even when global tui.json already
it.instance("drops unknown legacy tui keys during migration", () =>
withCleanState(
Effect.gen(function* () {
const fs = yield* AppFileSystem.Service
const fs = yield* FSUtil.Service
const test = yield* TestInstance
yield* fs.writeJson(path.join(test.directory, "opencode.json"), {
theme: "migrated-theme",
@@ -255,7 +255,7 @@ it.instance("drops unknown legacy tui keys during migration", () =>
it.instance("skips migration when opencode.jsonc is syntactically invalid", () =>
withCleanState(
Effect.gen(function* () {
const fs = yield* AppFileSystem.Service
const fs = yield* FSUtil.Service
const test = yield* TestInstance
yield* fs.writeFileString(
path.join(test.directory, "opencode.jsonc"),
@@ -281,7 +281,7 @@ it.instance("skips migration when opencode.jsonc is syntactically invalid", () =
it.instance("skips migration when tui.json already exists", () =>
withCleanState(
Effect.gen(function* () {
const fs = yield* AppFileSystem.Service
const fs = yield* FSUtil.Service
const test = yield* TestInstance
yield* fs.writeJson(path.join(test.directory, "opencode.json"), { theme: "legacy" })
yield* fs.writeJson(path.join(test.directory, "tui.json"), { diff_style: "stacked" })
@@ -300,7 +300,7 @@ it.instance("skips migration when tui.json already exists", () =>
it.instance("continues loading tui config when legacy source cannot be stripped", () =>
withCleanState(
Effect.gen(function* () {
const fs = yield* AppFileSystem.Service
const fs = yield* FSUtil.Service
const test = yield* TestInstance
const source = path.join(test.directory, "opencode.json")
yield* fs.writeJson(source, { theme: "readonly-theme" })
@@ -325,7 +325,7 @@ it.instance("continues loading tui config when legacy source cannot be stripped"
it.instance("migration backup preserves JSONC comments", () =>
withCleanState(
Effect.gen(function* () {
const fs = yield* AppFileSystem.Service
const fs = yield* FSUtil.Service
const test = yield* TestInstance
yield* fs.writeFileString(
path.join(test.directory, "opencode.jsonc"),
@@ -352,7 +352,7 @@ it.instance("migration backup preserves JSONC comments", () =>
it.instance("migrates legacy tui keys across multiple opencode.json levels", () =>
withCleanState(
Effect.gen(function* () {
const fs = yield* AppFileSystem.Service
const fs = yield* FSUtil.Service
const test = yield* TestInstance
const nested = path.join(test.directory, "apps", "client")
yield* fs.makeDirectory(nested, { recursive: true })
@@ -370,7 +370,7 @@ it.instance("migrates legacy tui keys across multiple opencode.json levels", ()
it.instance("flattens nested tui key inside tui.json", () =>
withCleanState(
Effect.gen(function* () {
const fs = yield* AppFileSystem.Service
const fs = yield* FSUtil.Service
const test = yield* TestInstance
yield* fs.writeJson(path.join(test.directory, "tui.json"), {
theme: "outer",
@@ -388,7 +388,7 @@ it.instance("flattens nested tui key inside tui.json", () =>
it.instance("top-level keys in tui.json take precedence over nested tui key", () =>
withCleanState(
Effect.gen(function* () {
const fs = yield* AppFileSystem.Service
const fs = yield* FSUtil.Service
const test = yield* TestInstance
yield* fs.writeJson(path.join(test.directory, "tui.json"), {
diff_style: "auto",
@@ -405,7 +405,7 @@ it.instance("top-level keys in tui.json take precedence over nested tui key", ()
it.instance("project config takes precedence over OPENCODE_TUI_CONFIG (matches OPENCODE_CONFIG)", () =>
withCleanState(
Effect.gen(function* () {
const fs = yield* AppFileSystem.Service
const fs = yield* FSUtil.Service
const test = yield* TestInstance
const custom = path.join(test.directory, "custom-tui.json")
yield* fs.writeJson(path.join(test.directory, "tui.json"), { theme: "project", diff_style: "auto" })
@@ -427,7 +427,7 @@ it.instance("project config takes precedence over OPENCODE_TUI_CONFIG (matches O
it.instance("merges keybind overrides across precedence layers", () =>
withCleanState(
Effect.gen(function* () {
const fs = yield* AppFileSystem.Service
const fs = yield* FSUtil.Service
const test = yield* TestInstance
yield* fs.writeJson(path.join(Global.Path.config, "tui.json"), { keybinds: { app_exit: "ctrl+q" } })
yield* fs.writeJson(path.join(test.directory, "tui.json"), { keybinds: { theme_list: "ctrl+k" } })
@@ -442,7 +442,7 @@ it.instance("merges keybind overrides across precedence layers", () =>
it.instance("ignores unknown keybind names without dropping valid overrides from the same file", () =>
withCleanState(
Effect.gen(function* () {
const fs = yield* AppFileSystem.Service
const fs = yield* FSUtil.Service
const test = yield* TestInstance
yield* fs.writeJson(path.join(Global.Path.config, "tui.json"), {
keybinds: {
@@ -461,7 +461,7 @@ it.instance("ignores unknown keybind names without dropping valid overrides from
it.instance("resolves keybind lookup from canonical keybinds", () =>
withCleanState(
Effect.gen(function* () {
const fs = yield* AppFileSystem.Service
const fs = yield* FSUtil.Service
const test = yield* TestInstance
yield* fs.writeJson(path.join(test.directory, "tui.json"), {
keybinds: {
@@ -506,7 +506,7 @@ it.instance("resolves keybind lookup from canonical keybinds", () =>
it.instance("keybinds accept OpenTUI binding specs", () =>
withCleanState(
Effect.gen(function* () {
const fs = yield* AppFileSystem.Service
const fs = yield* FSUtil.Service
const test = yield* TestInstance
yield* fs.writeJson(path.join(test.directory, "tui.json"), {
keybinds: {
@@ -546,7 +546,7 @@ winIt("defaults Ctrl+Z to input undo on Windows", () =>
winIt("keeps explicit input undo overrides on Windows", () =>
withCleanState(
Effect.gen(function* () {
const fs = yield* AppFileSystem.Service
const fs = yield* FSUtil.Service
const test = yield* TestInstance
yield* fs.writeJson(path.join(test.directory, "tui.json"), { keybinds: { input_undo: "ctrl+y" } })
@@ -560,7 +560,7 @@ winIt("keeps explicit input undo overrides on Windows", () =>
winIt("ignores terminal suspend bindings on Windows", () =>
withCleanState(
Effect.gen(function* () {
const fs = yield* AppFileSystem.Service
const fs = yield* FSUtil.Service
const test = yield* TestInstance
yield* fs.writeJson(path.join(test.directory, "tui.json"), { keybinds: { terminal_suspend: "alt+z" } })
@@ -590,7 +590,7 @@ it.instance("ignores explicit keybind terminal suspend binding on Windows", () =
withPlatform(
"win32",
Effect.gen(function* () {
const fs = yield* AppFileSystem.Service
const fs = yield* FSUtil.Service
const test = yield* TestInstance
yield* fs.writeJson(path.join(test.directory, "tui.json"), {
keybinds: {
@@ -610,7 +610,7 @@ it.instance("keeps explicit configured keybind input undo on Windows", () =>
withPlatform(
"win32",
Effect.gen(function* () {
const fs = yield* AppFileSystem.Service
const fs = yield* FSUtil.Service
const test = yield* TestInstance
yield* fs.writeJson(path.join(test.directory, "tui.json"), {
keybinds: {
@@ -628,7 +628,7 @@ it.instance("keeps explicit configured keybind input undo on Windows", () =>
it.instance("OPENCODE_TUI_CONFIG provides settings when no project config exists", () =>
withCleanState(
Effect.gen(function* () {
const fs = yield* AppFileSystem.Service
const fs = yield* FSUtil.Service
const test = yield* TestInstance
const custom = path.join(test.directory, "custom-tui.json")
yield* fs.writeJson(custom, { theme: "from-env", diff_style: "stacked" })
@@ -649,7 +649,7 @@ it.instance("OPENCODE_TUI_CONFIG provides settings when no project config exists
it.instance("does not derive tui path from OPENCODE_CONFIG", () =>
withCleanState(
Effect.gen(function* () {
const fs = yield* AppFileSystem.Service
const fs = yield* FSUtil.Service
const test = yield* TestInstance
const customDir = path.join(test.directory, "custom")
yield* fs.makeDirectory(customDir, { recursive: true })
@@ -674,7 +674,7 @@ it.instance("applies env and file substitutions in tui.json", () =>
"TUI_THEME_TEST",
"env-theme",
Effect.gen(function* () {
const fs = yield* AppFileSystem.Service
const fs = yield* FSUtil.Service
const test = yield* TestInstance
yield* fs.writeFileString(path.join(test.directory, "keybind.txt"), "ctrl+q")
yield* fs.writeJson(path.join(test.directory, "tui.json"), {
@@ -693,7 +693,7 @@ it.instance("applies env and file substitutions in tui.json", () =>
it.instance("applies file substitutions when first identical token is in a commented line", () =>
withCleanState(
Effect.gen(function* () {
const fs = yield* AppFileSystem.Service
const fs = yield* FSUtil.Service
const test = yield* TestInstance
yield* fs.writeFileString(path.join(test.directory, "theme.txt"), "resolved-theme")
yield* fs.writeFileString(
@@ -713,7 +713,7 @@ it.instance("applies file substitutions when first identical token is in a comme
it.instance("loads .opencode/tui.json", () =>
withCleanState(
Effect.gen(function* () {
const fs = yield* AppFileSystem.Service
const fs = yield* FSUtil.Service
const test = yield* TestInstance
yield* fs.writeWithDirs(
path.join(test.directory, ".opencode", "tui.json"),
@@ -729,7 +729,7 @@ it.instance("loads .opencode/tui.json", () =>
it.instance("supports tuple plugin specs with options in tui.json", () =>
withCleanState(
Effect.gen(function* () {
const fs = yield* AppFileSystem.Service
const fs = yield* FSUtil.Service
const test = yield* TestInstance
yield* fs.writeJson(path.join(test.directory, "tui.json"), {
plugin: [["acme-plugin@1.2.3", { enabled: true, label: "demo" }]],
@@ -751,7 +751,7 @@ it.instance("supports tuple plugin specs with options in tui.json", () =>
it.instance("deduplicates tuple plugin specs by name with higher precedence winning", () =>
withCleanState(
Effect.gen(function* () {
const fs = yield* AppFileSystem.Service
const fs = yield* FSUtil.Service
const test = yield* TestInstance
yield* fs.writeJson(path.join(Global.Path.config, "tui.json"), {
plugin: [["acme-plugin@1.0.0", { source: "global" }]],
@@ -787,7 +787,7 @@ it.instance("deduplicates tuple plugin specs by name with higher precedence winn
it.instance("tracks global and local plugin metadata in merged tui config", () =>
withCleanState(
Effect.gen(function* () {
const fs = yield* AppFileSystem.Service
const fs = yield* FSUtil.Service
const test = yield* TestInstance
yield* fs.writeJson(path.join(Global.Path.config, "tui.json"), { plugin: ["global-plugin@1.0.0"] })
yield* fs.writeJson(path.join(test.directory, "tui.json"), { plugin: ["local-plugin@2.0.0"] })
@@ -813,7 +813,7 @@ it.instance("tracks global and local plugin metadata in merged tui config", () =
it.instance("merges plugin_enabled flags across config layers", () =>
withCleanState(
Effect.gen(function* () {
const fs = yield* AppFileSystem.Service
const fs = yield* FSUtil.Service
const test = yield* TestInstance
yield* fs.writeJson(path.join(Global.Path.config, "tui.json"), {
plugin_enabled: {
@@ -841,7 +841,7 @@ it.instance("merges plugin_enabled flags across config layers", () =>
it.instance("silently skips malformed tui.json - load failures degrade to {}", () =>
withCleanState(
Effect.gen(function* () {
const fs = yield* AppFileSystem.Service
const fs = yield* FSUtil.Service
const test = yield* TestInstance
yield* fs.writeFileString(path.join(test.directory, "tui.json"), '{ "theme": "broken",')
yield* fs.writeWithDirs(path.join(test.directory, ".opencode", "tui.json"), JSON.stringify({ theme: "fallback" }))
@@ -855,7 +855,7 @@ it.instance("silently skips malformed tui.json - load failures degrade to {}", (
it.instance("silently skips non-ENOENT read failures (e.g. tui.json is a directory) - fallback layer still loads", () =>
withCleanState(
Effect.gen(function* () {
const fs = yield* AppFileSystem.Service
const fs = yield* FSUtil.Service
const test = yield* TestInstance
yield* fs.makeDirectory(path.join(test.directory, "tui.json"), { recursive: true })
yield* fs.writeWithDirs(path.join(test.directory, ".opencode", "tui.json"), JSON.stringify({ theme: "fallback" }))