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));