import { createInterface } from 'node:readline/promises'; import { Writable } from 'node:stream'; import { dataDirectory } from '../src/server/db'; import { initialize } from '../src/server/initialize'; import { provisionOwner } from '../src/server/owner'; import { importLegacy } from '../src/server/legacy-import'; import { backup, restore } from '../src/server/backup'; import { withContentLock } from '../src/server/write-lock'; async function credentials() { if (process.env.OWNER_EMAIL && process.env.OWNER_PASSWORD) return { email: process.env.OWNER_EMAIL, password: process.env.OWNER_PASSWORD }; if (!process.stdin.isTTY) throw new Error('Use an interactive terminal, or supply OWNER_EMAIL and OWNER_PASSWORD through the environment'); let muted = false; const output = new Writable({ write(chunk, _encoding, callback) { if (!muted) process.stdout.write(chunk); callback(); } }); const prompt = createInterface({ input: process.stdin, output, terminal: true }); try { const email = (await prompt.question('Owner email: ')).trim(); process.stdout.write('Password (12–128 characters, hidden): '); muted = true; const password = await prompt.question(''); muted = false; process.stdout.write('\n'); process.stdout.write('Confirm password (hidden): '); muted = true; const confirmation = await prompt.question(''); muted = false; process.stdout.write('\n'); if (password !== confirmation) throw new Error('Passwords do not match'); return { email, password }; } finally { prompt.close(); } } async function main() { const [command, argument, target] = process.argv.slice(2); if (command === 'restore') { if (!argument || !target) throw new Error('Usage: restore BACKUP_DIRECTORY NEW_DATA_DIRECTORY'); restore(argument, target); console.log('Restored into the new directory. Stop the app before switching its data volume.'); return; } if (!['init', 'create-owner', 'create-user', 'reset-password', 'import-legacy', 'migrate-legacy-media', 'backup'].includes(command)) throw new Error('Commands: init, create-owner (alias: create-user), reset-password, import-legacy BUNDLE_DIRECTORY, migrate-legacy-media BUNDLE_DIRECTORY, backup DIRECTORY, restore BACKUP_DIRECTORY NEW_DATA_DIRECTORY'); const connection = initialize(); try { if (command === 'create-owner' || command === 'create-user' || command === 'reset-password') { const { email, password } = await credentials(); await withContentLock(() => provisionOwner(connection, email, password, command === 'reset-password')); console.log(command === 'reset-password' ? 'Password updated. All sessions revoked.' : 'Owner created.'); } else if (command === 'import-legacy' || command === 'migrate-legacy-media') { await withContentLock(() => importLegacy(connection, argument, command === 'migrate-legacy-media')); console.log(command === 'import-legacy' ? 'Portfolio imported.' : 'Legacy media references migrated; content preserved.'); } else if (command === 'backup') { if (!argument) throw new Error('Supply a new backup directory'); await backup(connection, dataDirectory(), argument); console.log('Backup complete.'); } else console.log('Database initialized.'); } finally { connection.sqlite.close(); } } main().catch(error => { console.error(error instanceof Error ? error.message : 'Maintenance failed'); process.exitCode = 1; });