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({