#!/usr/bin/env node /** * One-shot importer: reads script_metadata.json (the format push_scripts.sh * writes) and populates the electric-cherry SQLite DB with the equivalent * rows. Idempotent — safe to re-run, will INSERT OR IGNORE on collisions. * * Trigger: docker/entrypoint.sh calls this when LEGACY_METADATA is set and * DB_PATH is empty. */ import { readFile, rename, stat } from 'node:fs/promises'; import { existsSync } from 'node:fs'; import { env, exit } from 'node:process'; import { db } from '../src/lib/server/db/index.js'; interface LegacyServer { komodo_type?: string; komodo_name?: string; komodo_server?: string; iw4m_match?: string; iw4m_value?: string; } interface LegacyBetaScript { staged_as: string; target: string; beta_file: string; backup: string; sha256: string; revision?: number; deployed?: Array<{ server: string; had_original: boolean }>; } interface LegacyBeta { name: string; folder: string; opened: string; updated?: string; revision?: number; servers?: string[]; scripts?: LegacyBetaScript[]; } interface LegacyDeployServer { server: string; files: number; restart: string; players: number | null; checked: string | null; restarted: string | null; attempts: number; error: string | null; } interface LegacyDeployment { id: string; action: string; started: string; servers?: LegacyDeployServer[]; } interface LegacyRoot { servers?: Record; betas?: Record; deployments?: LegacyDeployment[]; } async function main(): Promise { const src = env.LEGACY_METADATA; if (!src) { console.error('LEGACY_METADATA env var not set'); exit(1); } if (!existsSync(src)) { console.log(`no legacy file at ${src} — nothing to import`); return; } const stats = await stat(src); if (stats.size === 0) { console.log(`legacy file at ${src} is empty — nothing to import`); return; } const raw = await readFile(src, 'utf8'); const data = JSON.parse(raw) as LegacyRoot; const handle = db(); let servers = 0; let betas = 0; let betaScripts = 0; let betaDeploys = 0; let deployments = 0; let deploymentServers = 0; const tx = handle.transaction(() => { // servers const insServer = handle.prepare( `INSERT OR IGNORE INTO servers (name, komodo_type, komodo_name, komodo_server, iw4m_match, iw4m_value) VALUES (?, ?, ?, ?, ?, ?)` ); for (const [name, s] of Object.entries(data.servers ?? {})) { const r = insServer.run( name, s.komodo_type ?? 'deployment', s.komodo_name ?? name.replace(/_/g, '-'), s.komodo_server ?? null, s.iw4m_match ?? null, s.iw4m_value ?? null ); if (r.changes > 0) servers++; } // betas const insBeta = handle.prepare( `INSERT OR IGNORE INTO betas (slug, name, folder, opened_at, updated_at, revision) VALUES (?, ?, ?, ?, ?, ?)` ); const insScript = handle.prepare( `INSERT INTO beta_scripts (beta_slug, staged_as, target, beta_file, backup, sha256, revision) VALUES (?, ?, ?, ?, ?, ?, ?)` ); const insDeploy = handle.prepare( `INSERT OR IGNORE INTO beta_deploys (beta_script_id, server, had_original) VALUES (?, ?, ?)` ); for (const [slug, b] of Object.entries(data.betas ?? {})) { const br = insBeta.run(slug, b.name, b.folder, b.opened, b.updated ?? b.opened, b.revision ?? 1); if (br.changes > 0) betas++; for (const s of b.scripts ?? []) { const sr = insScript.run( slug, s.staged_as, s.target, s.beta_file, s.backup, s.sha256, s.revision ?? 1 ); betaScripts++; const scriptId = Number(sr.lastInsertRowid); for (const d of s.deployed ?? []) { // Skip deploys whose server isn't tracked — matches the // FK constraint. Log so we don't silently lose rows. const exists = handle.prepare('SELECT 1 FROM servers WHERE name = ?').get(d.server); if (!exists) { console.warn(`skipping beta_deploy for unknown server ${d.server}`); continue; } const dr = insDeploy.run(scriptId, d.server, d.had_original ? 1 : 0); if (dr.changes > 0) betaDeploys++; } } } // deployments const insDep = handle.prepare( `INSERT OR IGNORE INTO deployments (id, action, started_at, triggered_by) VALUES (?, ?, ?, 'legacy')` ); const insDepSrv = handle.prepare( `INSERT OR IGNORE INTO deployment_servers (deployment_id, server, files_changed, restart_state, players, checked_at, restarted_at, attempts, error) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)` ); for (const d of data.deployments ?? []) { const r = insDep.run(d.id, d.action, d.started); if (r.changes > 0) deployments++; for (const s of d.servers ?? []) { const exists = handle.prepare('SELECT 1 FROM servers WHERE name = ?').get(s.server); if (!exists) continue; const sr = insDepSrv.run( d.id, s.server, s.files ?? 0, s.restart ?? 'manual', s.players ?? null, s.checked ?? null, s.restarted ?? null, s.attempts ?? 0, s.error ?? null ); if (sr.changes > 0) deploymentServers++; } } }); tx(); console.log( `imported: ${servers} servers, ${betas} betas (${betaScripts} scripts, ${betaDeploys} deploys), ` + `${deployments} deployments (${deploymentServers} server rows)` ); // Move the source out of the way so we don't re-import on future boots. const moved = `${src}.migrated`; await rename(src, moved); console.log(`renamed ${src} → ${moved}`); } main().catch((err) => { console.error('legacy import failed:', err); exit(2); });