Files
2026-05-27 23:29:53 +01:00

132 lines
4.3 KiB
TypeScript

#!/usr/bin/env bun
import { existsSync, mkdirSync, readdirSync, readFileSync, statSync, writeFileSync } from "node:fs";
import { dirname, join, relative, resolve } from "node:path";
const PKG_ROOT = resolve(__dirname, "..");
const ASSETS_OUT = resolve(PKG_ROOT, "assets/skills");
const SOURCE_ROOT_DEFAULT = resolve(PKG_ROOT, "../../../../suaveplan-skills/plugins");
interface SkillEntry {
readonly id: string;
readonly version: string;
readonly dir: string;
readonly files: ReadonlyArray<string>;
}
function walk(dir: string, out: string[] = []): string[] {
let entries: string[];
try {
entries = readdirSync(dir);
} catch {
return out;
}
for (const name of entries) {
if (name === "node_modules" || name === ".git") continue;
const p = join(dir, name);
const st = statSync(p);
if (st.isDirectory()) {
walk(p, out);
} else {
out.push(p);
}
}
return out;
}
function ensureDir(path: string): void {
if (!existsSync(path)) mkdirSync(path, { recursive: true });
}
function copyFile(from: string, to: string): void {
ensureDir(dirname(to));
const bytes = readFileSync(from);
writeFileSync(to, bytes);
}
function discoverSkills(sourceRoot: string): SkillEntry[] {
if (!existsSync(sourceRoot)) {
console.warn(`[build-skills-bundle] no source at ${sourceRoot} — skipping`);
return [];
}
const skills: SkillEntry[] = [];
const plugins = readdirSync(sourceRoot);
for (const pluginName of plugins) {
const pluginPath = join(sourceRoot, pluginName);
if (!statSync(pluginPath).isDirectory()) continue;
const skillsDir = join(pluginPath, "skills");
if (!existsSync(skillsDir)) continue;
for (const skillName of readdirSync(skillsDir)) {
const skillSource = join(skillsDir, skillName);
if (!statSync(skillSource).isDirectory()) continue;
const skillManifest = join(skillSource, "SKILL.md");
if (!existsSync(skillManifest)) continue;
const targetDir = join(ASSETS_OUT, skillName);
ensureDir(targetDir);
const allFiles = walk(skillSource);
const fileList: string[] = [];
for (const file of allFiles) {
const rel = relative(skillSource, file);
copyFile(file, join(targetDir, rel));
fileList.push(rel.split("\\").join("/"));
}
skills.push({
id: skillName,
version: "0.1.0",
dir: skillName,
files: fileList.sort(),
});
}
}
return skills.sort((a, b) => a.id.localeCompare(b.id));
}
function discoverExistingSkills(): SkillEntry[] {
if (!existsSync(ASSETS_OUT)) {
return [];
}
const skills: SkillEntry[] = [];
for (const skillName of readdirSync(ASSETS_OUT)) {
const skillPath = join(ASSETS_OUT, skillName);
if (!statSync(skillPath).isDirectory()) continue;
const skillManifest = join(skillPath, "SKILL.md");
if (!existsSync(skillManifest)) continue;
const allFiles = walk(skillPath);
const fileList: string[] = [];
for (const file of allFiles) {
const rel = relative(skillPath, file);
fileList.push(rel.split("\\").join("/"));
}
skills.push({
id: skillName,
version: "0.1.0",
dir: skillName,
files: fileList.sort(),
});
}
return skills.sort((a, b) => a.id.localeCompare(b.id));
}
function writeManifest(skills: ReadonlyArray<SkillEntry>): void {
ensureDir(ASSETS_OUT);
const manifest = { schemaVersion: 1, skills };
writeFileSync(
join(ASSETS_OUT, "manifest.json"),
`${JSON.stringify(manifest, null, 2)}\n`
);
}
function main(): void {
const sourceRoot = process.env["SUAVEPLAN_SKILLS_ROOT"] ?? SOURCE_ROOT_DEFAULT;
let skills = discoverSkills(sourceRoot);
if (skills.length === 0) {
skills = discoverExistingSkills();
if (skills.length > 0) {
console.log(`[build-skills-bundle] source missing; regenerated manifest for ${skills.length} existing skills`);
}
}
writeManifest(skills);
console.log(`[build-skills-bundle] bundled ${skills.length} skills into ${ASSETS_OUT}`);
}
main();