#!/usr/bin/env node
import { readFileSync } from "node:fs";
import { resolve, dirname } from "node:path";
import { fileURLToPath } from "node:url";
import { spawn, spawnSync } from "node:child_process";
import os from "node:os";
const __dirname = dirname(fileURLToPath(import.meta.url));
const root = resolve(__dirname, "..");
const configPath = resolve(root, "boneyard.config.json");
const config = JSON.parse(readFileSync(configPath, "utf-8"));
const port = process.env.STORYBOOK_PORT || 6006;
const base = `http://127.0.0.1:${port}`;
const rawArgs = process.argv.slice(2);
const force = rawArgs.includes("--force");
const args = rawArgs.filter((a) => a !== "--force");
let urls = [];
if (args.length > 0) {
const patterns = args.map((a) => a.toLowerCase());
const matched = [];
for (const [name, entry] of Object.entries(config.skeletons || {})) {
if (patterns.some((p) => name.toLowerCase().includes(p))) {
matched.push(name);
urls.push(`${base}${entry.route}`);
}
}
if (urls.length === 0) {
console.error(`boneyard: no skeletons matched '${args.join(", ")}'`);
process.exit(1);
}
console.log(`boneyard: scoping to ${urls.length} skeleton(s): ${matched.join(", ")}\n`);
} else {
// For all skeletons, use explicit URLs so we can parallelize across workers
urls = Object.values(config.skeletons || {}).map((entry) => `${base}${entry.route}`);
}
const flags = force ? ["--force"] : [];
const defaultConcurrency = Math.min(6, Math.max(1, os.cpus().length - 2));
const concurrency = Math.min(
urls.length,
parseInt(process.env.BONES_CONCURRENCY || String(defaultConcurrency), 10)
);
async function run() {
if (concurrency <= 1 || urls.length <= 1) {
const res = spawnSync("pnpm", ["exec", "boneyard-js", "build", ...urls, ...flags], {
cwd: root,
stdio: "inherit"
});
if (res.status !== 0) process.exit(res.status ?? 1);
} else {
// Partition URLs across workers
const buckets = Array.from({ length: concurrency }, () => []);
urls.forEach((url, i) => {
buckets[i % concurrency].push(url);
});
console.log(
`boneyard: parallelizing ${urls.length} skeletons across ${concurrency} workers...\n`
);
const startTime = Date.now();
await Promise.all(
buckets.map((bucket, i) => {
return new Promise((resolveWorker, rejectWorker) => {
const child = spawn("pnpm", ["exec", "boneyard-js", "build", ...bucket, ...flags], {
cwd: root,
stdio: ["ignore", "pipe", "pipe"]
});
child.stdout.on("data", (d) => {
// Stream lines prefixed by worker id for clarity
const text = d.toString();
for (const line of text.split("\n")) {
if (line.trim()) console.log(`[w${i + 1}] ${line}`);
}
});
child.stderr.on("data", (d) => {
const text = d.toString();
for (const line of text.split("\n")) {
if (line.trim()) console.error(`[w${i + 1}] ${line}`);
}
});
child.on("close", (code) => {
if (code === 0) resolveWorker();
else rejectWorker(new Error(`Worker ${i + 1} exited with code ${code}`));
});
});
})
);
const elapsed = ((Date.now() - startTime) / 1000).toFixed(1);
console.log(`\nš all workers completed in ${elapsed}s!`);
}
// Clean up unused registry and format
spawnSync("rm", ["-f", "src/lib/bones/registry.ts"], { cwd: root });
spawnSync("pnpm", ["exec", "prettier", "--write", "src/lib/bones"], {
cwd: root,
stdio: "inherit"
});
}
run().catch((err) => {
console.error(err);
process.exit(1);
});