feat: add BOM generator script
This commit is contained in:
@@ -0,0 +1,102 @@
|
|||||||
|
#!/usr/bin/env node
|
||||||
|
// BOM generator — walks asset directories, lists files, prompts for descriptions, outputs BOM.yaml
|
||||||
|
//
|
||||||
|
// Usage:
|
||||||
|
// npx tsx scripts/bom-gen.ts [asset-root]
|
||||||
|
//
|
||||||
|
// Expected directory structure:
|
||||||
|
// audio/ — audio files
|
||||||
|
// images/ — image files
|
||||||
|
// fonts/ — font files
|
||||||
|
// videos/ — material video clips
|
||||||
|
//
|
||||||
|
// Output: BOM.yaml (YAML frontmatter-style)
|
||||||
|
|
||||||
|
import { readdirSync, statSync, writeFileSync } from "node:fs";
|
||||||
|
import { resolve, relative, join } from "node:path";
|
||||||
|
import { createInterface } from "node:readline";
|
||||||
|
|
||||||
|
type Asset = { path: string; size: number; description: string };
|
||||||
|
|
||||||
|
const ASSET_DIRS = ["audio", "images", "fonts", "videos"];
|
||||||
|
|
||||||
|
function walk(dir: string): Asset[] {
|
||||||
|
const assets: Asset[] = [];
|
||||||
|
for (const entry of readdirSync(dir, { withFileTypes: true })) {
|
||||||
|
const full = resolve(dir, entry.name);
|
||||||
|
if (entry.isDirectory()) {
|
||||||
|
assets.push(...walk(full));
|
||||||
|
} else {
|
||||||
|
assets.push({
|
||||||
|
path: relative(".", full),
|
||||||
|
size: statSync(full).size,
|
||||||
|
description: "",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return assets;
|
||||||
|
}
|
||||||
|
|
||||||
|
function humanSize(bytes: number): string {
|
||||||
|
const units = ["B", "KB", "MB", "GB"];
|
||||||
|
let i = 0;
|
||||||
|
while (bytes >= 1024 && i < units.length - 1) {
|
||||||
|
bytes /= 1024;
|
||||||
|
i++;
|
||||||
|
}
|
||||||
|
return `${bytes.toFixed(1)} ${units[i]}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function main() {
|
||||||
|
const root = process.argv[2] || ".";
|
||||||
|
const rl = createInterface({ input: process.stdin, output: process.stdout });
|
||||||
|
|
||||||
|
console.log("BOM Generator");
|
||||||
|
console.log("=============\n");
|
||||||
|
|
||||||
|
let all: Asset[] = [];
|
||||||
|
|
||||||
|
for (const dir of ASSET_DIRS) {
|
||||||
|
const full = resolve(root, dir);
|
||||||
|
try {
|
||||||
|
const assets = walk(full);
|
||||||
|
all.push(...assets);
|
||||||
|
console.log(` ${dir}/ — ${assets.length} file(s)`);
|
||||||
|
} catch {
|
||||||
|
console.log(` ${dir}/ — not found (skipped)`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (all.length === 0) {
|
||||||
|
console.log("\nNo assets found. Nothing to do.\n");
|
||||||
|
rl.close();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const totalSize = all.reduce((s, a) => s + a.size, 0);
|
||||||
|
console.log(`\nTotal: ${all.length} file(s), ${humanSize(totalSize)}\n`);
|
||||||
|
|
||||||
|
for (let i = 0; i < all.length; i++) {
|
||||||
|
const a = all[i];
|
||||||
|
console.log(`[${i + 1}/${all.length}] ${a.path} (${humanSize(a.size)})`);
|
||||||
|
a.description = (await new Promise<string>((r) =>
|
||||||
|
rl.question(" Description: ", r)
|
||||||
|
)).trim();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Build YAML manually (no dependency)
|
||||||
|
let yaml = "---\nbom:\n";
|
||||||
|
for (const a of all) {
|
||||||
|
const desc = a.description.replace(/"/g, '\\"');
|
||||||
|
yaml += ` ${a.path}:\n description: "${desc}"\n`;
|
||||||
|
}
|
||||||
|
yaml += `total_files: ${all.length}\ntotal_size: ${totalSize}\n...`;
|
||||||
|
|
||||||
|
const bomPath = resolve(root, "BOM.yaml");
|
||||||
|
writeFileSync(bomPath, yaml);
|
||||||
|
console.log(`\n✓ Written to ${relative(root, bomPath)}\n`);
|
||||||
|
|
||||||
|
rl.close();
|
||||||
|
}
|
||||||
|
|
||||||
|
main().catch(console.error);
|
||||||
Reference in New Issue
Block a user