/** * Reset all user data while keeping auth credentials. * * Deletes: * - DB: user_settings, user_state, user_integrations, dock_configs, * chat_sessions (cascades chat_messages), chat_groups, * dashboards, screens, projects, * task_logs, queue_jobs, terminal_containers * - Filesystem: entire $DATA_PATH// directory * (home, settings, state, dashboards, chat_sessions, emails.db, * Gmail, skills, tools, tasks, processes, extensions, logs, cache, etc.) * - Queue job files: $DATA_PATH/queue/jobs/*.json owned by user * - Terminal containers map: removes user entry from terminal-containers.json * * Preserves: * - users table row (account, password, role, status) * - passkeys table rows * - passkey_challenges, token_blacklist * * Usage: bun run scripts/reset-user-data.ts * bun run scripts/reset-user-data.ts --yes (skip confirmation) */ import { join } from 'node:path'; import { rm, readdir, unlink } from 'node:fs/promises'; import { db } from 'officerdb/db'; import { users } from 'officerdb/schema'; import { eq, sql } from 'drizzle-orm'; const DATA_PATH = process.env.DATA_PATH ?? join(process.cwd(), 'data'); const email = process.argv[2]; const skipConfirm = process.argv.includes('--yes'); if (!email) { console.error('Usage: bun run scripts/reset-user-data.ts [--yes]'); process.exit(1); } // ── Resolve user ── const [user] = await db.select({ id: users.id, email: users.email }).from(users).where(eq(users.email, email)); if (!user) { console.error(`User not found: ${email}`); process.exit(1); } console.log(`\nUser: ${user.email} (id: ${user.id})`); console.log(`Data dir: ${join(DATA_PATH, email)}`); console.log('\nThis will delete ALL user data (settings, chats, dashboards, emails, home dir, etc.)'); console.log('Auth credentials (account, passkeys) will be preserved.\n'); if (!skipConfirm) { process.stdout.write('Continue? [y/N] '); const response = await new Promise((resolve) => { process.stdin.once('data', (data) => resolve(data.toString().trim())); }); if (response.toLowerCase() !== 'y') { console.log('Aborted.'); process.exit(0); } } const userId = user.id; // ── Database cleanup ── // All these tables have ON DELETE CASCADE from users, but we don't want to delete the user. // Delete explicitly by user_id. console.log('\n── Database ──'); const tables = [ 'user_settings', 'user_state', 'user_integrations', 'dock_configs', 'chat_sessions', // cascades chat_messages 'chat_groups', 'dashboards', 'screens', 'projects', 'task_logs', 'queue_jobs', 'terminal_containers', ]; for (const table of tables) { const result = await db.execute(sql.raw(`DELETE FROM ${table} WHERE user_id = ${userId}`)); const count = result.length ?? 0; console.log(` ${table}: ${count} rows deleted`); } // Agent items (skills, tools, tasks, processes, extensions) are now flat files in // $OFFICER_ITEMS_DIR, shared and not user-owned — intentionally left untouched by a user reset. // ── Queue job files ── console.log('\n── Queue job files ──'); const queueDir = join(DATA_PATH, 'queue', 'jobs'); try { const entries = await readdir(queueDir); let deleted = 0; for (const entry of entries) { if (!entry.endsWith('.json')) continue; try { const file = Bun.file(join(queueDir, entry)); const job = await file.json(); if (job.userId === email) { await unlink(join(queueDir, entry)); deleted++; } } catch { // skip unreadable files } } console.log(` ${deleted} job files deleted`); } catch { console.log(' queue dir not found, skipping'); } // ── Terminal containers map ── console.log('\n── Terminal containers ──'); const containerMapPath = join(DATA_PATH, 'terminal-containers.json'); try { const file = Bun.file(containerMapPath); if (await file.exists()) { const map = await file.json(); let changed = false; for (const key of Object.keys(map)) { if (key === email || map[key]?.email === email) { delete map[key]; changed = true; } } if (changed) { await Bun.write(containerMapPath, JSON.stringify(map, null, 2)); console.log(' removed from terminal-containers.json'); } else { console.log(' no entry found'); } } } catch { console.log(' terminal-containers.json not found, skipping'); } // ── Filesystem ── console.log('\n── Filesystem ──'); const userDir = join(DATA_PATH, email); try { await rm(userDir, { recursive: true, force: true }); console.log(` removed ${userDir}`); } catch (err: unknown) { const msg = err instanceof Error ? err.message : String(err); console.log(` failed to remove ${userDir}: ${msg}`); } console.log('\nDone. User auth preserved, all data wiped.'); process.exit(0);