import { connect } from '@tursodatabase/database'; import { spawnSync } from 'node:child_process'; import { mkdir, readFile, writeFile } from 'node:fs/promises'; import { join, resolve } from 'node:path'; import { randomUUID } from 'node:crypto'; const bundleArgument = process.argv .slice(2) .find((argument) => !argument.startsWith('--')); if (!bundleArgument || !process.argv.includes('--confirm-merge')) { console.error( 'Usage: node scripts/managed-images-import.mjs --confirm-merge [--update-existing]', ); process.exit(2); } const updateExisting = process.argv.includes('--update-existing'); const bundlePath = resolve(bundleArgument); const bundle = JSON.parse(await readFile(bundlePath, 'utf8')); if ( bundle?.format !== 'shd-build-managed-images' || bundle?.version !== 1 || !Array.isArray(bundle.images) ) { throw new Error( 'The selected file is not a supported SHD.build managed-image bundle.', ); } for (const image of bundle.images) { if ( typeof image?.row?.id !== 'string' || !/^[0-9a-f-]{36}$/i.test(image.row.id) || typeof image.row.storage_key !== 'string' || !/^(?:library|talent|brand_set|os_protocol|skill_chip|specialization|specialization_ability|specialization_node)-[0-9a-f-]{36}\.webp$/i.test( image.row.storage_key, ) || typeof image.contentBase64 !== 'string' ) { throw new Error('The bundle contains an invalid managed image.'); } const isItemRender = ['weapon_render', 'gear_render'].includes( image.row.source_entity_type, ); if ( image.row.source_entity_type && (typeof image.row.source_entity_id !== 'string' || !/^[0-9a-f-]{36}$/i.test(image.row.source_entity_id) || (isItemRender ? !image.row.storage_key.startsWith('library-') : !image.row.storage_key.startsWith( `${image.row.source_entity_type}-`, ))) ) throw new Error('The bundle contains an invalid linked ISAC image.'); } const backup = spawnSync(process.execPath, ['scripts/db-backup.mjs'], { cwd: process.cwd(), stdio: 'inherit', env: process.env, }); if (backup.error) throw backup.error; if (backup.status !== 0) throw new Error('Safety backup failed; no managed images were imported.'); const assetBackup = spawnSync( process.execPath, ['scripts/isac-data-export.mjs'], { cwd: process.cwd(), stdio: 'inherit', env: process.env, }, ); if (assetBackup.error) throw assetBackup.error; if (assetBackup.status !== 0) throw new Error( 'Uploaded-asset safety snapshot failed; no managed images were imported.', ); const databasePath = resolve( process.env.TDR_DATABASE_PATH || './data/shd-build-development.db', ); const storageDirectory = resolve( process.env.TDR_GAME_ICON_STORAGE_PATH || './data/game-icons', ); const database = await connect(databasePath); await mkdir(storageDirectory, { recursive: true }); let inserted = 0; let updated = 0; let unchanged = 0; const preflight = []; for (const image of bundle.images) { const row = image.row; const existing = await ( await database.prepare('SELECT * FROM managed_images WHERE id=?') ).get(row.id); if (existing && !updateExisting) { const same = [ 'name', 'category', 'tags_json', 'is_icon', 'force_white', 'storage_key', 'byte_size', 'width', 'height', 'source_entity_type', 'source_entity_id', ].every((key) => String(existing[key]) === String(row[key])); if (!same) throw new Error( `Image ${row.id} already exists with different metadata. Re-run with --update-existing after review.`, ); unchanged++; continue; } const creator = await ( await database.prepare('SELECT id FROM users WHERE id=?') ).get(row.created_by); if (!creator) throw new Error( `Image "${row.name}" references a creator that does not exist in the target database.`, ); const nameConflict = await ( await database.prepare( 'SELECT id FROM managed_images WHERE name=? COLLATE NOCASE AND id<>?', ) ).get(row.name, row.id); if (nameConflict) throw new Error( `Image name "${row.name}" is already assigned to a different target asset.`, ); if (row.source_entity_type && row.source_entity_id) { const sourceConflict = await ( await database.prepare( 'SELECT id FROM managed_images WHERE source_entity_type=? AND source_entity_id=? AND id<>?', ) ).get(row.source_entity_type, row.source_entity_id, row.id); if (sourceConflict) throw new Error( `The ${row.source_entity_type} image ${row.source_entity_id} is already registered under a different library asset.`, ); } preflight.push({ image, existing }); } await database.exec('BEGIN'); try { for (const { image, existing } of preflight) { const row = image.row; await writeFile( join(storageDirectory, row.storage_key), Buffer.from(image.contentBase64, 'base64'), { mode: 0o600, flag: existing || row.source_entity_type ? 'w' : 'wx', }, ); if (existing) { await ( await database.prepare(`UPDATE managed_images SET name=?,category=?,tags_json=?,is_icon=?,force_white=?,storage_key=?,original_filename=?, byte_size=?,width=?,height=?,source_entity_type=?,source_entity_id=?,updated_at=? WHERE id=?`) ).run( row.name, row.category, row.tags_json, row.is_icon, row.force_white, row.storage_key, row.original_filename, row.byte_size, row.width, row.height, row.source_entity_type ?? null, row.source_entity_id ?? null, row.updated_at, row.id, ); updated++; } else { await ( await database.prepare(`INSERT INTO managed_images (id,name,category,tags_json,is_icon,force_white,storage_key,original_filename,byte_size,width,height, source_entity_type,source_entity_id,created_by,created_at,updated_at) VALUES(?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)`) ).run( row.id, row.name, row.category, row.tags_json, row.is_icon, row.force_white, row.storage_key, row.original_filename, row.byte_size, row.width, row.height, row.source_entity_type ?? null, row.source_entity_id ?? null, row.created_by, row.created_at, row.updated_at, ); inserted++; } await ( await database.prepare(`INSERT INTO audit_log (id,actor_id,entity_type,entity_id,action,before_json,after_json,created_at) VALUES(?,?,?,?,?,?,?,?)`) ).run( randomUUID(), row.created_by, 'system_configuration', row.id, existing ? 'image.import_updated' : 'image.imported', existing ? JSON.stringify(existing) : null, JSON.stringify(row), new Date().toISOString(), ); } await database.exec('COMMIT'); } catch (error) { await database.exec('ROLLBACK'); throw error; } console.log( `Managed-image merge complete: ${inserted} inserted, ${updated} updated, ${unchanged} unchanged.`, );