#!/usr/bin/env bun // Dev/debug CLI — reuses the pure core and the engine. `where`/`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-playnite where # where the exporter output was found // bunx punktfunk-plugin-playnite preview # the desired library (no host write) // bunx punktfunk-plugin-playnite sync # reconcile into the live library // bunx punktfunk-plugin-playnite uninstall # remove all this plugin's entries // bunx punktfunk-plugin-playnite set-password # standalone-UI password import { connect } from "@punktfunk/host"; import { Engine } from "./engine/index.js"; import { computeEntries } from "./engine/reconcile.js"; import { locateExport, readExport } from "./playnite.js"; import { hashPassword } from "./server/password.js"; import { loadConfig, saveConfig } from "./state.js"; const cmdWhere = (): void => { const config = loadConfig(); const loc = locateExport(config); console.log(`Playnite data dir: ${loc.dir ?? "(not found)"}`); console.log(`Export file: ${loc.file ?? "(not found)"}`); if (loc.mtime) console.log(`Last written: ${new Date(loc.mtime).toISOString()}`); console.log("Candidates probed:"); for (const c of loc.candidates) console.log(` ${c}`); if (!loc.file) { console.log( "\nNo export found. Install the Punktfunk Sync extension in Playnite (see exporter/README.md),", ); console.log("or set `playniteDir` in the plugin config.json."); } }; const cmdPreview = (): void => { const config = loadConfig(); const loc = locateExport(config); if (!loc.file) { console.log("No exporter output found — run `where` for details."); process.exitCode = 1; return; } const exp = readExport(loc.file); const { entries, report } = computeEntries({ exp, config, artFor: () => undefined, }); console.log( `Preview: ${report.included}/${report.considered} game(s) would sync (${report.skipped.length} skipped).`, ); const bySource = Object.entries(report.perSource).sort((a, b) => b[1] - a[1]); for (const [s, n] of bySource) console.log(` ${s}: ${n}`); console.log(); for (const e of entries.slice(0, 40)) console.log(` ${e.title} → ${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 (first 20):`); for (const s of report.skipped.slice(0, 20)) console.log(` ${s.title}: ${s.reason}`); } for (const w of report.warnings) console.log(`! ${w}`); }; const cmdSync = async (): Promise => { const pf = await connect(); try { const report = await new Engine({ pf }).sync("cli"); if (report) { console.log( `Synced: ${report.included} title(s), ${report.skipped.length} skipped.`, ); } else { console.log("Sync did not reconcile (no change, or see log)."); } } finally { pf.close(); } }; const cmdUninstall = async (): Promise => { const pf = await connect(); try { await new Engine({ pf }).uninstall(); console.log("Removed all playnite 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 "where": return cmdWhere(); case "preview": return cmdPreview(); case "sync": return cmdSync(); case "uninstall": return cmdUninstall(); case "set-password": return cmdSetPassword(arg); default: console.log( "usage: punktfunk-plugin-playnite ", ); process.exitCode = cmd ? 2 : 0; } }; await main();