#!/usr/bin/env node /** * Syncs avatarDecorations.ts with what's actually available on the Nextcloud CDN. * * Usage: * npm run sync:decorations * * Workflow after deleting files from Nextcloud: * 1. Delete decoration files from your Nextcloud share. * 2. Run: npm run sync:decorations * 3. It probes each catalog slug via HTTP HEAD and removes entries * whose files returned 404. Empty categories are dropped automatically. * 4. Commit the updated avatarDecorations.ts. */ import { readFileSync, writeFileSync } from 'fs'; import { join, dirname } from 'path'; import { fileURLToPath } from 'url'; const __dirname = dirname(fileURLToPath(import.meta.url)); const root = join(__dirname, '..'); const catalogPath = join(root, 'src', 'app', 'features', 'lotus', 'avatarDecorations.ts'); const CDN = 'https://drive.lotusguild.org/public.php/dav/files/bHswJ9pNKp2t26N/cinny-decorations'; // Extract all slugs from the catalog file const catalog = readFileSync(catalogPath, 'utf8'); const slugMatches = [...catalog.matchAll(/slug: '([^']+)'/g)].map((m) => m[1]); if (slugMatches.length === 0) { console.error('No slugs found in catalog — check the file path.'); process.exit(1); } console.log(`Checking ${slugMatches.length} decorations against ${CDN} …`); console.log('(This makes one HEAD request per decoration)\n'); // Probe all slugs in parallel batches of 16 async function headCheck(slug) { try { const res = await fetch(`${CDN}/${slug}.png`, { method: 'HEAD' }); return { slug, ok: res.ok, status: res.status }; } catch { return { slug, ok: false, status: 0 }; } } const BATCH = 16; const results = []; for (let i = 0; i < slugMatches.length; i += BATCH) { const batch = slugMatches.slice(i, i + BATCH); const batchResults = await Promise.all(batch.map(headCheck)); results.push(...batchResults); } const missing = results.filter((r) => !r.ok); const found = results.filter((r) => r.ok); if (missing.length === 0) { console.log(`All ${found.length} decorations are available — catalog is up to date.`); process.exit(0); } console.log(`Found: ${found.length} Missing: ${missing.length}\n`); missing.forEach((r) => console.log(` Removing (HTTP ${r.status}): ${r.slug}`)); const missingSet = new Set(missing.map((r) => r.slug)); // Remove individual entries for missing slugs let updated = catalog.replace(/^[ \t]*\{ slug: '([^']+)', name: .+\},?\r?\n/gm, (match, slug) => missingSet.has(slug) ? '' : match, ); // Drop category blocks that now have an empty decorations array updated = updated.replace( / \{\n id: '[^']+',\n label: '[^']+',\n decorations: \[\n?[ \t]*\],?\n \},?\n/g, '', ); // Clean up stray blank lines updated = updated.replace(/\n{3,}/g, '\n\n'); writeFileSync(catalogPath, updated, 'utf8'); console.log( `\nDone. Removed ${missing.length} entr${missing.length === 1 ? 'y' : 'ies'} from the catalog.`, ); console.log('Review with: git diff src/app/features/lotus/avatarDecorations.ts');