kog-holder-snapshot / tree / src/cli.ts ยท raw (this view has old GitHub links rewritten for the mirror; raw is byte-exact)
import { readFile as fsRead, writeFile as fsWrite } from "node:fs/promises";
import { buildSnapshot, type Exclusions } from "./snapshot.js";
import { fetchAccounts, type FetchLike, type RecordedAccounts } from "./fetch.js";

export const USAGE =
  "usage: kog-snapshot --mint <address> --exclusions <file> [--slot <n>] [--top <n>] [--out <file>] [--fixture <file>] [--save-fixture <file>]";

export interface RunDeps {
  env?: Record<string, string | undefined>;
  stdout?: (s: string) => void;
  stderr?: (s: string) => void;
  fetch?: FetchLike;
  readFile?: (path: string) => Promise<string>;
  writeFile?: (path: string, text: string) => Promise<void>;
}

class UsageError extends Error {}

const VALUE_FLAGS = new Set(["--mint", "--exclusions", "--slot", "--top", "--out", "--fixture", "--save-fixture"]);

function parseArgs(argv: string[]): Record<string, string> {
  const out: Record<string, string> = {};
  for (let i = 0; i < argv.length; i++) {
    let a = argv[i];
    let v: string | undefined;
    const eq = a.indexOf("=");
    if (a.startsWith("--") && eq > 0) {
      v = a.slice(eq + 1);
      a = a.slice(0, eq);
    }
    if (a === "--help" || a === "-h") throw new UsageError(USAGE);
    if (!VALUE_FLAGS.has(a)) throw new UsageError(`unknown option ${a}\n${USAGE}`);
    if (v === undefined) {
      v = argv[++i];
      if (v === undefined || v.startsWith("--")) throw new UsageError(`${a} needs a value`);
    }
    out[a.slice(2)] = v;
  }
  return out;
}

function wholeNumber(name: string, v: string | undefined, min: number): number | undefined {
  if (v === undefined) return undefined;
  if (!/^[0-9]+$/.test(v) || Number(v) < min || !Number.isSafeInteger(Number(v))) throw new UsageError(`${name} must be a whole number of at least ${min}`);
  return Number(v);
}

const BASE58 = /^[1-9A-HJ-NP-Za-km-z]{32,44}$/;

function readExclusions(text: string, path: string): Exclusions {
  let j: any;
  try {
    j = JSON.parse(text);
  } catch {
    throw new UsageError(`exclusions file ${path} is not valid JSON`);
  }
  if (!j || typeof j !== "object" || Array.isArray(j)) throw new UsageError(`exclusions file ${path} must be an object with owners and token_accounts`);
  for (const k of ["owners", "token_accounts"]) {
    if (j[k] !== undefined && !Array.isArray(j[k])) throw new UsageError(`exclusions ${k} must be a list`);
  }
  return { owners: j.owners ?? [], token_accounts: j.token_accounts ?? [] };
}

/** Runs the CLI and returns the exit code (0 on success, 1 on any error). Never prints the RPC URL. */
export async function run(argv: string[], deps: RunDeps = {}): Promise<number> {
  const env = deps.env ?? process.env;
  const stdout = deps.stdout ?? ((s) => process.stdout.write(s));
  const stderr = deps.stderr ?? ((s) => process.stderr.write(s));
  const readFile = deps.readFile ?? ((p) => fsRead(p, "utf8"));
  const writeFile = deps.writeFile ?? ((p, t) => fsWrite(p, t, "utf8"));
  const rpcUrl = env.SOLANA_RPC_URL;
  const scrub = (msg: string) => (rpcUrl ? msg.split(rpcUrl).join("<SOLANA_RPC_URL>") : msg);
  try {
    const args = parseArgs(argv);
    if (!args.mint) throw new UsageError(`missing --mint\n${USAGE}`);
    if (!BASE58.test(args.mint)) throw new UsageError("--mint is not a base58 Solana address");
    if (!args.exclusions) throw new UsageError(`missing --exclusions\n${USAGE}`);
    const slot = wholeNumber("--slot", args.slot, 0);
    const top = wholeNumber("--top", args.top, 1);

    let exText: string;
    try {
      exText = await readFile(args.exclusions);
    } catch {
      throw new UsageError(`cannot read exclusions file ${args.exclusions}`);
    }
    const exclusions = readExclusions(exText, args.exclusions);

    let recorded: RecordedAccounts;
    if (args.fixture) {
      let fx: any;
      try {
        fx = JSON.parse(await readFile(args.fixture));
      } catch {
        throw new UsageError(`cannot read fixture file ${args.fixture}`);
      }
      if (!fx || typeof fx.slot !== "number" || !Array.isArray(fx.accounts)) throw new UsageError(`fixture ${args.fixture} must hold {slot, accounts}`);
      if (slot !== undefined && fx.slot < slot) throw new UsageError(`fixture slot ${fx.slot} is below --slot ${slot}`);
      recorded = { slot: fx.slot, accounts: fx.accounts };
    } else {
      if (!rpcUrl) throw new UsageError("set SOLANA_RPC_URL or pass --fixture");
      recorded = await fetchAccounts({ rpcUrl, mint: args.mint, fetch: deps.fetch, minContextSlot: slot });
    }
    if (args["save-fixture"]) await writeFile(args["save-fixture"], JSON.stringify({ slot: recorded.slot, accounts: recorded.accounts }, null, 1) + "\n");

    const snap = buildSnapshot({ mint: args.mint, slot: recorded.slot, accounts: recorded.accounts, exclusions, top });
    const text = JSON.stringify(snap, null, 2) + "\n";
    if (args.out) await writeFile(args.out, text);
    else stdout(text);
    return 0;
  } catch (e) {
    stderr(`kog-snapshot: ${scrub(e instanceof Error ? e.message : String(e))}\n`);
    return 1;
  }
}

agentgate-source: code mirror on Cloudflare Pages. All repos