#!/usr/bin/env bun // Dev/debug CLI (design §8) — reuses the engine one-shot. `scan`/`detect`/`preview` are offline (no // host needed); `sync`/`uninstall` connect to the host and reconcile. Handy for configuring a headless // box or checking what a `config.json` will produce before pushing it to the library. // // bunx punktfunk-plugin-rom-manager scan # what the scanner finds // bunx punktfunk-plugin-rom-manager detect # which emulators are installed // bunx punktfunk-plugin-rom-manager preview # the desired library (no host write) // bunx punktfunk-plugin-rom-manager sync # reconcile into the live library // bunx punktfunk-plugin-rom-manager uninstall # remove all this plugin's entries // bunx punktfunk-plugin-rom-manager set-password # standalone-UI password import { connect } from "@punktfunk/host"; import type { Config } from "./config.js"; import { detectAll, realDetectEnv, resolveEmulators } from "./emulators.js"; import { Engine } from "./engine/index.js"; import { computeDesired, enumerateTitles } from "./engine/reconcile.js"; import { type RomCandidate, realScanFs, scanRoot } from "./engine/scanner.js"; import { resolvePlatforms } from "./platforms.js"; import { currentOs } from "./quote.js"; import { hashPassword } from "./server/password.js"; import { loadCache, loadConfig, saveConfig } from "./state.js"; const scanAll = (config: Config): RomCandidate[] => { const platforms = resolvePlatforms(config.platforms); const out: RomCandidate[] = []; for (const root of config.roots) { const platform = platforms.get(root.platform); if (!platform) { console.error(`! root ${root.dir}: unknown platform "${root.platform}"`); continue; } out.push(...scanRoot(realScanFs, root.dir, platform, root.excludes)); } return out; }; const cmdScan = (): void => { const config = loadConfig(); if (config.roots.length === 0) { console.log( "No ROM roots configured. Add some to config.json or via the UI.", ); return; } const scan = scanAll(config); const { survivors, skipped } = enumerateTitles(config, scan); const perPlatform = new Map(); for (const s of survivors) perPlatform.set(s.platform.id, (perPlatform.get(s.platform.id) ?? 0) + 1); console.log( `Scanned ${config.roots.length} root(s): ${scan.length} candidate file(s), ${survivors.length} title(s).`, ); for (const [id, n] of [...perPlatform].sort()) console.log(` ${id}: ${n}`); if (skipped.length) console.log(` (${skipped.length} skipped by exclude/dedupe)`); }; const cmdDetect = (): void => { const config = loadConfig(); const detected = detectAll( realDetectEnv(), resolveEmulators(config.emulators).values(), ); if (detected.length === 0) { console.log("No emulators detected."); return; } console.log(`Detected ${detected.length} emulator(s):`); for (const d of detected) { const cores = d.cores?.length ? ` — ${d.cores.length} core(s)` : ""; console.log( ` ${d.id} (${d.via})${d.contested ? " [contested]" : ""}${cores}`, ); } }; const cmdPreview = (): void => { const config = loadConfig(); const scan = scanAll(config); const detected = detectAll( realDetectEnv(), resolveEmulators(config.emulators).values(), ); const cache = loadCache(); const { entries, report } = computeDesired({ config, scan, detected, art: cache.art, os: currentOs(), }); console.log( `Preview: ${entries.length} entr(y/ies) would be reconciled${report.overWarn ? " [scale warning]" : ""}.`, ); for (const e of entries.slice(0, 40)) console.log(` ${e.external_id} → ${e.launch?.value ?? "(no launch)"}`); if (entries.length > 40) console.log(` … and ${entries.length - 40} more`); if (report.skipped.length) { console.log(`\n${report.skipped.length} skipped:`); for (const s of report.skipped.slice(0, 20)) console.log(` ${s.title}: ${s.reason}`); } for (const w of report.warnings.slice(0, 10)) console.log(`! ${w}`); }; const cmdSync = async (): Promise => { const pf = await connect(); try { const engine = new Engine({ pf }); const report = await engine.sync("cli"); if (report) { console.log( `Synced: ${report.included} entr(y/ies), ${report.skipped.length} skipped.`, ); } else { console.log("Sync did not complete (see log)."); } } finally { pf.close(); } }; const cmdUninstall = async (): Promise => { const pf = await connect(); try { await new Engine({ pf }).uninstall(); console.log("Removed all rom-manager entries from the library."); } finally { pf.close(); } }; const cmdSetPassword = (password: string | undefined): void => { if (!password) { console.error("usage: set-password "); process.exitCode = 2; return; } const config = loadConfig(); config.ui.passwordHash = hashPassword(password); saveConfig(config); console.log("Standalone-UI password set (config.ui.passwordHash)."); }; const main = async (): Promise => { const [cmd, arg] = process.argv.slice(2); switch (cmd) { case "scan": return cmdScan(); case "detect": return cmdDetect(); case "preview": return cmdPreview(); case "sync": return cmdSync(); case "uninstall": return cmdUninstall(); case "set-password": return cmdSetPassword(arg); default: console.log( "usage: punktfunk-plugin-rom-manager ", ); process.exitCode = cmd ? 2 : 0; } }; await main();