Files
pbakaus_impeccable/scripts/publish-world-cards.mjs
T
Paul BakausandClaude Fable 5 7557935fdb Expand concept system: modes, ratings, re-roll, breadth strategy
Catalog: mode-aligned staging surfaces (persuade/operate/read/experience),
star ratings on approvals feeding challenger draw weights, family
retirements, authoring strategy and territory guide, rework and breadth
authoring rounds, composition mining from rejected worlds.

Seed: six challengers (two per tier), --reroll chains, --mode staging
filter, rating-weighted draws. New-work: Present/visualize/re-roll flow,
image-gen requirement, register-neutral vocabulary.

Pipeline: per-mode staging prompts with split frames, hero-from-board
reference generation, render-safety guards. Labs: ratings UI, unrated
filter, mode chips, composition approve-guard.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-20 22:10:10 -07:00

87 lines
3.1 KiB
JavaScript

#!/usr/bin/env node
/**
* Upload world design-system cards to the R2 bucket that backs
* functions/worlds/cards/[[file]].js. Skips files already uploaded at their
* current generation (tracked in a local .published.json sidecar), so routine
* runs only push what changed. Requires an authenticated wrangler (same auth
* as `bun run deploy`) and the bucket:
* wrangler r2 bucket create impeccable-world-cards
*
* Usage:
* bun run world-cards:publish # changed files only
* bun run world-cards:publish -- --force # everything
*/
import { execFile } from 'node:child_process';
import { readdirSync, readFileSync, writeFileSync, existsSync } from 'node:fs';
import { dirname, join } from 'node:path';
import { fileURLToPath } from 'node:url';
import { promisify } from 'node:util';
const run = promisify(execFile);
const ROOT = join(dirname(fileURLToPath(import.meta.url)), '..');
const CARD_DIR = join(ROOT, 'site', 'public', 'worlds', 'cards');
const STATE_PATH = join(CARD_DIR, '.published.json');
const BUCKET = 'impeccable-world-cards';
const CONCURRENCY = 6;
const force = process.argv.includes('--force');
const manifest = JSON.parse(readFileSync(join(CARD_DIR, 'manifest.json'), 'utf8'));
const state = existsSync(STATE_PATH) ? JSON.parse(readFileSync(STATE_PATH, 'utf8')) : {};
const files = readdirSync(CARD_DIR).filter(file => file.endsWith('.webp'));
const queue = files.filter(file => {
if (force) return true;
const id = file.replace(/(-hero)?\.webp$/, '');
const stamp = file.endsWith('-hero.webp') ? manifest[id]?.heroGeneratedAt : manifest[id]?.generatedAt;
return !stamp || state[file] !== stamp;
});
console.log(`publishing ${queue.length} of ${files.length} cards to r2://${BUCKET}`);
let done = 0;
let failed = 0;
async function upload(file) {
const contentType = 'image/webp';
await run('wrangler', [
'r2', 'object', 'put', `${BUCKET}/${file}`,
'--file', join(CARD_DIR, file),
'--content-type', contentType,
'--remote',
], { cwd: ROOT });
}
const worker = async () => {
while (queue.length > 0) {
const file = queue.shift();
try {
await upload(file);
const id = file.replace(/(-hero)?\.webp$/, '');
state[file] = (file.endsWith('-hero.webp') ? manifest[id]?.heroGeneratedAt : manifest[id]?.generatedAt) || new Date().toISOString();
writeFileSync(STATE_PATH, `${JSON.stringify(state, null, 2)}\n`);
done += 1;
if (done % 25 === 0 || queue.length === 0) console.log(` ${done} uploaded, ${queue.length} remaining`);
} catch (error) {
failed += 1;
console.error(` FAILED ${file}: ${error.message}`);
}
}
};
await Promise.all(Array.from({ length: CONCURRENCY }, worker));
try {
await run('wrangler', [
'r2', 'object', 'put', `${BUCKET}/manifest.json`,
'--file', join(CARD_DIR, 'manifest.json'),
'--content-type', 'application/json',
'--remote',
], { cwd: ROOT });
console.log(' manifest.json uploaded');
} catch (error) {
failed += 1;
console.error(` FAILED manifest.json: ${error.message}`);
}
console.log(`done: ${done} uploaded, ${failed} failed`);
if (failed > 0) process.exitCode = 1;