SPB Git

spb/cancerindex

Public
5.9 KB · 137 lines typescript
1#!/usr/bin/env tsx2/**3 * CancerIndex operator CLI.4 *5 *   pnpm cix connectors                      list connectors + health6 *   pnpm cix run <id> [--mode full|incremental|dry_run] [--max-records N] [--max-minutes M] [--reset-cursor]7 *   pnpm cix run-all [--max-minutes M]       run every active connector in registry order8 *   pnpm cix health <id>                     source liveness probe9 *   pnpm cix sources:sync                    manifests → sources table10 *   pnpm cix counters                        recompute entity counters11 *   pnpm cix rank [--scope …]                recompute ranking snapshots12 *   pnpm cix stats                           table counts13 *   pnpm cix trace <table> <id>              lineage of a value (rankings/epidemiology/frequency)14 */15import { existsSync } from 'node:fs';16import path from 'node:path';17import { sql } from 'drizzle-orm';1819for (const candidate of [path.resolve(process.cwd(), '.env')]) {20  if (existsSync(candidate)) {21    try {22      process.loadEnvFile(candidate);23    } catch {24      /* ignore */25    }26  }27}2829const [, , cmd, ...rest] = process.argv;30const flags: Record<string, string | boolean> = {};31const positional: string[] = [];32for (let i = 0; i < rest.length; i++) {33  const a = rest[i]!;34  if (a.startsWith('--')) {35    const k = a.slice(2);36    const next = rest[i + 1];37    if (next && !next.startsWith('--')) {38      flags[k] = next;39      i++;40    } else flags[k] = true;41  } else positional.push(a);42}4344async function main() {45  const { getDb, closeDb } = await import('@cancerindex/database');46  const db = getDb();47  try {48    switch (cmd) {49      case 'connectors': {50        const { CONNECTORS } = await import('@cancerindex/connectors');51        const rows = await db.execute<{ connector_id: string; health: string; last_success_at: string | null; cursor: unknown }>(sql`SELECT connector_id, health, last_success_at, cursor FROM connector_cursors`);52        const byId = new Map(rows.map((r) => [r.connector_id, r]));53        for (const c of CONNECTORS) {54          const h = byId.get(c.manifest.id);55          console.log(`${c.manifest.id.padEnd(18)} tier${c.manifest.tier} ${c.manifest.status.padEnd(20)} license=${c.manifest.licenseStatus.padEnd(10)} health=${(h?.health ?? 'never-run').padEnd(12)} last_ok=${h?.last_success_at ?? '-'}`);56        }57        break;58      }59      case 'sources:sync': {60        const { syncSources, PLANNED_MANIFESTS } = await import('@cancerindex/connectors');61        const r = await syncSources(db, PLANNED_MANIFESTS);62        console.log(`[sources] created=${r.created} updated=${r.updated}`);63        break;64      }65      case 'health': {66        const { getConnector, RunContext } = await import('@cancerindex/connectors');67        const c = getConnector(positional[0] ?? '');68        if (!c) throw new Error(`unknown connector ${positional[0]}`);69        const [src] = await db.execute<{ id: string }>(sql`SELECT id FROM sources WHERE slug = ${c.manifest.id}`);70        const ctx = new RunContext(db, c.manifest, src?.id ?? 'CI-SOURCE-00000000', 'probe', { maxMinutes: 1 }, 0);71        console.log(await c.healthCheck(ctx));72        break;73      }74      case 'run': {75        const { getConnector, runConnector } = await import('@cancerindex/connectors');76        const c = getConnector(positional[0] ?? '');77        if (!c) throw new Error(`unknown connector ${positional[0]}`);78        const r = await runConnector(db, c, {79          mode: (flags.mode as never) ?? undefined,80          maxRecords: flags['max-records'] ? Number(flags['max-records']) : undefined,81          maxMinutes: flags['max-minutes'] ? Number(flags['max-minutes']) : undefined,82          resetCursor: !!flags['reset-cursor'],83        });84        console.log(`[run] ${r.runId} ${r.status}`, r.counters);85        if (r.status === 'failed') process.exitCode = 1;86        break;87      }88      case 'run-all': {89        const { CONNECTORS, runConnector } = await import('@cancerindex/connectors');90        for (const c of CONNECTORS) {91          if (c.manifest.status !== 'active') {92            console.log(`[skip] ${c.manifest.id} (${c.manifest.status})`);93            continue;94          }95          const r = await runConnector(db, c, { maxMinutes: flags['max-minutes'] ? Number(flags['max-minutes']) : undefined });96          console.log(`[run] ${r.runId} ${r.status}`, r.counters);97        }98        break;99      }100      case 'counters': {101        const { refreshCounters } = await import('@cancerindex/ranking');102        const n = await refreshCounters(db);103        console.log(`[counters] refreshed ${n} entities`);104        break;105      }106      case 'rank': {107        const { computeAllRankings } = await import('@cancerindex/ranking');108        const res = await computeAllRankings(db);109        for (const r of res) console.log(`[rank] ${r.metricSlug} ${r.scopeKey} eligible=${r.eligible}`);110        break;111      }112      case 'stats': {113        const tables = ['sources', 'ingest_runs', 'source_records', 'provenance', 'cancers', 'cancer_aliases', 'cancer_hierarchy', 'cancer_codes', 'genes', 'variants', 'drugs', 'clinical_trials', 'trial_conditions', 'publications', 'literature_counts', 'civic_evidence_items', 'genomic_cohorts', 'cancer_gene_frequencies', 'epidemiology_observations', 'survival_observations', 'knowledge_edges', 'unresolved_labels', 'rankings', 'ranking_snapshots'];114        for (const t of tables) {115          const [r] = await db.execute<{ n: string }>(sql.raw(`SELECT count(*)::text AS n FROM ${t}`));116          console.log(`${t.padEnd(28)} ${r?.n}`);117        }118        break;119      }120      case 'trace': {121        const { traceValue } = await import('@cancerindex/ranking');122        console.log(JSON.stringify(await traceValue(db, positional[0] ?? '', positional[1] ?? ''), null, 2));123        break;124      }125      default:126        console.log('usage: pnpm cix <connectors|run|run-all|health|sources:sync|counters|rank|stats|trace>');127    }128  } finally {129    await closeDb();130  }131}132133main().catch((e) => {134  console.error(e);135  process.exit(1);136});137
Files · f2c2adc075a3468bef3da16df60342d05cf7c062
Menu

Theme