Foundation: single arm64 container (Hono + SQLite via Drizzle, React + Vite), local sign-in with long-lived sessions and passkeys, roles (read-only, full access, admin), nightly and pre-upgrade backups, light and dark themes, date-based version numbers, and the ideas lightbulb behind IDEAS_ENABLED. Money in: accounts with balances, CSV import with bank detection, a column mapping pop-up for unknown layouts, duplicate checks, transfer pairing and undo; one-tap review with learned rules; activity with search, splits, tags, notes and cash entries. Statement files are blocked from commits by a pre-commit hook; tests use synthetic fixtures only. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
91 lines
3.3 KiB
TypeScript
91 lines
3.3 KiB
TypeScript
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 <username>
|
|
// spendlight backup
|
|
// spendlight version
|
|
|
|
const HELP = `Spendlight admin commands
|
|
|
|
spendlight ideas [--status=new] Print ideas as Markdown (all statuses by default)
|
|
spendlight reset-password <user> 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 <username>');
|
|
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);
|
|
}
|