From 475783556fa9a6386387263191d46d3af3cd3fa1 Mon Sep 17 00:00:00 2001 From: Irene Date: Wed, 9 Sep 2026 17:50:46 +0900 Subject: [PATCH] feat: add BOM generator script --- scripts/bom-gen.ts | 102 +++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 102 insertions(+) create mode 100644 scripts/bom-gen.ts diff --git a/scripts/bom-gen.ts b/scripts/bom-gen.ts new file mode 100644 index 0000000..5b954fe --- /dev/null +++ b/scripts/bom-gen.ts @@ -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((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);