managarten/scripts/personas/cleanup.ts
Till JS 546b94d472 feat(personas): move admin + internal endpoints from mana-auth to apps/api
Schließt die platform/product-split-Lücke: HEAD's apps/api/src/index.ts
referenziert seit dem Forms-M10d-Commit personasInternalRoutes /
personasAdminRoutes — die Implementierung lag aber noch nicht im Repo.
Build war strukturell broken bis hierhin.

Was wandert von mana-auth nach apps/api:

  apps/api/src/modules/personas/
    ├── schema.ts          — pgSchema('personas') mit personas /
    │                        persona_actions / persona_feedback;
    │                        userId ist plain text (Cross-DB-FK auf
    │                        mana-auth's auth.users geht nach Split nicht).
    ├── internal-routes.ts — service-key gated GET /due, POST /:id/actions
    │                        und POST /:id/feedback. Append-only +
    │                        idempotent über deterministische row-ids
    │                        (tickId-i-tool / tickId-module).
    └── admin-routes.ts    — admin-JWT gated CRUD; ruft mana-auth via
                             /api/v1/admin/users + /api/v1/auth/register
                             + /api/v1/internal/users/:id/persona-stamp
                             für den User-Lifecycle.

Persona-runner-Client zeigt jetzt auf apps/api:

  - config.ts: neues apiUrl-Feld (default http://localhost:3060,
    Env MANA_API_URL); authUrl bleibt für /api/v1/auth/login + spaces.
  - clients/mana-auth-internal.ts: drei Calls treffen jetzt
    /api/v1/personas/internal/* statt mana-auth's
    /api/v1/internal/personas/* — Datei-Name bleibt um Call-Site-Diff
    klein zu halten.
  - index.ts: ManaAuthInternalClient bekommt config.apiUrl statt authUrl.

Seed/Cleanup-Skripte:

  - --api= als bevorzugter Flag, --auth= als Legacy-Alias (cached
    Shell-History würde sonst hart brechen).
  - default http://localhost:3060, Env MANA_API_URL.
  - Endpoint-Pfade umgeschrieben:
      POST   /api/v1/admin/personas        → /api/v1/personas/admin
      DELETE /api/v1/admin/personas/:id    → /api/v1/personas/admin/:id

drizzle.config.ts: schema-Array + schemaFilter um 'personas' erweitert.
DB-push ist Pflicht-Schritt vor erstem Boot, sonst 42P01 auf /due.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-06 20:38:29 +02:00

111 lines
3.4 KiB
TypeScript

#!/usr/bin/env bun
/**
* Hard-delete every persona known to the catalog.
*
* Wipes the user row (cascades through personas, persona_actions,
* persona_feedback, sessions, vault, organizations the user owns alone,
* and downstream sync data). Useful after iterating on the catalog or
* before a fresh seed run.
*
* Refuses to touch any user where kind != 'persona' (the admin endpoint
* enforces this server-side too — defense in depth).
*
* Usage:
* pnpm seed:personas:cleanup
* pnpm seed:personas:cleanup --api=https://api.mana.how --jwt=eyJ…
*/
import { loadCatalog } from './catalog';
interface CliOptions {
apiUrl: string;
adminJwt: string;
}
function parseArgs(): CliOptions {
const args = process.argv.slice(2);
const get = (key: string): string | undefined => {
const found = args.find((a) => a.startsWith(`--${key}=`));
return found?.slice(`--${key}=`.length);
};
const apiUrl = get('api') ?? get('auth') ?? process.env.MANA_API_URL ?? 'http://localhost:3060';
const adminJwt = get('jwt') ?? process.env.MANA_ADMIN_JWT ?? '';
if (!adminJwt) {
console.error('❌ Missing admin JWT. Set MANA_ADMIN_JWT or pass --jwt=…');
process.exit(1);
}
return { apiUrl, adminJwt };
}
interface PersonaListEntry {
userId: string;
email: string;
archetype: string;
}
async function listPersonas(opts: CliOptions): Promise<PersonaListEntry[]> {
const res = await fetch(`${opts.apiUrl}/api/v1/personas/admin`, {
headers: { authorization: `Bearer ${opts.adminJwt}` },
});
if (!res.ok) {
throw new Error(`GET /admin/personas → ${res.status}: ${await res.text()}`);
}
const body = (await res.json()) as { personas: PersonaListEntry[] };
return body.personas;
}
async function deletePersona(opts: CliOptions, userId: string, email: string): Promise<void> {
const res = await fetch(`${opts.apiUrl}/api/v1/personas/admin/${userId}`, {
method: 'DELETE',
headers: { authorization: `Bearer ${opts.adminJwt}` },
});
if (!res.ok) {
const body = await res.text().catch(() => '<unreadable>');
throw new Error(`DELETE /admin/personas/${userId}${res.status}: ${body.slice(0, 300)}`);
}
console.log(` ✓ deleted ${email} (${userId.slice(0, 8)}…)`);
}
async function main(): Promise<void> {
const opts = parseArgs();
// Cross-reference catalog ↔ live to catch drift (a persona that lives
// in the DB but not the catalog still gets deleted — cleanup means
// "back to zero personas", not "back to catalog").
const catalogEmails = new Set(loadCatalog().personas.map((p) => p.email));
const live = await listPersonas(opts);
if (live.length === 0) {
console.log('▸ Nothing to delete — no personas in mana-auth.');
return;
}
console.log(`▸ Found ${live.length} persona(s) to delete:`);
for (const p of live) {
const inCatalog = catalogEmails.has(p.email);
console.log(` · ${p.email} (${p.archetype})${inCatalog ? '' : ' [⚠ not in catalog]'}`);
}
console.log('');
const failures: string[] = [];
for (const p of live) {
try {
await deletePersona(opts, p.userId, p.email);
} catch (err) {
const msg = err instanceof Error ? err.message : String(err);
console.error(`${p.email}${msg}`);
failures.push(p.email);
}
}
console.log('');
if (failures.length > 0) {
console.error(`✗ Done with ${failures.length} failure(s).`);
process.exit(1);
}
console.log(`✓ Done. ${live.length} personas deleted.`);
}
void main();