import { spawnSync } from 'node:child_process'; import { mkdir, readFile, readdir, rm, writeFile } from 'node:fs/promises'; import { dirname, join, resolve } from 'node:path'; import process from 'node:process'; const target = process.argv[2]; if (!['staging', 'production'].includes(target)) { console.error( 'Usage: node scripts/environment-bootstrap-export.mjs [output.json]', ); process.exit(2); } const outputArgument = process.argv[3]; const timestamp = new Date() .toISOString() .replaceAll(':', '-') .replace(/\.\d{3}Z$/, 'Z'); const outputPath = resolve( outputArgument || `./data/backups/shd-${target}-bootstrap-${timestamp}.json`, ); const temporaryBackupPath = `${outputPath}.database.json`; const databaseBackup = spawnSync( process.execPath, ['scripts/db-backup.mjs', temporaryBackupPath], { cwd: process.cwd(), stdio: 'inherit', env: process.env, }, ); if (databaseBackup.error) throw databaseBackup.error; if (databaseBackup.status !== 0) throw new Error( `Database backup failed; no ${target} bootstrap bundle was created.`, ); async function collectFiles(directory, pattern) { try { const names = (await readdir(directory)) .filter((name) => pattern.test(name)) .sort(); return await Promise.all( names.map(async (name) => ({ name, contentBase64: (await readFile(join(directory, name))).toString( 'base64', ), })), ); } catch (error) { if (error?.code === 'ENOENT') return []; throw error; } } try { const database = JSON.parse(await readFile(temporaryBackupPath, 'utf8')); const avatars = await collectFiles( resolve(process.env.TDR_AVATAR_STORAGE_PATH || './data/avatars'), /^[0-9a-f-]{36}\.webp$/i, ); const gameIcons = await collectFiles( resolve(process.env.TDR_GAME_ICON_STORAGE_PATH || './data/game-icons'), /^[a-z_]+-[0-9a-f-]{36}\.webp$/i, ); const bundle = { format: 'shd-build-environment-bootstrap', version: 1, target, createdAt: new Date().toISOString(), database, files: { avatars, gameIcons }, }; await mkdir(dirname(outputPath), { recursive: true }); await writeFile(outputPath, `${JSON.stringify(bundle, null, 2)}\n`, { encoding: 'utf8', flag: 'wx', }); console.log( `${target} bootstrap bundle includes ${avatars.length} avatars and ${gameIcons.length} managed image assets.`, ); console.log(`Bootstrap bundle: ${outputPath}`); } finally { await rm(temporaryBackupPath, { force: true }); }