From 78774754c04714ab24c8b90ca2481c1e70e922c0 Mon Sep 17 00:00:00 2001 From: James Long Date: Mon, 6 Jul 2026 23:56:27 +0000 Subject: [PATCH] fix(config): handle unavailable config directories --- packages/core/src/fs-util.ts | 7 +++--- packages/opencode/src/config/config.ts | 1 + packages/opencode/test/config/config.test.ts | 25 ++++++++++++++++++++ 3 files changed, 30 insertions(+), 3 deletions(-) diff --git a/packages/core/src/fs-util.ts b/packages/core/src/fs-util.ts index ff71477d70f4..d637be49e221 100644 --- a/packages/core/src/fs-util.ts +++ b/packages/core/src/fs-util.ts @@ -59,9 +59,10 @@ export namespace FSUtil { }) const readFileStringSafe = Effect.fn("FileSystem.readFileStringSafe")(function* (path: string) { - return yield* fs - .readFileString(path) - .pipe(Effect.catchReason("PlatformError", "NotFound", () => Effect.succeed(undefined))) + return yield* fs.readFileString(path).pipe( + Effect.catchReason("PlatformError", "NotFound", () => Effect.succeed(undefined)), + Effect.catchReason("PlatformError", "PermissionDenied", () => Effect.succeed(undefined)), + ) }) const isDir = Effect.fn("FileSystem.isDir")(function* (path: string) { diff --git a/packages/opencode/src/config/config.ts b/packages/opencode/src/config/config.ts index 35bd2e0cce9d..86238f1a844c 100644 --- a/packages/opencode/src/config/config.ts +++ b/packages/opencode/src/config/config.ts @@ -293,6 +293,7 @@ const layer = Layer.effect( }) const ensureGitignore = Effect.fn("Config.ensureGitignore")(function* (dir: string) { + yield* fs.ensureDir(dir) const gitignore = path.join(dir, ".gitignore") const hasIgnore = yield* fs.existsSafe(gitignore) if (!hasIgnore) { diff --git a/packages/opencode/test/config/config.test.ts b/packages/opencode/test/config/config.test.ts index 4b68725852ae..e281831ddfa8 100644 --- a/packages/opencode/test/config/config.test.ts +++ b/packages/opencode/test/config/config.test.ts @@ -923,6 +923,31 @@ it.effect("does not try to install dependencies in read-only OPENCODE_CONFIG_DIR }).pipe(Effect.provide(testInstanceStoreLayer), Effect.provide(LayerNode.compile(CrossSpawnSpawner.node))), ) +it.effect("ignores an inaccessible OPENCODE_CONFIG_DIR", () => + Effect.gen(function* () { + if (process.platform === "win32") return + + const dir = yield* tmpdirScoped() + const configDir = path.join(dir, "inaccessible") + yield* FSUtil.use.ensureDir(configDir) + yield* FSUtil.use.chmod(configDir, 0o000) + yield* Effect.addFinalizer(() => FSUtil.use.chmod(configDir, 0o755).pipe(Effect.ignore)) + + yield* withProcessEnv("OPENCODE_CONFIG_DIR", configDir, Config.use.get().pipe(provideInstanceEffect(dir))) + }).pipe(Effect.provide(testInstanceStoreLayer), Effect.provide(LayerNode.compile(CrossSpawnSpawner.node))), +) + +it.effect("creates a missing OPENCODE_CONFIG_DIR", () => + Effect.gen(function* () { + const dir = yield* tmpdirScoped() + const configDir = path.join(dir, "configdir") + + yield* withProcessEnv("OPENCODE_CONFIG_DIR", configDir, Config.use.get().pipe(provideInstanceEffect(dir))) + + expect(yield* FSUtil.use.readFileString(path.join(configDir, ".gitignore"))).toContain("node_modules") + }).pipe(Effect.provide(testInstanceStoreLayer), Effect.provide(LayerNode.compile(CrossSpawnSpawner.node))), +) + it.effect("installs dependencies in writable OPENCODE_CONFIG_DIR", () => Effect.gen(function* () { const dir = yield* tmpdirScoped()