Something went wrong. Try again.
Something went wrong. Try again.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298#!/usr/bin/env bun/** * Nuke all data from D1, R2, and Vectorize. * * Destroys everything — nodes table, pastes table, all R2 objects, * and the entire Vectorize index (deletes + recreates). * * Usage: * bun scripts/cleanup.ts # nuke all three * bun scripts/cleanup.ts --d1-only # only D1 * bun scripts/cleanup.ts --r2-only # only R2 * bun scripts/cleanup.ts --vectorize-only # only Vectorize * bun scripts/cleanup.ts --dry-run # preview what would happen */
import { readFileSync } from "node:fs";import { homedir } from "node:os";import { join } from "node:path";
// ── Args ───────────────────────────────────────────────────────────────────
function arg(name: string): string | undefined { const i = process.argv.indexOf(name); return i !== -1 ? process.argv[i + 1] : undefined;}
const dryRun = process.argv.includes("--dry-run");const d1Only = process.argv.includes("--d1-only");const r2Only = process.argv.includes("--r2-only");const vectorizeOnly = process.argv.includes("--vectorize-only");const all = !d1Only && !r2Only && !vectorizeOnly;
const stage = arg("--stage") ?? "production";const stack = arg("--stack") ?? "Orbit";
const EMBED_DIMENSIONS = 1024; // @cf/baai/bge-m3
// ── Wrangler credentials ───────────────────────────────────────────────────
function readWranglerToken(): string { const candidates = [ join(homedir(), "Library", "Preferences", ".wrangler", "config", "default.toml"), join(homedir(), ".wrangler", "config", "default.toml"), ]; for (const p of candidates) { try { const raw = readFileSync(p, "utf-8"); const m = raw.match(/^oauth_token\s*=\s*"(.+?)"/m); if (m) return m[1]; } catch {} } throw new Error("Cannot find wrangler OAuth token. Log in with `wrangler login` first.");}
const API_TOKEN = readWranglerToken();
async function getAccountId(): Promise<string> { const res = await fetch("https://api.cloudflare.com/client/v4/accounts", { headers: { Authorization: `Bearer ${API_TOKEN}` }, }); const json = (await res.json()) as { success: boolean; result: Array<{ id: string; name: string }>; }; if (!json.success || json.result.length === 0) { throw new Error("Cannot fetch Cloudflare account ID."); } return json.result[0].id;}
const ACCOUNT_ID = await getAccountId();const API_BASE = `https://api.cloudflare.com/client/v4/accounts/${ACCOUNT_ID}`;
async function cfFetch<T>(path: string, init?: RequestInit & { method?: string }): Promise<T> { const res = await fetch(`${API_BASE}${path}`, { ...init, headers: { Authorization: `Bearer ${API_TOKEN}`, "Content-Type": "application/json", ...init?.headers, }, }); const json = (await res.json()) as { success: boolean; errors?: unknown[]; result: T }; if (!json.success) throw new Error(`CF API error: ${JSON.stringify(json.errors)}`); return json.result;}
// ── Alchemy Resource Resolution ────────────────────────────────────────────
interface AlchemyResource { status: string; fqn: string; logicalId: string; instanceId: string; resourceType: string; attr: Record<string, unknown>;}
async function alchemyStateGet(fqn: string): Promise<AlchemyResource> { const cmd = ["alchemy", "state", "get", "--stack", stack, "--stage", stage, "--fqn", fqn]; const proc = Bun.spawn(cmd, { stdout: "pipe", stderr: "pipe" }); const stdout = await new Response(proc.stdout).text(); const exitCode = await proc.exited; if (exitCode !== 0) { const stderr = await new Response(proc.stderr).text(); throw new Error(stderr || `alchemy state get exited with code ${exitCode}`); } return JSON.parse(stdout);}
// ── Resolve Alchemy Resources ──────────────────────────────────────────────
console.log("=== Resolving Alchemy Resources ===\n");const [dbResource, bucketResource, vectorizeResource] = await Promise.all([ alchemyStateGet("DB"), alchemyStateGet("Bucket"), alchemyStateGet("VectorizeIndex"),]);const DB_ID = String(dbResource.attr.databaseId);const R2_BUCKET = String(bucketResource.attr.bucketName);const VECTORIZE_INDEX = String(vectorizeResource.attr.indexName);console.log(` D1: ${DB_ID}`);console.log(` R2: ${R2_BUCKET}`);console.log(` Vectorize: ${VECTORIZE_INDEX}\n`);
// ── D1 ─────────────────────────────────────────────────────────────────────
async function cleanD1(): Promise<void> { console.log("\n── D1 ──");
const tables = ["nodes", "pastes"]; for (const table of tables) { console.log(` Truncating ${table}...`); if (!dryRun) { try { await cfFetch(`/d1/database/${DB_ID}/query`, { method: "POST", body: JSON.stringify({ sql: `DELETE FROM ${table};` }), }); console.log(` ✓ ${table} cleared`); } catch (err) { console.error(` ✗ Failed to clear ${table}:`, err); } } else { console.log(` [dry-run] DELETE FROM ${table}`); } }
if (!dryRun) { try { await cfFetch(`/d1/database/${DB_ID}/query`, { method: "POST", body: JSON.stringify({ sql: "VACUUM;" }), }); } catch { // VACUUM may fail in some D1 contexts; non-critical } }
console.log(" ✓ D1 cleaned");}
// ── R2 ─────────────────────────────────────────────────────────────────────
async function cleanR2(): Promise<void> { console.log("\n── R2 ──");
// List all objects via CF API (paginated) const keys: string[] = []; let cursor: string | undefined; do { const params = new URLSearchParams( cursor ? { cursor, per_page: "1000" } : { per_page: "1000" }, ); const res = await fetch(`${API_BASE}/r2/buckets/${R2_BUCKET}/objects?${params}`, { headers: { Authorization: `Bearer ${API_TOKEN}`, "Content-Type": "application/json", }, }); const json = (await res.json()) as { success: boolean; result: Array<{ key: string }>; result_info?: { cursor?: string; is_truncated?: boolean }; }; if (!json.success) { const body = await res.text(); throw new Error(`R2 list error: ${body}`); } for (const obj of json.result ?? []) { keys.push(obj.key); } cursor = json.result_info?.cursor; } while (cursor);
if (keys.length === 0) { console.log(" Nothing in R2, skipping."); return; }
console.log(` ${keys.length} objects to delete`);
if (!dryRun) { const concurrency = 10; const results = await Promise.allSettled( keys.map((key, i) => (async () => { // stagger starts slightly to avoid connection burst await new Promise((r) => setTimeout(r, (i % concurrency) * 50)); const res = await fetch( `${API_BASE}/r2/buckets/${R2_BUCKET}/objects/${encodeURIComponent(key).replace(/%2F/g, "/")}`, { method: "DELETE", headers: { Authorization: `Bearer ${API_TOKEN}` }, }, ); if (!res.ok) throw new Error(`HTTP ${res.status}`); })(), ), ); let failed = 0; for (let i = 0; i < keys.length; i++) { if (results[i].status === "rejected") { console.error( ` ✗ Failed to delete ${keys[i]}: ${(results[i] as PromiseRejectedResult).reason}`, ); failed++; } } if (failed > 0) { console.log(` ⚠ ${failed}/${keys.length} failed — re-run to clean up remaining`); } } else { for (const key of keys) { console.log(` [dry-run] would delete ${key}`); } }
console.log(` ✓ R2 cleaned (${keys.length} objects deleted)`);}
// ── Vectorize ──────────────────────────────────────────────────────────────
async function cleanVectorize(): Promise<void> { console.log("\n── Vectorize ──");
if (dryRun) { console.log(` [dry-run] would delete and recreate index "${VECTORIZE_INDEX}"`); return; }
// Delete the whole index, then recreate with same config try { await cfFetch(`/vectorize/v2/indexes/${VECTORIZE_INDEX}`, { method: "DELETE" }); console.log(" Index deleted"); } catch { console.log(" Index doesn't exist or already deleted"); }
await cfFetch(`/vectorize/v2/indexes`, { method: "POST", body: JSON.stringify({ name: VECTORIZE_INDEX, config: { dimensions: EMBED_DIMENSIONS, metric: "cosine", }, }), }); console.log(` ✓ Vectorize index "${VECTORIZE_INDEX}" recreated`);}
// ── Main ───────────────────────────────────────────────────────────────────
console.log(dryRun ? "══ DRY RUN ══\n" : "══ CLEANUP ══\n");
try { if (all || d1Only) await cleanD1(); if (all || r2Only) await cleanR2(); if (all || vectorizeOnly) await cleanVectorize();
console.log("\n── Summary ──"); if (all || d1Only) console.log(" D1 ✓ wiped"); if (all || r2Only) console.log(" R2 ✓ wiped"); if (all || vectorizeOnly) console.log(" Vectorize ✓ deleted + recreated");
if (all) { console.log("\nReady to backfill. Run: bun scripts/backfill-vectors.ts"); }
if (dryRun) { console.log("\n══ DRY RUN — no changes made ══"); }} catch (err) { console.error("\n✗ Cleanup failed:", err); process.exit(1);}