check-brand-assets.mjs2.9 KBView on GitHub #!/usr/bin/env node
/**
* Brand guard: Cedar's logo is the green frond, and nothing else may pose as it.
*
* This repo is a fork of Zero, and Zero's mark — a pixel-block glyph — shipped with it as
* `black-icon.svg` / `white-icon.svg`, wearing `alt="Cedar"` on the login page. It is not
* our logo and it read as ours everywhere it appeared.
*
* Deleting the files is not enough on its own: the mark is one `<path d="…">`, so it comes
* back the moment anyone pastes it under a new filename, or an agent lifts it out of git
* history looking for "the Cedar icon". So this checks for the SHAPE as well as the paths —
* the `d` attribute is the identity, and it survives renaming, recolouring and reformatting.
*
* Run by `pnpm precommit`. To add a genuinely new brand asset, put it in
* apps/mail/public/ and reference it by path; do not inline a logo as SVG source.
*/
import { execFileSync } from 'node:child_process';
/** Distinctive leading run of Zero's mark path. Recolouring or renaming does not change it. */
const ZERO_MARK_PATH_DATA = 'M38.125 190.625V152.5H0V38.125H38.125V0H152.5V38.125H190.625';
/** Filenames the mark shipped under, so a restored file is caught even if reformatted. */
const BANNED_PATHS = ['black-icon.svg', 'white-icon.svg'];
/** The real thing, named here so an error message can point somewhere useful. */
const CEDAR_LOGO = 'apps/mail/public/CedarLogoTransparent.png';
function tracked() {
return execFileSync('git', ['ls-files'], { encoding: 'utf8' }).split('\n').filter(Boolean);
}
function grep(pattern) {
try {
// -F: the path data contains regex metacharacters and is matched literally.
return execFileSync('git', ['grep', '-lF', pattern], { encoding: 'utf8' })
.split('\n')
.filter(Boolean);
} catch (err) {
// git grep exits 1 with no output when there are no matches — that is the good case.
if (err.status === 1) return [];
throw err;
}
}
const failures = [];
for (const file of tracked()) {
const base = file.split('/').pop();
if (BANNED_PATHS.includes(base)) {
failures.push(`${file} — Zero's mark, deleted deliberately. Do not restore it.`);
}
}
for (const file of grep(ZERO_MARK_PATH_DATA)) {
// This file is allowed to name the shape; that is its whole job.
if (file === 'scripts/check-brand-assets.mjs') continue;
failures.push(`${file} — contains Zero's mark path data.`);
}
for (const file of grep('/black-icon.svg').concat(grep('/white-icon.svg'))) {
if (file === 'scripts/check-brand-assets.mjs') continue;
failures.push(`${file} — references a deleted Zero mark.`);
}
if (failures.length) {
console.error("Brand check failed. Cedar's logo is the green frond, and this is not it:\n");
for (const f of new Set(failures)) console.error(` ✗ ${f}`);
console.error(`\nUse ${CEDAR_LOGO} (or a sibling in apps/mail/public/) instead.`);
process.exit(1);
}
console.log('Brand check passed.');