delete the spent one-shot scripts
Eight scripts in scripts/ that nothing references and that mostly can no longer run. Kept in history;
none of them is recoverable knowledge that isn't already in the code they migrated to.
Three could not run at all against the current database:
migrate-items-to-files.ts SELECT * FROM tasks — that table was dropped when items became files
reset-user-data.ts deletes chat_sessions, chat_groups, projects; none exists. It has no
transaction, so it would wipe user_settings, user_state,
user_integrations and dock_configs and THEN throw. A half-wiped account
is worse than no script. It also misses chat_session_events, which is
where chat state actually lives now.
add-email-dock-user2.ts one-time, hardcoded to user 2, seeds a dock containing /projects
The rest are spent migrations whose destination is now the only implementation:
migrate-auth-to-pg.ts JSON -> Postgres, 2026-02
migrate-pg-to-files.ts Postgres -> JSON, the other leg of the same abandoned round trip
migrate-server-settings-to-pg.ts 2026-02
migrate-emails-to-sqlite.ts backfill into the email sidecar's store, 2026-07-31
seed-imap-uids.ts the sidecar writes imap_lastuid/imap_uidvalidity itself now
(sidecar/email/gmail-api.ts:533-535)
Kept, and why, since "unreferenced" was not the test: rebuild-soulseek-tree.ts is reusable by
construction — it runs the same buildTree the sidecar's ingest runs, so it answers any future change
in tree shape. reindex-music.ts is named in sidecar/music/index.ts:447. provision-user-dirs.ts shares
USER_DIRS with data-path.ts. cleanup-desktop.sh and officer-set-display.sh are called by
setup-desktop.sh.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -1,30 +0,0 @@
|
||||
/**
|
||||
* One-time script: add /email to user 2's dock
|
||||
*
|
||||
* Usage: bun run scripts/add-email-dock-user2.ts
|
||||
*/
|
||||
|
||||
import { getDockPaths, setDockPaths } from 'officerdb';
|
||||
|
||||
const USER_ID = 2;
|
||||
const DEFAULT_PATHS = ['/', '/files', '/automation', '/projects', '/dashboards', '/chat'];
|
||||
|
||||
async function main() {
|
||||
const existing = await getDockPaths(USER_ID);
|
||||
const paths = existing ?? DEFAULT_PATHS;
|
||||
|
||||
if (paths.includes('/email')) {
|
||||
console.log(`[dock] User ${USER_ID} already has /email in dock`);
|
||||
} else {
|
||||
paths.push('/email');
|
||||
await setDockPaths(USER_ID, paths);
|
||||
console.log(`[dock] Added /email to user ${USER_ID}'s dock: ${JSON.stringify(paths)}`);
|
||||
}
|
||||
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
main().catch((err) => {
|
||||
console.error('[dock] Failed:', err);
|
||||
process.exit(1);
|
||||
});
|
||||
@@ -1,137 +0,0 @@
|
||||
/**
|
||||
* Migration script: auth data from JSON files → PostgreSQL
|
||||
*
|
||||
* Migrates:
|
||||
* - users.json → users table
|
||||
* - passkeys.json → passkeys table (email → userId FK)
|
||||
* - token-blacklist.json → token_blacklist table
|
||||
*
|
||||
* Usage: bun run scripts/migrate-auth-to-pg.ts
|
||||
*/
|
||||
|
||||
import { join } from 'node:path';
|
||||
import { db } from 'officerdb/db';
|
||||
import { users, passkeys, tokenBlacklist } from 'officerdb/schema';
|
||||
|
||||
const DATA_PATH = process.env.DATA_PATH ?? join(process.cwd(), 'data');
|
||||
const AUTH_DIR = join(DATA_PATH, 'auth');
|
||||
|
||||
type OldUser = {
|
||||
id: number;
|
||||
email: string;
|
||||
password: string | null;
|
||||
role: string;
|
||||
status: string;
|
||||
name: string | null;
|
||||
username: string | null;
|
||||
avatar: string | null;
|
||||
passwordChangedAt: number | null;
|
||||
};
|
||||
|
||||
type OldPasskey = {
|
||||
id: number;
|
||||
email: string;
|
||||
origin: string | null;
|
||||
credentialId: string | null;
|
||||
publicKey: string | null;
|
||||
counter: number;
|
||||
};
|
||||
|
||||
type OldBlacklistEntry = {
|
||||
jti: string;
|
||||
expiresAt: number;
|
||||
};
|
||||
|
||||
async function readJson<T>(path: string, fallback: T): Promise<T> {
|
||||
try {
|
||||
const file = Bun.file(path);
|
||||
if (!(await file.exists())) return fallback;
|
||||
return (await file.json()) as T;
|
||||
} catch {
|
||||
return fallback;
|
||||
}
|
||||
}
|
||||
|
||||
async function migrate() {
|
||||
console.log(`[migrate] Reading JSON files from ${AUTH_DIR}`);
|
||||
|
||||
const oldUsers = await readJson<OldUser[]>(join(AUTH_DIR, 'users.json'), []);
|
||||
const oldPasskeys = await readJson<OldPasskey[]>(join(AUTH_DIR, 'passkeys.json'), []);
|
||||
const oldBlacklist = await readJson<OldBlacklistEntry[]>(join(AUTH_DIR, 'token-blacklist.json'), []);
|
||||
|
||||
console.log(`[migrate] Found: ${oldUsers.length} users, ${oldPasskeys.length} passkeys, ${oldBlacklist.length} blacklisted tokens`);
|
||||
|
||||
if (oldUsers.length === 0) {
|
||||
console.log('[migrate] No users to migrate. Done.');
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
// Build email → userId map for passkey migration
|
||||
const emailToUserId = new Map<string, number>();
|
||||
|
||||
// Migrate users
|
||||
console.log('[migrate] Migrating users...');
|
||||
for (const u of oldUsers) {
|
||||
const [inserted] = await db
|
||||
.insert(users)
|
||||
.values({
|
||||
email: u.email,
|
||||
password: u.password,
|
||||
status: u.status as 'Unverified' | 'Active' | 'Prospect' | 'Invited' | 'Blocked' | 'Banned' | 'Deleted',
|
||||
name: u.name,
|
||||
username: u.username,
|
||||
avatar: u.avatar,
|
||||
passwordChangedAt: u.passwordChangedAt ? new Date(u.passwordChangedAt) : null,
|
||||
})
|
||||
.returning();
|
||||
|
||||
emailToUserId.set(u.email, inserted!.id);
|
||||
console.log(` [user] ${u.email} (old id=${u.id} → new id=${inserted!.id})`);
|
||||
}
|
||||
|
||||
// Migrate passkeys
|
||||
if (oldPasskeys.length > 0) {
|
||||
console.log('[migrate] Migrating passkeys...');
|
||||
for (const p of oldPasskeys) {
|
||||
const userId = emailToUserId.get(p.email);
|
||||
if (!userId) {
|
||||
console.warn(` [passkey] Skipping passkey for unknown email: ${p.email}`);
|
||||
continue;
|
||||
}
|
||||
|
||||
await db.insert(passkeys).values({
|
||||
userId,
|
||||
origin: p.origin,
|
||||
credentialId: p.credentialId,
|
||||
publicKey: p.publicKey,
|
||||
counter: p.counter,
|
||||
});
|
||||
console.log(` [passkey] ${p.email} / ${p.origin}`);
|
||||
}
|
||||
}
|
||||
|
||||
// Migrate token blacklist
|
||||
if (oldBlacklist.length > 0) {
|
||||
const now = Math.floor(Date.now() / 1000);
|
||||
const active = oldBlacklist.filter((b) => b.expiresAt >= now);
|
||||
console.log(`[migrate] Migrating ${active.length} active blacklisted tokens (${oldBlacklist.length - active.length} expired, skipped)...`);
|
||||
|
||||
for (const b of active) {
|
||||
await db
|
||||
.insert(tokenBlacklist)
|
||||
.values({
|
||||
jti: b.jti,
|
||||
expiresAt: new Date(b.expiresAt * 1000),
|
||||
})
|
||||
.onConflictDoNothing();
|
||||
}
|
||||
}
|
||||
|
||||
console.log('[migrate] Done!');
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
migrate().catch((err) => {
|
||||
console.error('[migrate] Failed:', err);
|
||||
process.exit(1);
|
||||
});
|
||||
@@ -1,81 +0,0 @@
|
||||
import { readdirSync, readFileSync } from 'node:fs';
|
||||
import { join } from 'node:path';
|
||||
import { openEmailDb, upsertFromRawEml, setSyncMeta } from '../src/servers/sidecar/email/store';
|
||||
|
||||
const DATA_PATH = process.env.DATA_PATH ?? join(process.cwd(), 'data');
|
||||
|
||||
// Find all user directories that have Gmail emails
|
||||
const targetEmail = process.argv[2];
|
||||
|
||||
if (targetEmail) {
|
||||
migrate(targetEmail);
|
||||
} else {
|
||||
const entries = readdirSync(DATA_PATH, { withFileTypes: true });
|
||||
for (const entry of entries) {
|
||||
if (!entry.isDirectory()) continue;
|
||||
if (!entry.name.includes('@')) continue;
|
||||
const emailDir = join(DATA_PATH, entry.name, 'Gmail', 'emails');
|
||||
try {
|
||||
const files = readdirSync(emailDir).filter((f) => f.endsWith('.eml'));
|
||||
if (files.length > 0) migrate(entry.name);
|
||||
} catch {
|
||||
// no Gmail dir for this user
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function migrate(userEmail: string): void {
|
||||
console.log(`Migrating ${userEmail}...`);
|
||||
const emailDir = join(DATA_PATH, userEmail, 'Gmail', 'emails');
|
||||
// Obsolete one-off migration (old .eml-file store → SQLite); kept only to compile.
|
||||
const db = openEmailDb(userEmail, userEmail);
|
||||
|
||||
let filenames: string[];
|
||||
try {
|
||||
filenames = readdirSync(emailDir).filter((f) => f.endsWith('.eml'));
|
||||
} catch {
|
||||
console.log(' No .eml files found');
|
||||
db.close();
|
||||
return;
|
||||
}
|
||||
|
||||
const existingIds = new Set<string>();
|
||||
const rows = db.query('SELECT id FROM emails').all() as Array<{ id: string }>;
|
||||
for (const row of rows) existingIds.add(row.id);
|
||||
|
||||
let added = 0;
|
||||
let skipped = 0;
|
||||
let errors = 0;
|
||||
|
||||
db.exec('BEGIN');
|
||||
try {
|
||||
for (const filename of filenames) {
|
||||
const id = filename.replace(/\.eml$/, '');
|
||||
if (existingIds.has(id)) {
|
||||
skipped++;
|
||||
continue;
|
||||
}
|
||||
try {
|
||||
const raw = readFileSync(join(emailDir, filename), 'utf-8');
|
||||
upsertFromRawEml({ db, id, raw, integration: 'gmail', emailAccount: userEmail, labels: ['INBOX'] });
|
||||
added++;
|
||||
} catch {
|
||||
errors++;
|
||||
}
|
||||
}
|
||||
db.exec('COMMIT');
|
||||
} catch (err) {
|
||||
db.exec('ROLLBACK');
|
||||
throw err;
|
||||
}
|
||||
|
||||
console.log(` ${filenames.length} .eml files — ${added} added, ${skipped} skipped, ${errors} errors`);
|
||||
|
||||
// Store the latest email date so the next sync only fetches emails after it
|
||||
const row = db.query('SELECT date FROM emails ORDER BY date DESC LIMIT 1').get() as { date: string } | null;
|
||||
if (row?.date) {
|
||||
setSyncMeta(db, 'last_sync_date', row.date);
|
||||
console.log(` Stored last_sync_date: ${row.date}`);
|
||||
}
|
||||
db.close();
|
||||
}
|
||||
@@ -1,112 +0,0 @@
|
||||
/**
|
||||
* One-time migration: consolidate every agent item into the flat, file-based store
|
||||
* ($OFFICER_ITEMS_DIR) and export the DB-backed `tasks` table to TASK.md files.
|
||||
*
|
||||
* Idempotent — safe to re-run. Run this BEFORE applying the drop-tables DB migration
|
||||
* (it reads the `tasks` table, which still exists until that migration runs).
|
||||
*
|
||||
* Sources, in precedence order (later overwrites earlier on a dirName collision):
|
||||
* - tasks: officer_db.tasks rows (native → global → user)
|
||||
* - skills / tools / processes / extensions: $DATA_PATH/<type> then $DATA_PATH/<email>/<type>
|
||||
* - tools: marketplace registry tools not already present (archive safety)
|
||||
*
|
||||
* Usage: bun run scripts/migrate-items-to-files.ts
|
||||
*/
|
||||
|
||||
import { join, resolve } from 'node:path';
|
||||
import { readdir, cp } from 'node:fs/promises';
|
||||
import { existsSync } from 'node:fs';
|
||||
import { db } from 'officerdb/db';
|
||||
import { sql } from 'drizzle-orm';
|
||||
import { itemsDir, ensureItemDirs, DATA_PATH, OFFICER_ITEMS_DIR, type ItemType } from '../src/servers/data-path';
|
||||
import { importTask } from '../src/servers/api/tasks/task-files';
|
||||
|
||||
ensureItemDirs();
|
||||
console.log(`Target store: ${OFFICER_ITEMS_DIR}`);
|
||||
|
||||
async function listSubdirs(dir: string): Promise<string[]> {
|
||||
try {
|
||||
return (await readdir(dir, { withFileTypes: true })).filter((e) => e.isDirectory()).map((e) => e.name);
|
||||
} catch {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
// ── 1. Tasks: Postgres → TASK.md files ──
|
||||
// Order native → global → user so user/global overwrite native on a dirName collision.
|
||||
const scopeRank = (s: string) => (s === 'user' ? 2 : s === 'global' ? 1 : 0);
|
||||
|
||||
console.log('\n── Tasks (DB → files) ──');
|
||||
let taskRows: Record<string, unknown>[] = [];
|
||||
try {
|
||||
taskRows = (await db.execute(sql.raw('SELECT * FROM tasks'))) as unknown as Record<string, unknown>[];
|
||||
} catch (err) {
|
||||
console.log(` could not read tasks table (already dropped?): ${err instanceof Error ? err.message : err}`);
|
||||
}
|
||||
taskRows.sort((a, b) => scopeRank(String(a.scope)) - scopeRank(String(b.scope)));
|
||||
|
||||
for (const row of taskRows) {
|
||||
const dirName = String(row.dir_name);
|
||||
await importTask(dirName, {
|
||||
name: String(row.name ?? dirName),
|
||||
description: row.description == null ? null : String(row.description),
|
||||
version: Number(row.version) || 1,
|
||||
mode: String(row.mode ?? 'agentic'),
|
||||
language: row.language == null ? null : String(row.language),
|
||||
args: (row.args as string[] | null) ?? null,
|
||||
tags: (row.tags as string[] | null) ?? null,
|
||||
tools: (row.tools as string[] | null) ?? null,
|
||||
skills: (row.skills as string[] | null) ?? null,
|
||||
inputs: row.inputs ?? null,
|
||||
outputs: row.outputs ?? null,
|
||||
dependencies: row.dependencies ?? null,
|
||||
config: row.config ?? null,
|
||||
trigger: row.trigger ?? null,
|
||||
body: row.body == null ? '' : String(row.body),
|
||||
implementation: row.implementation == null ? null : String(row.implementation),
|
||||
});
|
||||
console.log(` ${dirName} (${row.scope})`);
|
||||
}
|
||||
console.log(` ${taskRows.length} task file(s) written`);
|
||||
|
||||
// ── 2. On-disk items → flat store ──
|
||||
const DISK_TYPES: ItemType[] = ['skills', 'tools', 'processes', 'extensions'];
|
||||
|
||||
async function copyItemsFrom(srcTypeDir: string, type: ItemType): Promise<number> {
|
||||
let n = 0;
|
||||
for (const name of await listSubdirs(srcTypeDir)) {
|
||||
await cp(join(srcTypeDir, name), join(itemsDir(type), name), { recursive: true, force: true });
|
||||
n++;
|
||||
}
|
||||
return n;
|
||||
}
|
||||
|
||||
console.log('\n── Disk items (DATA_PATH → flat store) ──');
|
||||
const emailDirs = (await listSubdirs(DATA_PATH)).filter((n) => n.includes('@'));
|
||||
|
||||
for (const type of DISK_TYPES) {
|
||||
let n = await copyItemsFrom(join(DATA_PATH, type), type); // global
|
||||
for (const email of emailDirs) n += await copyItemsFrom(join(DATA_PATH, email, type), type); // user (overwrites)
|
||||
console.log(` ${type}: ${n} item(s) copied`);
|
||||
}
|
||||
|
||||
// ── 3. Marketplace registry tools not already present (archive safety) ──
|
||||
const MARKETPLACE_REGISTRY = process.env.MARKETPLACE_REGISTRY ?? resolve(import.meta.dir, '../../marketplace/registry');
|
||||
console.log(`\n── Marketplace registry (${MARKETPLACE_REGISTRY}) ──`);
|
||||
if (existsSync(MARKETPLACE_REGISTRY)) {
|
||||
let n = 0;
|
||||
for (const name of await listSubdirs(join(MARKETPLACE_REGISTRY, 'tools'))) {
|
||||
const target = join(itemsDir('tools'), name);
|
||||
if (existsSync(target)) continue; // don't clobber a synced/user version
|
||||
await cp(join(MARKETPLACE_REGISTRY, 'tools', name), target, { recursive: true });
|
||||
n++;
|
||||
console.log(` tool ${name} (from registry)`);
|
||||
}
|
||||
console.log(` ${n} registry tool(s) added`);
|
||||
console.log(' registry tasks come from the DB export above (native scope) — skipped here');
|
||||
} else {
|
||||
console.log(' registry not found, skipping');
|
||||
}
|
||||
|
||||
console.log('\nDone. Verify counts in the UI, then apply the drop-tables DB migration.');
|
||||
process.exit(0);
|
||||
@@ -1,79 +0,0 @@
|
||||
/**
|
||||
* One-time migration: PostgreSQL auth tables → JSON files
|
||||
*
|
||||
* Usage:
|
||||
* POSTGRES_URL="postgres://..." bun run scripts/migrate-pg-to-files.ts
|
||||
*
|
||||
* Reads users and passkeys from Postgres, writes JSON files to {DATA_PATH}/auth/.
|
||||
* Safe to run multiple times (overwrites files).
|
||||
*/
|
||||
|
||||
import { join } from 'node:path';
|
||||
import { mkdir } from 'node:fs/promises';
|
||||
import postgres from 'postgres';
|
||||
|
||||
const POSTGRES_URL = process.env.POSTGRES_URL;
|
||||
if (!POSTGRES_URL) {
|
||||
console.error('POSTGRES_URL env var is required');
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const DATA_PATH = process.env.DATA_PATH ?? join(process.cwd(), 'data');
|
||||
const AUTH_DIR = join(DATA_PATH, 'auth');
|
||||
|
||||
const sql = postgres(POSTGRES_URL);
|
||||
|
||||
try {
|
||||
await mkdir(AUTH_DIR, { recursive: true });
|
||||
|
||||
const users = await sql`SELECT id, email, password, role, status, name, username, avatar, password_changed_at FROM users ORDER BY id`;
|
||||
const passkeys = await sql`SELECT id, email, origin, credential_id, public_key, counter FROM passkeys ORDER BY id`;
|
||||
|
||||
const mappedUsers = users.map((u) => ({
|
||||
id: Number(u.id),
|
||||
email: u.email,
|
||||
password: u.password ?? null,
|
||||
role: u.role ?? 'Member',
|
||||
status: u.status ?? 'Unverified',
|
||||
name: u.name ?? null,
|
||||
username: u.username ?? null,
|
||||
avatar: u.avatar ?? null,
|
||||
passwordChangedAt: u.password_changed_at ? Number(u.password_changed_at) : null,
|
||||
}));
|
||||
|
||||
const mappedPasskeys = passkeys.map((p) => ({
|
||||
id: Number(p.id),
|
||||
email: p.email,
|
||||
origin: p.origin ?? null,
|
||||
credentialId: p.credential_id ?? null,
|
||||
publicKey: p.public_key ?? null,
|
||||
counter: Number(p.counter ?? 0),
|
||||
}));
|
||||
|
||||
const maxUserId = mappedUsers.reduce((max, u) => Math.max(max, u.id), 0);
|
||||
const maxPasskeyId = mappedPasskeys.reduce((max, p) => Math.max(max, p.id), 0);
|
||||
|
||||
const meta = {
|
||||
nextUserId: maxUserId + 1,
|
||||
nextPasskeyId: maxPasskeyId + 1,
|
||||
};
|
||||
|
||||
const write = (file: string, data: unknown) => Bun.write(join(AUTH_DIR, file), JSON.stringify(data, null, 2));
|
||||
|
||||
await Promise.all([
|
||||
write('users.json', mappedUsers),
|
||||
write('passkeys.json', mappedPasskeys),
|
||||
write('passkey-challenges.json', []),
|
||||
write('token-blacklist.json', []),
|
||||
write('meta.json', meta),
|
||||
]);
|
||||
|
||||
console.log(`Migrated ${mappedUsers.length} users, ${mappedPasskeys.length} passkeys`);
|
||||
console.log(`Files written to ${AUTH_DIR}`);
|
||||
console.log(`meta: nextUserId=${meta.nextUserId}, nextPasskeyId=${meta.nextPasskeyId}`);
|
||||
} catch (err) {
|
||||
console.error('Migration failed:', err);
|
||||
process.exit(1);
|
||||
} finally {
|
||||
await sql.end();
|
||||
}
|
||||
@@ -1,42 +0,0 @@
|
||||
/**
|
||||
* Migration script: server-settings.json → PostgreSQL server_config table
|
||||
*
|
||||
* Usage: bun run scripts/migrate-server-settings-to-pg.ts
|
||||
*/
|
||||
|
||||
import { join } from 'node:path';
|
||||
import { writeServerSettings } from 'officerdb';
|
||||
|
||||
const DATA_PATH = process.env.DATA_PATH ?? join(process.cwd(), 'data');
|
||||
const settingsPath = join(DATA_PATH, 'server-settings', 'server-settings.json');
|
||||
|
||||
async function migrate() {
|
||||
console.log(`[migrate] Reading ${settingsPath}`);
|
||||
|
||||
const file = Bun.file(settingsPath);
|
||||
if (!(await file.exists())) {
|
||||
console.log('[migrate] No server-settings.json found. Done.');
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
let settings: Record<string, unknown>;
|
||||
try {
|
||||
settings = await file.json();
|
||||
} catch {
|
||||
console.log('[migrate] Could not parse server-settings.json. Done.');
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
const keys = Object.keys(settings);
|
||||
console.log(`[migrate] Found ${keys.length} keys: ${keys.join(', ')}`);
|
||||
|
||||
await writeServerSettings(settings);
|
||||
console.log('[migrate] Written to server_config table.');
|
||||
console.log('[migrate] Done!');
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
migrate().catch((err) => {
|
||||
console.error('[migrate] Failed:', err);
|
||||
process.exit(1);
|
||||
});
|
||||
@@ -1,161 +0,0 @@
|
||||
/**
|
||||
* 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/<email>/ 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 <email>
|
||||
* bun run scripts/reset-user-data.ts <email> --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 <email> [--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<string>((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);
|
||||
@@ -1,88 +0,0 @@
|
||||
import { ImapFlow } from 'imapflow';
|
||||
import { getUserByEmail, getUserIntegration, getServerIntegration } from 'officerdb';
|
||||
import { openEmailDb, setSyncMeta } from '../src/servers/sidecar/email/store';
|
||||
|
||||
const userEmail = process.argv[2];
|
||||
if (!userEmail) {
|
||||
console.error('Usage: bun run scripts/seed-imap-uids.ts <email>');
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
// ── Load credentials ──
|
||||
|
||||
const dbUser = await getUserByEmail(userEmail);
|
||||
if (!dbUser) throw new Error('User not found');
|
||||
|
||||
const userGoogle = await getUserIntegration(dbUser.id, 'google');
|
||||
const config = userGoogle?.config as Record<string, unknown> | undefined;
|
||||
if (!config?.accessToken) throw new Error('No OAuth tokens found');
|
||||
|
||||
// Refresh token if needed
|
||||
let accessToken = config.accessToken as string;
|
||||
const expiresAt = config.expiresAt as number | undefined;
|
||||
if (!expiresAt || expiresAt < Date.now() + 60_000) {
|
||||
console.log('Refreshing expired token...');
|
||||
const serverGoogle = await getServerIntegration('google');
|
||||
const serverConfig = serverGoogle?.config as Record<string, unknown>;
|
||||
const res = await fetch('https://oauth2.googleapis.com/token', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
|
||||
body: new URLSearchParams({
|
||||
client_id: serverConfig.clientId as string,
|
||||
client_secret: serverConfig.clientSecret as string,
|
||||
refresh_token: config.refreshToken as string,
|
||||
grant_type: 'refresh_token',
|
||||
}),
|
||||
});
|
||||
if (!res.ok) throw new Error(`Token refresh failed: ${await res.text()}`);
|
||||
const data = (await res.json()) as { access_token: string };
|
||||
accessToken = data.access_token;
|
||||
}
|
||||
|
||||
// ── Connect IMAP ──
|
||||
|
||||
const client = new ImapFlow({
|
||||
host: 'imap.gmail.com',
|
||||
port: 993,
|
||||
secure: true,
|
||||
auth: { user: config.email as string, accessToken },
|
||||
logger: false,
|
||||
});
|
||||
|
||||
await client.connect();
|
||||
console.log('Connected to IMAP');
|
||||
|
||||
const GMAIL_PREFIX_RE = /^\[(?:Gmail|Google Mail)\]\//;
|
||||
const SKIP_SUFFIXES = new Set(['All Mail', 'Trash', 'Spam', 'Bin']);
|
||||
|
||||
const folders = await client.list();
|
||||
const db = openEmailDb(userEmail, config.email as string);
|
||||
|
||||
let seeded = 0;
|
||||
|
||||
for (const folder of folders) {
|
||||
const suffix = folder.path.replace(GMAIL_PREFIX_RE, '');
|
||||
const isGmailFolder = suffix !== folder.path;
|
||||
if (isGmailFolder && SKIP_SUFFIXES.has(suffix)) continue;
|
||||
if (folder.specialUse && ['\\Trash', '\\Junk', '\\All'].includes(folder.specialUse)) continue;
|
||||
|
||||
try {
|
||||
const status = await client.status(folder.path, { uidNext: true, uidValidity: true });
|
||||
const lastUid = (status.uidNext ?? 1) - 1;
|
||||
const uidValidity = String(status.uidValidity);
|
||||
|
||||
setSyncMeta(db, `imap_lastuid:${folder.path}`, String(lastUid));
|
||||
setSyncMeta(db, `imap_uidvalidity:${folder.path}`, uidValidity);
|
||||
|
||||
console.log(` ${folder.path}: lastUid=${lastUid}, uidValidity=${uidValidity}`);
|
||||
seeded++;
|
||||
} catch (err) {
|
||||
console.log(` ${folder.path}: skipped (${err instanceof Error ? err.message : err})`);
|
||||
}
|
||||
}
|
||||
|
||||
db.close();
|
||||
await client.logout();
|
||||
|
||||
console.log(`\nSeeded ${seeded} folders. Next sync will only fetch new messages.`);
|
||||
process.exit(0);
|
||||
Reference in New Issue
Block a user