E4: generators tolerate Bun's spurious EEXIST on Windows (#2329)

Bun on Windows throws EEXIST from a recursive mkdir of an existing directory,
so gen:skill-docs and the llms.txt writer crashed once a second file landed
in a directory. Both now use lib/fs-utils' mkdirpSync (ported from PR #2329
onto the existing helper instead of a new scripts/fs-utils.ts).

Co-authored-by: Vaughan <vaughan@cairnsitguy.com.au>
This commit is contained in:
garrytanandVaughan committed 2026-10-03 21:38:00 +00:00
1 parent 9eda423a85
commit a1969d9287
3 files changed
+42 -2

No files matched your search

+2 -1
View File
@@ -19,6 +19,7 @@
import * as fs from 'fs';
import * as path from 'path';
import { mkdirpSync } from '../lib/fs-utils';
import { discoverTemplates } from './discover-skills';
import { COMMAND_DESCRIPTIONS as BROWSE_COMMANDS } from '../browse/src/commands';
@@ -224,7 +225,7 @@ export async function generateLlmsTxt(opts: GenerateOptions = {}): Promise<Gener
export async function writeLlmsTxt(opts: GenerateOptions & { outputPath?: string } = {}): Promise<GenerateResult> {
const result = await generateLlmsTxt(opts);
const outputPath = opts.outputPath ?? OUTPUT;
fs.mkdirSync(path.dirname(outputPath), { recursive: true });
mkdirpSync(path.dirname(outputPath));
fs.writeFileSync(outputPath, result.content, { encoding: 'utf-8' });
return result;
}
+2 -1
View File
@@ -29,6 +29,7 @@ import type { HostConfig } from './host-config';
const ROOT = path.resolve(import.meta.dir, '..');
import { ALL_MODEL_NAMES, resolveModel, type Model } from './models';
import { resolveStateRoot } from '../lib/state-root';
import { mkdirpSync } from '../lib/fs-utils';
type HostArg = Host | 'all';
@@ -967,7 +968,7 @@ export async function runGeneration(settings: GenerationOptions = {}): Promise<G
log(`FRESH: ${relativePath}`);
}
} else {
fs.mkdirSync(path.dirname(outputPath), { recursive: true });
mkdirpSync(path.dirname(outputPath));
fs.writeFileSync(outputPath, content);
log(`GENERATED: ${relativePath}`);
}
+38
View File
@@ -0,0 +1,38 @@
/**
* #2329: Bun on Windows throws EEXIST from mkdirSync(dir, { recursive: true })
* when dir already exists, so a second `gen:skill-docs` (or llms.txt write)
* crashed. Both generators now use lib/fs-utils' mkdirpSync. Emulated on every
* platform with the same --preload fixture as test/fs-utils.test.ts (#2635).
*/
import { afterAll, describe, expect, test } from 'bun:test';
import { spawnSync } from 'child_process';
import * as fs from 'fs';
import * as os from 'os';
import * as path from 'path';
const ROOT = path.resolve(import.meta.dir, '..');
const PRELOAD = path.join(import.meta.dir, 'helpers', 'emulate-bun-windows-eexist.ts');
const tmp = fs.mkdtempSync(path.join(os.tmpdir(), 'gstack-gen-eexist-'));
afterAll(() => fs.rmSync(tmp, { recursive: true, force: true }));
const bun = (...args: string[]) => spawnSync(process.execPath, ['--preload', PRELOAD, ...args], { cwd: ROOT, encoding: 'utf8', timeout: 120_000 });
describe('#2329: generators tolerate existing output directories under Windows EEXIST semantics', () => {
test('gen-skill-docs renders twice into the same out-dir', () => {
const out = path.join(tmp, 'render');
for (let run = 1; run <= 2; run++) {
const r = bun('scripts/gen-skill-docs.ts', '--host', 'codex', '--out-dir', out);
expect(`run ${run}: exit ${r.status}`, r.stderr).toBe(`run ${run}: exit 0`);
expect(r.stderr).not.toContain('EEXIST');
}
expect(fs.existsSync(path.join(out, '.agents', 'skills', 'gstack-review', 'SKILL.md'))).toBe(true);
});
test('writeLlmsTxt writes into an existing directory', () => {
const target = path.join(tmp, 'llms', 'llms.txt');
fs.mkdirSync(path.dirname(target), { recursive: true });
const r = bun('-e', `const { writeLlmsTxt } = await import(${JSON.stringify(path.join(ROOT, 'scripts/gen-llms-txt.ts'))}); await writeLlmsTxt({ outputPath: ${JSON.stringify(target)} });`);
expect(r.status, r.stderr).toBe(0);
expect(fs.readFileSync(target, 'utf8')).toContain('gstack');
});
});