2026-06-14 12:02:50 -04:00
|
|
|
#!/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');
|
|
|
|
|
|
2026-06-15 20:50:00 -04:00
|
|
|
const CDN = 'https://drive.lotusguild.org/public.php/dav/files/bHswJ9pNKp2t26N/cinny-decorations';
|
2026-06-14 12:02:50 -04:00
|
|
|
|
|
|
|
|
// 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`);
|
2026-06-15 20:50:00 -04:00
|
|
|
missing.forEach((r) => console.log(` Removing (HTTP ${r.status}): ${r.slug}`));
|
2026-06-14 12:02:50 -04:00
|
|
|
|
|
|
|
|
const missingSet = new Set(missing.map((r) => r.slug));
|
|
|
|
|
|
|
|
|
|
// Remove individual entries for missing slugs
|
2026-06-15 20:50:00 -04:00
|
|
|
let updated = catalog.replace(/^[ \t]*\{ slug: '([^']+)', name: .+\},?\r?\n/gm, (match, slug) =>
|
|
|
|
|
missingSet.has(slug) ? '' : match,
|
2026-06-14 12:02:50 -04:00
|
|
|
);
|
|
|
|
|
|
|
|
|
|
// 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');
|
2026-06-15 20:50:00 -04:00
|
|
|
console.log(
|
|
|
|
|
`\nDone. Removed ${missing.length} entr${missing.length === 1 ? 'y' : 'ies'} from the catalog.`,
|
|
|
|
|
);
|
2026-06-14 12:02:50 -04:00
|
|
|
console.log('Review with: git diff src/app/features/lotus/avatarDecorations.ts');
|