import "dotenv/config"; import { config } from "../src/config.js"; type ActorRecord = { actor_id: string; name: string; key?: string | null; namespace_id?: string; runner_name_selector?: string; crash_policy?: string; create_ts?: number; start_ts?: number | null; connectable_ts?: number | null; sleep_ts?: number | null; destroy_ts?: number | null; error?: unknown; }; type ListActorsResponse = { actors?: ActorRecord[]; pagination?: { cursor?: string | null }; }; type Options = { command: "list" | "flush"; endpoint: string; namespace: string; name?: string; key?: string; crashedOnly: boolean; dryRun: boolean; }; function parseArgs(): Options { const args = process.argv.slice(2); const command = args[0] === "flush" ? "flush" : "list"; const options: Options = { command, endpoint: process.env.RIVET_ENDPOINT ?? config.rivetEndpoint, namespace: process.env.RIVET_NAMESPACE ?? "default", crashedOnly: false, dryRun: false, }; for (let i = command === "flush" || args[0] === "list" ? 1 : 0; i < args.length; i += 1) { const arg = args[i]; if (arg === "--") continue; else if (arg === "--endpoint") options.endpoint = requireValue(args, ++i, arg); else if (arg === "--namespace") options.namespace = requireValue(args, ++i, arg); else if (arg === "--name") options.name = requireValue(args, ++i, arg); else if (arg === "--key") options.key = requireValue(args, ++i, arg); else if (arg === "--crashed-only") options.crashedOnly = true; else if (arg === "--dry-run") options.dryRun = true; else if (arg === "--help" || arg === "-h") printHelpAndExit(); else throw new Error(`Unknown argument: ${arg}`); } return options; } function requireValue(args: string[], index: number, flag: string) { const value = args[index]; if (!value || value.startsWith("--")) throw new Error(`Missing value for ${flag}`); return value; } function printHelpAndExit(): never { console.log(`Usage: pnpm rivet:actors:list [--name growActor] [--key user_123] [--crashed-only] pnpm rivet:actors:flush -- --name growActor [--key user_123] [--crashed-only] [--dry-run] Environment: RIVET_ENDPOINT Defaults to config.rivetEndpoint (${config.rivetEndpoint}) RIVET_NAMESPACE Defaults to default Examples: pnpm rivet:actors:list pnpm rivet:actors:flush:crashed pnpm rivet:actors:flush:grow pnpm rivet:actors:flush -- --name growActor --key user_123 `); process.exit(0); } function buildEndpoint(rawEndpoint: string) { const url = new URL(rawEndpoint); const token = url.password || process.env.RIVET_TOKEN || process.env.RIVET_ADMIN_TOKEN || "dev-admin-token"; url.username = ""; url.password = ""; return { baseUrl: url.toString().replace(/\/$/, ""), headers: token ? { Authorization: `Bearer ${token}`, "x-rivet-token": token } : {}, }; } function actorHasError(actor: ActorRecord) { return Boolean(actor.error); } async function listActors(options: Options) { const { baseUrl, headers } = buildEndpoint(options.endpoint); const actors: ActorRecord[] = []; let cursor: string | null | undefined; do { const url = new URL(`${baseUrl}/actors`); url.searchParams.set("namespace", options.namespace); if (options.name) url.searchParams.set("name", options.name); if (options.key) url.searchParams.set("key", options.key); if (cursor) url.searchParams.set("cursor", cursor); const response = await fetch(url, { headers }); if (!response.ok) { throw new Error(`Failed to list actors: ${response.status} ${response.statusText}\n${await response.text()}`); } const body = (await response.json()) as ListActorsResponse; actors.push(...(body.actors ?? [])); cursor = body.pagination?.cursor; } while (cursor); return options.crashedOnly ? actors.filter(actorHasError) : actors; } async function deleteActor(options: Options, actor: ActorRecord) { const { baseUrl, headers } = buildEndpoint(options.endpoint); const url = new URL(`${baseUrl}/actors/${encodeURIComponent(actor.actor_id)}`); url.searchParams.set("namespace", options.namespace); if (options.dryRun) return; const response = await fetch(url, { method: "DELETE", headers }); if (!response.ok) { throw new Error(`Failed to delete ${actor.actor_id}: ${response.status} ${response.statusText}\n${await response.text()}`); } } function printActors(actors: ActorRecord[]) { if (!actors.length) { console.log("No actors matched."); return; } for (const actor of actors) { console.log( [ actor.actor_id, actor.name, `key=${actor.key ?? ""}`, actor.error ? `error=${JSON.stringify(actor.error)}` : "ok", ].join("\t"), ); } } async function main() { const options = parseArgs(); const actors = await listActors(options); if (options.command === "list") { printActors(actors); return; } if (!options.name && !options.crashedOnly && !options.key) { throw new Error("Refusing to flush every actor without a filter. Pass --name, --key, or --crashed-only."); } printActors(actors); for (const actor of actors) { await deleteActor(options, actor); console.log(`${options.dryRun ? "Would delete" : "Deleted"} ${actor.name} ${actor.actor_id} key=${actor.key ?? ""}`); } } main().catch((error) => { console.error(error instanceof Error ? error.message : error); process.exit(1); });