#!/usr/bin/env node /** * Write or verify every extension's README banner. * * node scripts/readme/banners.ts write extensions//assets/banner.svg * node scripts/readme/banners.ts --check exit 1 when any committed banner differs * node scripts/readme/banners.ts ... 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 { 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));