180 lines
5.3 KiB
TypeScript
180 lines
5.3 KiB
TypeScript
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);
|
|
});
|