From 7a956f6e7de02a6471a57da9d33bdc64c462bc63 Mon Sep 17 00:00:00 2001 From: sandy Date: Fri, 12 Jun 2026 15:13:22 +0800 Subject: [PATCH] fix(baoyu-image-gen): tolerate Bun-on-Windows EEXIST from mkdir(recursive) On Windows, Bun throws EEXIST for mkdir(dir, { recursive: true }) when the directory already exists, contradicting Node's documented contract (it should resolve silently). Image generation then succeeds but the file save fails whenever the output directory already exists (e.g. the Desktop): EEXIST: file already exists, mkdir 'C:\Users\...\Desktop' Add an ensureDir() helper that tolerates EEXIST only when the path really is a directory (rethrowing otherwise, so a genuine EEXIST against an existing file is not swallowed), and route writeImage() and migrateLegacyExtendConfig() through it. Covered by a cross-platform unit test. Co-Authored-By: Claude Opus 4.8 (1M context) --- skills/baoyu-image-gen/scripts/main.test.ts | 21 +++++++++++++++++++++ skills/baoyu-image-gen/scripts/main.ts | 20 +++++++++++++++++--- 2 files changed, 38 insertions(+), 3 deletions(-) diff --git a/skills/baoyu-image-gen/scripts/main.test.ts b/skills/baoyu-image-gen/scripts/main.test.ts index 5f0250e..2c3dd01 100644 --- a/skills/baoyu-image-gen/scripts/main.test.ts +++ b/skills/baoyu-image-gen/scripts/main.test.ts @@ -8,6 +8,7 @@ import type { CliArgs, ExtendConfig } from "./types.ts"; import { createTaskArgs, detectProvider, + ensureDir, getConfiguredMaxWorkers, getConfiguredProviderRateLimits, getWorkerCount, @@ -201,6 +202,26 @@ batch: }); }); +test("ensureDir creates nested dirs, is idempotent on an existing dir, and rethrows for a non-directory", async (t: TestContext) => { + const root = await makeTempDir("ensure-dir-"); + t.after(() => fs.rm(root, { recursive: true, force: true })); + + const nested = path.join(root, "a", "b", "c"); + await ensureDir(nested); + assert.equal((await fs.stat(nested)).isDirectory(), true); + + // Idempotent: a second call on an existing directory must not throw. This is the + // Bun-on-Windows regression the helper guards against (Bun wrongly throws EEXIST + // for mkdir(existingDir, { recursive: true })). + await ensureDir(nested); + + // Rethrows when the path exists but is a file rather than a directory, so a real + // EEXIST against a non-directory is not silently swallowed. + const filePath = path.join(root, "not-a-dir"); + await fs.writeFile(filePath, "x"); + await assert.rejects(() => ensureDir(filePath)); +}); + test("loadExtendConfig renames legacy EXTEND.md when the new path is missing", async () => { const root = await makeTempDir("baoyu-image-gen-extend-"); const cwd = path.join(root, "project"); diff --git a/skills/baoyu-image-gen/scripts/main.ts b/skills/baoyu-image-gen/scripts/main.ts index f08bd9b..8bf99c2 100644 --- a/skills/baoyu-image-gen/scripts/main.ts +++ b/skills/baoyu-image-gen/scripts/main.ts @@ -2,7 +2,7 @@ import path from "node:path"; import process from "node:process"; import { homedir } from "node:os"; import { fileURLToPath } from "node:url"; -import { access, mkdir, readFile, rename, writeFile } from "node:fs/promises"; +import { access, mkdir, readFile, rename, stat, writeFile } from "node:fs/promises"; import type { BatchFile, BatchTaskInput, @@ -563,11 +563,25 @@ async function exists(filePath: string): Promise { } } +export async function ensureDir(dir: string): Promise { + try { + await mkdir(dir, { recursive: true }); + } catch (err) { + // Bun on Windows incorrectly throws EEXIST for mkdir(dir, { recursive: true }) + // when the directory already exists, contradicting Node's documented contract + // (mkdir with recursive: true resolves silently for an existing directory). + // Tolerate EEXIST only when the path really is a directory; rethrow otherwise + // (e.g. EEXIST raised because the path points at an existing file). + if ((err as { code?: string }).code !== "EEXIST") throw err; + if (!(await stat(dir)).isDirectory()) throw err; + } +} + async function migrateLegacyExtendConfig(cwd: string, home: string): Promise { for (const { current, legacy } of getExtendConfigPathPairs(cwd, home)) { const [hasCurrent, hasLegacy] = await Promise.all([exists(current), exists(legacy)]); if (hasCurrent || !hasLegacy) continue; - await mkdir(path.dirname(current), { recursive: true }); + await ensureDir(path.dirname(current)); await rename(legacy, current); } } @@ -1049,7 +1063,7 @@ async function prepareBatchTasks( } async function writeImage(outputPath: string, imageData: Uint8Array): Promise { - await mkdir(path.dirname(outputPath), { recursive: true }); + await ensureDir(path.dirname(outputPath)); await writeFile(outputPath, imageData); }