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); }