From 470ecda8d354b4355cf7d1f4bf08763d135c9236 Mon Sep 17 00:00:00 2001 From: weiconghe <13976098570@163.com> Date: Mon, 8 Jun 2026 17:21:01 +0800 Subject: [PATCH] fix(config): use writeWithDirs in ensureGitignore to handle missing parent directory ensureGitignore used writeFileString which fails with ENOENT when the parent directory does not exist (e.g. when OPENCODE_CONFIG_DIR points to a directory that was deleted by an auto-update). Changed to writeWithDirs which auto-creates the parent directory before writing. Reproduced on Windows where OPENCODE_CONFIG_DIR was set to a path inside the app installation directory. After each auto-update, the directory was wiped but the env var persisted, causing a startup crash with ENOENT on every boot. Test: verifies .gitignore is created successfully when OPENCODE_CONFIG_DIR points to a non-existent directory. --- packages/opencode/src/config/config.ts | 4 ++-- packages/opencode/test/config/config.test.ts | 17 +++++++++++++++++ 2 files changed, 19 insertions(+), 2 deletions(-) diff --git a/packages/opencode/src/config/config.ts b/packages/opencode/src/config/config.ts index 2527303d92b5..52521ebe791a 100644 --- a/packages/opencode/src/config/config.ts +++ b/packages/opencode/src/config/config.ts @@ -289,9 +289,9 @@ export const layer = Layer.effect( const hasIgnore = yield* fs.existsSafe(gitignore) if (!hasIgnore) { yield* fs - .writeFileString( + .writeWithDirs( gitignore, - ["node_modules", "package.json", "package-lock.json", "bun.lock", ".gitignore"].join("\n"), + Buffer.from(["node_modules", "package.json", "package-lock.json", "bun.lock", ".gitignore"].join("\n")), ) .pipe( Effect.catchIf( diff --git a/packages/opencode/test/config/config.test.ts b/packages/opencode/test/config/config.test.ts index c016431bc781..624abfb1a5b5 100644 --- a/packages/opencode/test/config/config.test.ts +++ b/packages/opencode/test/config/config.test.ts @@ -919,6 +919,23 @@ it.effect("installs dependencies in writable OPENCODE_CONFIG_DIR", () => }).pipe(Effect.provide(testInstanceStoreLayer), Effect.provide(CrossSpawnSpawner.defaultLayer)), ) +it.effect("creates .gitignore in OPENCODE_CONFIG_DIR when directory does not yet exist", () => + Effect.gen(function* () { + const dir = yield* tmpdirScoped() + const configDir = path.join(dir, "nonexistent-config-dir") + + yield* withProcessEnv( + "OPENCODE_CONFIG_DIR", + configDir, + Config.Service.use((svc) => svc.get().pipe(Effect.andThen(svc.waitForDependencies()))).pipe( + provideInstanceEffect(dir), + ), + ) + + expect(yield* FSUtil.use.readFileString(path.join(configDir, ".gitignore"))).toContain("node_modules") + }).pipe(Effect.provide(testInstanceStoreLayer), Effect.provide(CrossSpawnSpawner.defaultLayer)), +) + // Note: deduplication and serialization of npm installs is now handled by the // core Npm.Service (via EffectFlock). Those behaviors are tested in the core // package's npm tests, not here.