import { IDEA_STATUSES, type IdeaStatus } from '@spendlight/shared'; import { eq } from 'drizzle-orm'; import { existsSync } from 'node:fs'; import { join } from 'node:path'; import { hashPassword, temporaryPassword } from './auth/password'; import { revokeUserSessions } from './auth/sessions'; import { createBackup, pruneBackups } from './backups/backup'; import { loadConfig } from './config'; import type { AppContext } from './context'; import { openDatabase } from './db/client'; import { users } from './db/schema'; import { ideasToMarkdown } from './ideas/markdown'; import { listIdeas } from './ideas/queries'; import { silentLogger } from './log'; import { findActiveUser } from './users'; // Admin commands, run inside the container: // spendlight ideas [--status=new|planned|done|declined] // spendlight reset-password // spendlight backup // spendlight version const HELP = `Spendlight admin commands spendlight ideas [--status=new] Print ideas as Markdown (all statuses by default) spendlight reset-password Give someone a temporary password spendlight backup Back up the database now spendlight version Show the running version `; const config = loadConfig(); const [command, ...args] = process.argv.slice(2); function openExisting() { const file = join(config.dataDir, 'spendlight.db'); if (!existsSync(file)) { console.error(`No Spendlight database found at ${file}.`); process.exit(1); } return openDatabase(file); } switch (command) { case 'ideas': { const statusArg = args.find((a) => a.startsWith('--status='))?.split('=')[1]; if (statusArg && !IDEA_STATUSES.includes(statusArg as IdeaStatus)) { console.error(`Unknown status "${statusArg}". Use one of: ${IDEA_STATUSES.join(', ')}.`); process.exit(1); } const { db } = openExisting(); process.stdout.write(ideasToMarkdown(listIdeas(db, statusArg as IdeaStatus | undefined))); break; } case 'reset-password': { const username = args[0]; if (!username) { console.error('Usage: spendlight reset-password '); process.exit(1); } const { db, sqlite } = openExisting(); const ctx = { db, sqlite, config, log: silentLogger } as unknown as AppContext; const user = findActiveUser(ctx, { username }); if (!user) { console.error(`Nobody uses the username "${username}".`); process.exit(1); } const password = temporaryPassword(); db.update(users) .set({ passwordHash: await hashPassword(password), mustChangePassword: true, updatedAt: new Date().toISOString() }) .where(eq(users.id, user.id)) .run(); revokeUserSessions(ctx, user.id); console.log(`Temporary password for ${user.displayName} (${user.username}): ${password}`); console.log('They will choose a new one when they sign in.'); break; } case 'backup': { const { sqlite } = openExisting(); const backup = await createBackup(sqlite, config.backupDir, 'manual'); pruneBackups(config.backupDir, { keepDaily: config.keepDaily, keepMonthly: config.keepMonthly }); console.log(`Backed up to ${join(config.backupDir, backup.name)}`); break; } case 'version': console.log(config.version); break; default: process.stdout.write(HELP); process.exit(command ? 1 : 0); }