// Generate SCOPES.md (the human-readable list of live domains) from the single source of truth, // src/config/scopes.ts. Docs must NOT hand-maintain the scope list: they drift the moment a domain // is added. Run: // // npm run gen:scopes # regenerate SCOPES.md // npm run check:scopes # fail if SCOPES.md is out of sync (also enforced by a vitest test) // // Node imports the .ts source natively (>=22.6 strip-types; CI/dev are on 26). No new deps. // Output is deterministic (no timestamp) so the git-diff / test gate only trips on real changes. import { writeFile } from 'node:fs/promises'; import { fileURLToPath, pathToFileURL } from 'node:url'; import { dirname, resolve } from 'node:path'; import { SCOPES } from '../src/config/scopes.ts'; /** Render SCOPES.md from a SCOPES map. Pure, so the vitest gate can compare it to the on-disk file. */ export function renderScopesMd(scopes) { // No maintainer column: the config stores manager DIDs (per the "reference DIDs internally, // resolve handles at display time" hard rule), which are not human-readable, and resolving them // would add a nondeterministic network call that breaks the diff/test gate. Managers render as // avatars on the live site instead. const rows = Object.entries(scopes).map(([id, s]) => { const domain = new URL(s.site).host; const type = s.aggregate ? 'aggregate' : s.tier === 'city' ? 'city' : 'country'; const langs = s.locales.join(', '); const verifier = s.verify ? 'yes' : 'no'; return `| ${domain} | \`${id}\` | ${type} | ${langs} | ${verifier} |`; }); const count = Object.keys(scopes).length; return ( [ '# Live scopes', '', '', '', 'Every domain below is a standalone build driven by `SITE_SCOPE`, deployed by the Tangled Spindle', 'to its own Codeberg Pages repo (`atproto-website`). Languages are the build locales,', 'default first. Deploy runbook: `AGENTS.md`. Colours per scope: `src/config/accents.ts`.', '', '| Domain | Scope | Type | Languages | Verifier |', '|--------|-------|------|-----------|----------|', ...rows, '', `${count} scopes.`, ].join('\n') + '\n' ); } /** The scope ids, space-separated, in declaration order. The deploy loop (deploy.yml) and any * manual fallback consume this so the publish set is derived from scopes.ts, never hand-listed: * adding a scope to scopes.ts is then a one-file change that the deployer picks up automatically. */ export function scopeList(scopes) { return Object.keys(scopes).join(' '); } // CLI. Guarded so importing this module (the test) never runs it. if (import.meta.url === pathToFileURL(process.argv[1]).href) { // `--list`: print the scope ids for the deployer to iterate. Prints ONLY the list (no extra // logging) so `for scope in $(node scripts/gen-scopes.mjs --list)` gets a clean word list. if (process.argv.includes('--list')) { process.stdout.write(scopeList(SCOPES) + '\n'); } else { const here = dirname(fileURLToPath(import.meta.url)); await writeFile(resolve(here, '../SCOPES.md'), renderScopesMd(SCOPES)); console.log(`wrote SCOPES.md (${Object.keys(SCOPES).length} scopes)`); } }