Replace the marketplace service dependency and the native/global/user scope tiers with a single external directory ($OFFICER_ITEMS_DIR) holding skills, tools, tasks, processes and extensions as plain files. - tasks move from Postgres to TASK.md files (new file-backed task layer); task editing now works, which the DB path never supported - skills/tools/processes collapse into one shared file router (single dir) - remove the marketplace client (sync-marketplace/sync-version) and the boot-time sync; pi-bridge/pi-manager/sandbox point at the flat store - drop the dead tasks + vestigial skills/tools/processes/extensions + item_chats tables (migration 0004) - one-time migration script exports DB tasks and consolidates disk items Migration verified: all 6 tasks round-trip through the runtime parser identically to their DB rows (pipeline steps, triggers, script impls and agentic bodies all intact). NOTE: not yet functionally tested end-to-end — every item (each task mode, tool, skill, extension) still needs to be run/exercised in the app before this is trusted. To be done manually. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
162 lines
4.8 KiB
TypeScript
162 lines
4.8 KiB
TypeScript
/**
|
|
* 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);
|