Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
29 changes: 29 additions & 0 deletions scripts/fs-utils.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import * as fs from 'fs';

/**
* Drop-in replacement for fs.mkdirSync(dir, { recursive: true }).
*
* Bun on Windows (still present in 1.3.11) throws EEXIST from recursive
* mkdirSync when the target directory already exists AND carries the
* FILE_ATTRIBUTE_READONLY attribute (harmless on directories - Explorer
* repurposes it as a "customized folder" marker). Node treats the same
* call as a no-op success. See oven-sh/bun#16466 (the plain-dir variant,
* fixed) - the ReadOnly variant still reproduces.
*
* Guard: swallow EEXIST only after confirming the path is an existing
* directory; a collision with a regular file still throws.
*/
export function mkdirpSync(dir: string): void {
try {
fs.mkdirSync(dir, { recursive: true });
} catch (error) {
if ((error as { code?: string }).code === 'EEXIST') {
try {
if (fs.statSync(dir).isDirectory()) return;
} catch {
// fall through and rethrow the original mkdir error
}
}
throw error;
}
}
3 changes: 2 additions & 1 deletion scripts/gen-llms-txt.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
import * as fs from 'fs';
import * as path from 'path';
import { discoverTemplates } from './discover-skills';
import { mkdirpSync } from './fs-utils';
import { COMMAND_DESCRIPTIONS as BROWSE_COMMANDS } from '../browse/src/commands';

const ROOT = path.resolve(import.meta.dir, '..');
Expand Down Expand Up @@ -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;
}
Expand Down
11 changes: 6 additions & 5 deletions scripts/gen-skill-docs.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import { COMMAND_DESCRIPTIONS } from '../browse/src/commands';
import { SNAPSHOT_FLAGS } from '../browse/src/snapshot';
import { discoverTemplates, discoverSectionTemplates } from './discover-skills';
import { writeLlmsTxt } from './gen-llms-txt';
import { mkdirpSync } from './fs-utils';
import * as fs from 'fs';
import * as path from 'path';
import type { Host, TemplateContext } from './resolvers/types';
Expand Down Expand Up @@ -752,7 +753,7 @@ function processExternalHost(

const name = externalSkillName(skillDir === '.' ? '' : skillDir, frontmatterName);
const outputDir = path.join(ROOT, hostConfig.hostSubdir, 'skills', name);
fs.mkdirSync(outputDir, { recursive: true });
mkdirpSync(outputDir);
const outputPath = path.join(outputDir, 'SKILL.md');

// Guard against symlink loops
Expand Down Expand Up @@ -787,7 +788,7 @@ function processExternalHost(
// Config-driven: generate metadata (e.g., openai.yaml for Codex)
if (hostConfig.generation.generateMetadata && !symlinkLoop) {
const agentsDir = path.join(outputDir, 'agents');
fs.mkdirSync(agentsDir, { recursive: true });
mkdirpSync(agentsDir);
const shortDescription = condenseOpenAIShortDescription(extractedDescription);
fs.writeFileSync(path.join(agentsDir, 'openai.yaml'), generateOpenAIYaml(name, shortDescription));
}
Expand Down Expand Up @@ -921,7 +922,7 @@ function processSectionTemplate(
const externalName = externalSkillName(skillDir, parentName);
outputPath = path.join(ROOT, hostConfig.hostSubdir, 'skills', externalName, 'sections', fileName);
}
if (!DRY_RUN) fs.mkdirSync(path.dirname(outputPath), { recursive: true });
if (!DRY_RUN) mkdirpSync(path.dirname(outputPath));
return { outputPath, content };
}

Expand Down Expand Up @@ -996,7 +997,7 @@ for (const currentHost of hostsToRun) {
} else {
// In-place writes land in existing dirs; --out-dir needs the mirrored
// skill dir created first.
if (OUT_DIR) fs.mkdirSync(path.dirname(outputPath), { recursive: true });
if (OUT_DIR) mkdirpSync(path.dirname(outputPath));
fs.writeFileSync(outputPath, content);
console.log(`GENERATED: ${relOutput}`);
}
Expand Down Expand Up @@ -1058,7 +1059,7 @@ for (const currentHost of hostsToRun) {
// Generate gstack-lite and gstack-full for OpenClaw host
if (currentHost === 'openclaw' && !DRY_RUN) {
const openclawDir = path.join(ROOT, 'openclaw');
if (!fs.existsSync(openclawDir)) fs.mkdirSync(openclawDir, { recursive: true });
mkdirpSync(openclawDir);

const gstackLite = `# gstack-lite Planning Discipline

Expand Down