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:
2026-08-12 03:58:13 +00:00
co-authored by Claude Opus 5
parent a6acfea9a6
commit 37adc65a12
8 changed files with 0 additions and 730 deletions
-30
View File
@@ -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);
});
-137
View File
@@ -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);
});
-81
View File
@@ -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();
}
-112
View File
@@ -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);
-79
View File
@@ -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();
}
-42
View File
@@ -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);
});
-161
View File
@@ -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);
-88
View File
@@ -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);