Luigit
repositories / pi-ext

pi-ext

bugabingas pi extensions

owned by admin

scripts/readme/banners.ts

Raw
#!/usr/bin/env node
/**
 * Write or verify every extension's README banner.
 *
 *   node scripts/readme/banners.ts            write extensions/<name>/assets/banner.svg
 *   node scripts/readme/banners.ts --check    exit 1 when any committed banner differs
 *   node scripts/readme/banners.ts <name>...  limit to the named extensions
 *
 * `--check` ignores positional arguments: Mise forwards trailing task names
 * (`mise run check lint`) to every task in a run list.
 *
 * Per-extension overrides live in package.json `pi-ext.logo`: `seed`,
 * `mutation`, `version`. The palette is assets/nugu.json, installed by the
 * nugu `pi-ext` recipe.
 */
import {
	existsSync,
	mkdirSync,
	readdirSync,
	readFileSync,
	writeFileSync,
} from "node:fs";
import { join, relative, resolve } from "node:path";
import { pathToFileURL } from "node:url";
import { ROOT, readJson } from "../lib/common.ts";
import {
	type BannerOptions,
	type NuguPalette,
	renderBanner,
} from "./banner.ts";

export const BANNER_PATH = "assets/banner.svg";
const PALETTE_PATH = join(ROOT, "assets", "nugu.json");

interface LogoDeclaration {
	seed?: number;
	mutation?: number;
	version?: number;
}

function logoOptions(name: string, dir: string): Partial<BannerOptions> {
	const pkg = readJson(join(dir, "package.json")) as {
		"pi-ext"?: { logo?: LogoDeclaration };
	};
	const logo = pkg["pi-ext"]?.logo ?? {};
	for (const [key, value] of Object.entries(logo)) {
		if (!["seed", "mutation", "version"].includes(key))
			throw new Error(`${name}: unknown pi-ext.logo key "${key}"`);
		if (!Number.isInteger(value) || (value as number) < 0)
			throw new Error(
				`${name}: pi-ext.logo.${key} must be a non-negative integer`,
			);
	}
	return logo;
}

export function extensionDirs(selected: string[] = []): string[] {
	const root = join(ROOT, "extensions");
	const all = readdirSync(root, { withFileTypes: true })
		.filter(
			(entry) =>
				entry.isDirectory() &&
				existsSync(join(root, entry.name, "package.json")),
		)
		.map((entry) => entry.name)
		.sort();
	if (!selected.length) return all;
	for (const name of selected)
		if (!all.includes(name)) throw new Error(`unknown extension: ${name}`);
	return selected;
}

export function expectedBanner(name: string): string {
	const palette = readJson(PALETTE_PATH) as NuguPalette;
	const dir = join(ROOT, "extensions", name);
	return renderBanner(name, palette, logoOptions(name, dir));
}

function main(argv: string[]): number {
	const check = argv.includes("--check");
	const names = extensionDirs(
		check ? [] : argv.filter((arg) => !arg.startsWith("--")),
	);
	const stale: string[] = [];
	for (const name of names) {
		const target = join(ROOT, "extensions", name, BANNER_PATH);
		const expected = expectedBanner(name);
		const current = existsSync(target)
			? readFileSync(target, "utf8")
			: undefined;
		if (current === expected) continue;
		if (check) {
			stale.push(relative(ROOT, target));
			continue;
		}
		mkdirSync(join(target, ".."), { recursive: true });
		writeFileSync(target, expected, "utf8");
		console.log(`wrote ${relative(ROOT, target)}`);
	}
	if (check && stale.length) {
		console.error(
			`banners out of date (run \`mise run //:banners\`):\n${stale.map((p) => `  ${p}`).join("\n")}`,
		);
		return 1;
	}
	if (check) console.log(`OK banners ${names.length}`);
	return 0;
}

if (
	process.argv[1] &&
	import.meta.url === pathToFileURL(resolve(process.argv[1])).href
)
	process.exitCode = main(process.argv.slice(2));