Repository Public
1#!/usr/bin/env node2/**3 * Renders the PWA icons from the SVG sources with sharp (resolved through next's dependency tree).4 *5 * public/icons/ the default product (Claude Remote): SVG sources are hand-written6 * public/brand/codex/icons/ Codex Remote: the SAME drawings with the emerald → teal → cyan7 * gradient (globals.css `html[data-agent='codex']`), derived here8 * from the default SVGs, plus public/brand/codex/manifest.webmanifest.9 *10 * The server serves /icons/* and /manifest.webmanifest from out/brand/<agent>/ for a non-default11 * agent, so the file names and the icon URLs inside the manifest are the same for every product.12 * Usage: node dev/gen-icons.mjs [claude|codex|all] (default: all)13 */14import { createRequire } from 'node:module';15import fs from 'node:fs';16import path from 'node:path';17import { fileURLToPath } from 'node:url';1819const here = path.dirname(fileURLToPath(import.meta.url));20const require = createRequire(import.meta.url);21const sharp = createRequire(require.resolve('next/package.json'))('sharp');22const pub = path.join(here, '../public');23const jobs = [24 ['icon.svg', 'icon-192.png', 192],25 ['icon.svg', 'icon-512.png', 512],26 ['icon-maskable.svg', 'icon-maskable-192.png', 192],27 ['icon-maskable.svg', 'icon-maskable-512.png', 512],28 ['icon-maskable.svg', 'apple-touch-icon.png', 180], // iOS applies its own mask: full-bleed source29];30/** Gradient stops of the default drawings → the brand gradient of each other product. */31const DEFAULT_STOPS = ['#5B5BF0', '#8B5CF6', '#FF7A59'];32const BRANDS = {33 codex: { name: 'Codex Remote', stops: ['#047857', '#0D9488', '#0891B2'] },34};3536async function render(dir) {37 for (const [src, out, size] of jobs) {38 await sharp(path.join(dir, src), { density: 300 }).resize(size, size).png({ compressionLevel: 9 }).toFile(path.join(dir, out));39 console.log(`${path.relative(pub, path.join(dir, out))} ${size}x${size}`);40 }41}4243const which = process.argv[2] ?? 'all';44if (which === 'all' || which === 'claude') await render(path.join(pub, 'icons'));45for (const [id, brand] of Object.entries(BRANDS)) {46 if (which !== 'all' && which !== id) continue;47 const dir = path.join(pub, 'brand', id, 'icons');48 fs.mkdirSync(dir, { recursive: true });49 for (const svg of ['icon.svg', 'icon-maskable.svg']) {50 let text = fs.readFileSync(path.join(pub, 'icons', svg), 'utf8');51 DEFAULT_STOPS.forEach((from, i) => {52 if (!text.includes(from)) throw new Error(`${svg}: gradient stop ${from} not found`);53 text = text.replaceAll(from, brand.stops[i]);54 });55 fs.writeFileSync(path.join(dir, svg), text);56 }57 await render(dir);58 // Same manifest, other product name. Colours are the (shared) page background; icon URLs stay /icons/*.59 const manifest = JSON.parse(fs.readFileSync(path.join(pub, 'manifest.webmanifest'), 'utf8'));60 Object.assign(manifest, { name: brand.name, short_name: brand.name, description: `${brand.name} — MacLustr Command Center` });61 fs.writeFileSync(path.join(pub, 'brand', id, 'manifest.webmanifest'), JSON.stringify(manifest, null, 2) + '\n');62 console.log(`brand/${id}/manifest.webmanifest`);63}64