import {readFile} from 'node:fs/promises'; import {fileURLToPath} from 'node:url'; import {join,dirname} from 'node:path'; import {createHash} from 'node:crypto'; import {requiredPackImages} from './verify-character-pack.mjs'; const root=join(dirname(fileURLToPath(import.meta.url)),'..','seed','characters'); const base=process.env.PROMPT_IMPORT_BASE_URL||'http://127.0.0.1:3000'; const media=JSON.parse(await readFile(join(root,'..','..','src','content','managed-media.json'),'utf8')); const packFiles=pack=>Object.entries(media).filter(([name])=>name.startsWith(`apps/waterfall-wiki/seed/characters/${pack.directory}/`)).sort(([a],[b])=>a.localeCompare(b)); const packs=JSON.parse(await readFile(join(root,'packs.json'),'utf8')); async function verify(pack){ let cursor='',post;const seen=new Set(); do{const response=await fetch(base+'/api/feed?'+new URLSearchParams({section:'images',...(cursor?{cursor}:{})}));if(!response.ok)throw Error('Cannot verify character feed: '+response.status);const page=await response.json();post=page.posts.find(p=>p.slug===pack.slug);cursor=page.nextCursor||'';if(cursor&&seen.has(cursor))throw Error('Character feed cursor did not advance');seen.add(cursor);}while(!post&&cursor); if(!post)throw Error('Character pack missing: '+pack.slug); const files=packFiles(pack);if(files.length!==pack.count)throw Error('Seed file count mismatch: '+pack.slug); const hashes=files.map(([,asset])=>asset.sha256); const originals=requiredPackImages(hashes,post.assets.filter(a=>a.kind==='image')); for(const image of originals){const media=await fetch(`${base}/api/media/${image.id}`,{method:'HEAD'});if(!media.ok||media.headers.get('content-type')!=='image/png')throw Error('Original PNG unavailable');} console.log(`Verified: /p/${pack.slug} (${originals.length} original PNGs preserved; editorial covers allowed)`); } for(const pack of packs){ const existing=await fetch(`${base}/p/${pack.slug}`); if(existing.ok){console.log(`Already imported: /p/${pack.slug}`);await verify(pack);continue;} if(existing.status!==404)throw new Error(`Cannot check pack ${pack.slug}: ${existing.status}`); const files=packFiles(pack); if(files.length!==pack.count)throw new Error(`Expected ${pack.count} PNGs for ${pack.slug}, got ${files.length}`); const data=new FormData();data.set('title',pack.title);data.set('slug',pack.slug);data.set('description',pack.description); for(const [name,asset] of files){ const response=await fetch(`${base}/api/media/${asset.id}`);if(!response.ok)throw Error('Cannot read migrated original: '+name); const bytes=Buffer.from(await response.arrayBuffer());if(bytes.length!==asset.bytes||createHash('sha256').update(bytes).digest('hex')!==asset.sha256)throw Error('Original checksum mismatch: '+name); data.append('files',new Blob([bytes],{type:'image/png'}),name.split('/').pop()); } const response=await fetch(`${base}/api/skills/street-people`,{method:'POST',body:data}); if(response.status!==201&&response.status!==409)throw new Error(`Import ${pack.slug}: ${response.status} ${await response.text()}`); if(response.status===409&&!(await fetch(`${base}/p/${pack.slug}`)).ok)throw new Error(`Conflicting import ${pack.slug}`); console.log(`Imported: /p/${pack.slug} (${files.length} transparent PNGs)`); await verify(pack); }